《Java多线程编程核心技术》

@author ergwang

文章末尾附pdf和png下载链接

第1章 Java多线程技能

1. 进程与线程 区别? 联系?

2. 创建多线程的方式,有几种?怎么创建

  • 继承Thread类 (一般不单独用)
  • 实现Runnable接口 + Thread对象
  • 实现Callable接口+FutureTask<>对象+Thread对象
  • 线程池 + (实现Callable接口+FutureTask<>对象)或者(实现Runnable接口)

3. Thread类的常见API

  • currentThread() 获取当前线程的信息
  • isAlive() 验证当前线程是否存活
  • sleep(long millis) 线程休眠
  • 线程堆栈相关
  • StackTraceElement[] getStackTrace()
    获取一个标识该线程堆栈跟踪元素数组
  • dumpStack()
    将当前线程的堆栈跟踪信息输出至标准错误流
    (该方法只用于调试)
  • Map<> getAllStackTraces()
    返回所有活动线程的堆栈跟踪的映射
  • getId() 获取线程的唯一标识、Id
  • 停止线程的几种方式
  • 使用退出标志使流程正常退出,
    如在线程中用使用break、抛异常等
  • 使用stop()强行终止
    【可能会丢数据,已被弃用】
  • 使用interrupt()方法中断线程
    执行这个方法后,会将线程标记为中断,
    【但还会继续执行,直到执行完毕】
  • interrupted()
    判断线程是否已经中断
    【执行后,会将中断线程状态清除!】
  • this.isInterrupted()
    判断this关键字所在的类的对象是否已中断
  • 线程的暂停与重启
  • suspend() 暂停线程
    缺点:【独占资源】【易造成数据不完整】
  • resume()
    重启线程
  • yield() 释放当前线程所占用的CPU资源
  • 线程优先级
  • getPriority() 获取当前线程的优先级
    【最大10 ,最小1】
  • setPriority() 设置当前线程的优先级
    MIN_PRIORITY = 1;
    NORM_PRIORITY = 5;
    MAX_PRIORITY = 10;
  • 优先级具有【继承性】
    A线程启动B线程,则B优先级等于A
  • setDaemon(true) 设置当前线程为守护线程
  • Java中的线程分为:
    守护线程
    用户线程(非守护线程)

第2章 线程同步(对象及变量的并发访问)

synchronized 同步方法

  • 方法内的变量永远线程安全
  • 实例变量线程不安全,怎么解决
  • 同步方法
    同步代码块
  • synchronized在字节码指令中的原理
  • 同步方法:
    在class文件中标记了ACC_SYNCHRONIZED
  • 同步代码块:
    class文件中用monitorenter和monitorexit分别表示同步代码块的开始和结束
  • 脏读问题
  • 发生脏读:
    因为读取成员变量(实例变量)时,此值已经被其他线程修改了
  • synchronized 锁重入
  • 锁重入:
    可以重复加锁,
    有几个锁加几个锁,一层套一层
  • 支持父子类继承
  • 子类同步方法调用父类同步方法,有一层锁是一层,就当是在自己方法里面一样,不会出现线程安全问题
  • 重写synchronized方法
  • 如果重写方法不加synchronized关键字,会破坏同步方法
  • 抛异常,锁自动释放
  • println() 也是同步方法,线程安全
  • 【同步方法】锁是对象的锁,不同的对象是不同的锁
  • 【缺点】一个对象中多个同步方法,用的同一个锁(都是对象锁),Thread1调用A方法一直没执行完毕,从而阻塞Thread2调用的B方法。

synchronized 同步代码块

  • 同步和异步
  • 区别和联系
  • 同锁 -> 同步
    异锁 -> 异步
  • 各有什么优缺点
  • 同步执行 成员变量线程安全 但效率低
    异步执行 成员变量线程不安全 效率高
  • 什么时候用同步/异步?
  • 操作同一个成员变量的不同方法,要用同一把锁,保证成员变量的线程安全,如果是不同业务,比如执行完毕后发送邮件,就异步执行
  • 同步代码块相比于同步方法的优势
  • 同步方法的锁: 同步方法所在类的对象
    同步代码块的锁: 很灵活, 可以是当前方法所在类的对象,也可以是其他类对象(比如Class类对象,String类对象等)
  • 锁对象
  • this
  • 当前方法所在类的对象
  • 同步方法的锁对象
  • 当前方法所在类的对象
  • 类名.class
  • Class类的对象
  • 静态同步方法的锁对象
  • Class类的对象
  • 其他类对象
  • 如String、Object等类对象
  • 【注1】String做锁对象时
  • 受JVM常量池的影响,如果String的值被改变了,锁就变成了不同锁,会造成线程异步进而可能导致线程不安全问题
  • 【注2】锁对象修改对线程同步的影响
  • String常量修改 =》 会影响线程锁
  • 对象属性修改 =》 不会影响线程锁
  • 死锁问题
  • 两个线程相互等待对方释放自己的锁

synchronized 同步写法总结

public class MyService{
    public synchronized static void method1(){}
    public static void method2(){
         ssynchronized(MyServcie.class){}
    }
    public synchronized void method3(){}
    public static void method4(){
         ssynchronized(this){}
    }
    public static void method5(){
         ssynchronized("abc"){}
    }
  }

(A) method1 和 method2 持有同一把锁,即:MyService.java 对应的 Class类对象(其实就是Class对象)
(B) method3 和 method4 持有同一把锁,即:MyService.java类的对象 (等同于this)
(C) method5 持有的锁是字符串对象

synchronized 关键字

  • 原子性
  • 使用synchronized实现了同步,同步实现了原子性,保证了被同步的代码段在同一时间只被一个线程操作,故实现了原子性
  • 可见性
  • B线程马上就能看到A线程修改的数据
    【原理】用volatile或者synchronized修饰的方法,修改读取变量时,强制从公共堆栈读,不从线程私有堆栈中读取。
    【注】线程修改数据,写都是写到公共堆栈中
  • 禁止代码重排序
  • JAVA程序运行时,JIT(即时编译器)会根据代码执行时间等动态调整执行顺序,而volatile和synchronized关键字会隔断这种调整,隔断成两块后,前面可以内部调整,后面可以内部调整,但是两块之间不能相互调整了

volatile 关键字

  • 原子性
  • 32位系统中,未用volatile声明的long和double数据类型是非原子的,64位则要根据具体实现判断。
    【注】无论32位还是64位,无论用不用volatile声明,i++ 都是一个非原子操作,除非用AtomicInteger声明变量
  • 可见性
  • 和synchronized一样,都是强制从公共堆栈读,不从线程私有堆栈中读取。
  • 禁止代码重排序
  • 和synchronized一样

总结

  • synchronized关键字
  • 作用:保证同一时刻,只有一个线程能执行某个方法或代码块。
    修饰:可以修饰方法、代码块。
    特征:可见性、原子性、禁止代码重排序。
    使用场景:多个线程对同一个对象中的同一个成员变量变量操作时,为了避免出现线程安全问题时使用。
  • volatile关键字
  • 主要作用:让其他线程能看到修改后的最新的值。
    修饰:只能修饰变量。
    特征:可见性、原子性、禁止代码重排序。
    使用场景:欲实现一个变量在被A线程修改后,其他线程立马能获取到最新值时候,就用volatile修饰这个变量。

第3章 线程间通信

wait / notify 机制

  • 原理
  • 持有相同锁(对象级别的)的多个线程,在wait()处暂停执行,释放锁,直到接到通知,notify() 或者 notifyAll()再获取锁,继续执行。
    【注】锁必须是对象级别的,因为wait(), notify(), notifyAll()是Object类中的,不是Thread类中的方法,所以必须要对象。
  • wait()
  • 暂停当前线程,释放锁
    【注】执行wait()方法后,马上暂停线程么,并释放锁
  • notify()
  • 发出通知,wait状态的线程可以准备获取锁,开始执行了,只能通知“一个”线程,唤醒顺序同执行wait()顺序一致
    【注】执行notify()方法后,不是马上暂停当前线程,而是要将当前线程同步代码块执行完毕后,才释放锁,故wait状态的线程也要等它执行完毕才能抢到锁
  • notifyAll()
  • 发出通知,wait状态的线程可以准备获取锁,开始执行了,默认按照执行wait()相反的顺序依次唤醒全部线程
    【注】锁释放时机同notify()
  • 释放锁的时机
  • wait() 立即释放
  • notify() / notifyAll() 同步代码块执行完毕后再释放
  • wait(long time)
  • 如果线程超过time时间没有被唤醒,则自动醒来,但是要执行的前提条件是再次持有锁,没有持有锁的话,一直等待,直到拿到锁才开始执行
    time的单位为毫秒,其他用法一致
  • wait() 与 sleep()的区别
  • wait() 线程阻塞,立即释放锁
    sleep() 线程阻塞,不 释放锁
  • 用while替代if
  • 执行wait()后,线程阻塞,当线程醒来的时候
  • 用if 不会再判断条件,直接运行
  • 用while 会再次判断条件,满足条件才运行
  • 生产者消费者模型的几种实现
  • 不带缓冲区
  • 1生产 1消费 操作值
  • 多生产 多消费 操作值
  • 带缓冲区
  • 1生产 1消费 操作栈
  • 1生产 多消费 操作栈
  • 多生产 1消费 操作栈
  • 多生产 多消费 操作栈
  • 管道流通信
  • 特殊的流,用于在不同的线程间直接传输数据
  • 字节流
  • 字符流
  • 【案例】利用wait/notify实现数据库交叉备份

join() 方法的使用

  • 使用场景
  • 主线程要获取子线程的结果时,要用到join(),和使用Callable接口和FutureTask效果类似
  • join() 原理
  • 主线程中,子线程调用此方法,则会相当于主线程执行wait()方法,直到子线程执行完毕,会通知主线程,主线程拿到锁以后继续执行
  • join() 与 sleep() 的区别
  • join() 是线程间通信,释放锁 (主线程执行wait(),子线程执行完毕后通知主线程)
    sleep() 是线程内部通信,不释放锁
  • join(long time)
  • 不管子线程是否执行完毕,到时间后,且主线程拿到锁后,主线程就继续往下执行
  • join() 和 interrupt() 同时使用出现异常
  • 彼此遇到会出现 InterruptedException,终止的是主线程,子线程还在继续
  • join(long time) 后面的代码可能提前运行,其实就是锁的问题

类ThreadLocal 的使用

  • 原理及作用
  • get() / set()
  • 隔离性验证
  • 重写initialValue() 方法解决get() 方法返回null的问题
  • 不能实现值继承

类InheritableThreadLocal 的使用

  • 原理及其作用
  • 验证
  • 子线程将父线程中的table对象以 复制 的方式赋值给子线程的table
  • 值继承
  • 父线程新值 子线程旧值
  • 父线程旧值 子线程新值
  • 对象继承
  • 父子统一
  • 重写childValue() 方法,对值进行加工

线程的生命周期

java 多线程 核心 java多线程核心技术ppt_同步方法

  • 图源:

第4章 Lock 对象的使用

使用ReentrantLook类

重进入锁

  • 相比于synchronize关键字实现线程间同步,ReentrantLook更加灵活,如具有嗅探锁定、多路分支通知等功能
  • 线程间通信
  • ReentrantLock实现了java.util.concurrent.locks包中的Lock接口,利用ReentrantLock对象中的lock()和unlock() 方法可以实现线程间同步,ReentrantLock具有互斥排他性。
  • 结合Condition对象实现线程间通信
  • 利用Condition对象的await()和signal() 方法实现wait()/notify()机制
  • 线程对象注册在不同的Condition中,可以实现有选择性地进行线程通知(多路分支通知)
  • 【注】必须在condition.await() 之前调用lock.lock()获取锁,因为Condition对象的await()方法执行后会将线程转换为wait状态,并释放锁
  • await() 方法暂停线程运行的原理
    (这一块暂时不理解)
  • 内部执行了Unsafe类中的park() 方法
  • 生产者消费者模型
  • 公平锁与非公平锁
  • 公平锁
  • 先用先得,必须排队,排队尾
  • 非公平锁
  • “有机会插队”,先抢锁,抢不到再排到队尾去
  • 实现
  • ReentrantLock默认是非公平锁
  • 构造公平/非公平锁的构造函数
    public ReentrantLock(boolean fair)
    true => 公平锁
    false => 非公平锁
  • API
  • public int getHoldCount()
  • 查询“当前线程”保持锁定的个数,
    即调用lock()方法的次数
  • public final int getQueueLength()
  • 获取正等待获取此锁的估计数,
    【注】已经获取锁的不算
  • public int getWaitQueueLength(Condition condition)
  • 获取与此锁相同的condition且正等待中的线程估计数
  • public final boolean hasQueuedThread(Thread thread)
  • 判断参数中的线程是否在等待获取锁的队列中
  • public final boolean hasQueuedThreads()
  • 判断是否在等待获取当前锁的队列中
  • public final boolean hasWaiters(Condition condition)
  • 查询是否有线程执行了参数中的condition.await() 方法而呈等待状态
  • public final boolean isFair()
  • 判断当前锁是不是公平锁
  • public boolean isHeldByCurrentThread()
  • 判断当前线程是否持有当前锁
  • public final boolean isLocked()
  • 判断当前锁是不是已经被获取且没有释放
  • public void lockInterruptibly()
  • 当某个线程尝试获得锁并且阻塞在lockInterruptibly() 方法时,该线程可以被中断
  • public boolean tryLock()
  • 嗅探拿锁,判断当前锁能不能拿到(没有被持有),能就拿到返回true
  • public boolean tryLock(long timeout, TimeUnit unit)
  • 嗅探拿锁,判断当前锁能不能在有限时间内(timeout)拿到(没有被持有),能就拿到返回true
  • public boolean await(long timeout, TimeUnit unit)
  • 线程等待,一段时间后自动唤醒线程,单位毫秒
  • public long awaitNanos(long nanosTimeout)
  • 线程等待,一段时间后自动唤醒线程,单位纳秒
  • public boolean awaitUntil(Date deadline)
  • 线程等待,在deadline时自动唤醒,单位毫秒
  • public void awaitUninterruptiably()
  • 线程等待过程中,不允许被中断

使用ReentrantReadWriteLook类

读写锁

  • 原理与使用
  • 读操作相关的锁——共享锁
  • 写相关的锁——排他锁
  • 读写分离,提高系统效率
    因为读几乎不需要同步,大家都可以读,但是写必须保证数据同步,所以可以只加“写锁”,不加“读锁”
  • Demo
  • ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ……
    lock.readLock().lock();
    lock.writeLock().lock();
    ……
    lock.readLock().unlock();
    lock.writeLock().unlock();
  • 读读共享
  • 写写互斥
  • 读写互斥
  • 写读互斥

第5章 定时器 Timer

原理及使用

  1. MyTask继承TimerTask类,重新run()方法
  1. 新建Timer对象,调用其不同的schedule方法实现定时任务
  • 创建Timer对象的时候,会启动一个新的非守护线程TimerThread,且线程中存在一个死循环导致线程一直运行,可以调用timer.cancel()方法终止计时器
  • timer.cancel()方法
    在多任务的Timer对象中调用时,会优先清空任务队列(已经在执行的任务不受影响),然后再销毁进程
  • 多任务Timer对象中执行task任务的算法
    每次最后一个执行的放入队列头,
    如:第一次 ABC,
    第二次 CAB,
    第三次 BCA
  • 执行情况
  • 正常执行
    单个TimerTask任务
    多个TimerTask任务,但时间没有交集,
  • 立即执行
    计划执行的时间早于当前时间
  • 延时执行
    因为前面的任务可能消耗的时间比较长,后面的任务就会被延后

API

  • schedule(TimerTask task, Date time)
  • 指定时间执行一次某任务
  • schedule(TimerTask task, Date firstTime, long period)
  • 指定时间之后,按照间隔时间无限循环执行某一个任务
  • schedule(TimerTask task, long delay)
  • 以当前时间点为基准,delay毫秒后执行一次任务
  • schedule(TimerTask task, long delay, long period)
  • 以当前时间点为基准,delay毫秒后
    按照间隔时间无限循环执行任务
  • scheduleAtFixedRate(TimerTask task, long firstTime, long period)
  • 与schedule()方法一样,只是加了【追赶性】
    【大白话】这个方法就是要把无论因为某某原因导致延迟、没执行的任务,全部补上,补上后就和schedule()一样了

第6章 单例模式与多线程

单例模式特征

  • 单例模式特点:
  1. 一个类只有一个实例
  2. 自行实例化,并且向整个系统提供
  3. 反序列化时不会重新实例化对象
  • 实现关键点:
  1. 私有化构造函数
  2. 自行实例化单个对象
  3. 使用静态方法提供自行实例化的单个对象

单例模式的使用场景

  • 最好只能有一个对象存在的场景时使用单例模式。
    如:1. 日志系统,只需要一个日志系统记录全局的
    2. id生成器,保证id唯一性,单例更合适
    3. 计时器、计数器,都是保证数据准确
    4. 多数多线程的线程池,方便线程管理
    5. 数据库连接,资源重用,减少频繁开关的资源消耗

几种不同单例模式的实现

  • 饿汉模式 / 立即加载
  • 类加载的时候实例化
    优点:线程安全
    缺点:
  1. 资源可能浪费
  2. 不能出现其他实例变量(不能传参),否则可能线程不安全
  • 懒汉模式 / 延迟加载
  • DCL 双检查锁
  • 第一次调用时实例化
    优点:延迟加载,可能节省资源
    缺点:必须实现同步,否则线程不安全
  • DCL——Double-Check Locking,双检查锁
    保证线程安全,提高多线程下效率
  • 用volatile修饰实例变量的必要性
  1. 实现实例变量线程间可见
  2. 禁止实例化时代码重排序
  • 静态内部类
  • 第一次调用时实例化
    优点:
  1. 线程安全
  2. 延迟加载
    缺点:
    实例化时不能传参
  • 实现原理:
    利用类加载的特点(即:外部类加载时,并不需要立即加载内部类,内部类不被加载则不实例化)实现单例实例化和延迟加载,又因为静态内部类线程安全的特性,保证了整体的线程安全。
    【内部类线程安全的原理不理解,摘抄自网上】
  • 内部类线程安全的原理:
    虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。)
    【引用】
  • static代码块
  • 原理
    利用静态代码块的特性(使用类时加载静态代码块)实现
    优点:
  1. 延迟加载
  2. 线程安全
    缺点:不能传参
  • 【我感觉】这东西就是饿汉模式实现了延迟加载,也是一种懒汉模式的实现吧
  • enum枚举
  • 原理
    利用枚举类的特性(使用枚举类时,自动调用其构造方法)实现
    优点:延迟加载、线程安全
    缺点:不能传参
  • 和静态代码块实现差不多呀。。。。

序列化与反序列化

  • 使用默认反序列行为
    单例模式下的对象也会变成多例
  • 保证序列化和反序列化后单例的条件:
  1. 单例模式的类必须实现Serializable接口
  2. 且必须在同一个类中实现序列化和反序列化操作
  3. 且反序列化必须调用readResolve()方法

第7章 拾遗补漏

线程的状态

  • 状态信息存储在Thread.State枚举类中
  • 线程的五种状态
  • NEW
  • 至今尚未启动的线程状态
  • RUNABLE
  • 正在Java虚拟机中执行的线程状态
  • BLOCKED
  • 受阻塞,且等待某个监视器锁的线程状态
  • WAITING
  • 无限等待另一个线程执行某一操作的线程状态
  • TIMED_WAITING
  • 有时间限制地等待另一个线程执行某一操作的线程状态
  • TERMINATED
  • 已退出的线程状态
  • 线程的生命周期

java 多线程 核心 java多线程核心技术ppt_java 多线程 核心_02

  • 根据书上的图简化了一点

线程组

  • 线程组实现和特性
  • ThreadGroup类
  • 一级关联(常用)
    多级关联(不常用)
  • 线程自动归组属性
  • ThreadGroup类中的API
  • activeCount()
  • 获取当前线程组中子线程组的数量
  • enumerate(ThreadGroup array[])
  • 将当前线程组中的子线程 复制 到数组中
  • enumerate(ThreadGroup array[], boolean recurve)
    true 递归复制,false 非递归
  • ThreadGroup.getParent()
  • 获取父线程组
  • 【根线程组】system,再往上就抛空指针了
  • Thread类中的API
  • activeCount()
  • 返回 当前线程 的线程组 中活动线程的数目
  • enumerate(Thread tarray[])
  • 调用 当前线程 的线程组 的enumerate()方法
  • 再次实现线程执行有序性
  • “没看懂这个。。。。”

SimpleDateFormat类 非线程安全

  • 非线程安全原因:单例
  • 解决办法:
  1. 多例,每次使用都new一个
  2. 使用ThreadLocal保证其线程安全

线程中异常处理

  • 异常处理
  • 线程中处理
  • 线程组内处理
  • 异常处理优先级
  • 调用过setUncaughtExceptionHandler()方法的优先处理,其他的不处理了

思维导图

PDF下载: https://img.lyy52.wang/blogs/Java多线程编程核心技术.pdf

PNG下载:https://img.lyy52.wang/blogs/Java多线程编程核心技术.png