《Java多线程编程核心技术》知识梳理

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

@author ergwang
https://www.cnblogs.com/ergwang/
文章末尾附pdf和png下载链接

第1章 Java多线程技能

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

  • 这篇博客记录了
    https://www.cnblogs.com/ergwang/p/15997503.html

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多线程编程核心技术》知识梳理_第1张图片

  • 图源:https://blog.csdn.net/dongcheng_2015/article/details/116848394

第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. 禁止实例化时代码重排序

    • 静态内部类

      • 第一次调用时实例化
        优点:
  3. 线程安全

  4. 延迟加载
    缺点:
    实例化时不能传参

    • 实现原理:
      利用类加载的特点(即:外部类加载时,并不需要立即加载内部类,内部类不被加载则不实例化)实现单例实例化和延迟加载,又因为静态内部类线程安全的特性,保证了整体的线程安全。
      【内部类线程安全的原理不理解,摘抄自网上】

    • 内部类线程安全的原理:
      虚拟机会保证一个类的 ()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待,直到活动线程执行 ()方法完毕。如果在一个类的 ()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行 ()方法后,其他线程唤醒之后不会再次进入 ()方法。同一个加载器下,一个类型只会初始化一次。)
      【引用】 https://blog.csdn.net/mnb65482/article/details/80458571

    • static代码块

      • 原理
        利用静态代码块的特性(使用类时加载静态代码块)实现
        优点:
  5. 延迟加载

  6. 线程安全
    缺点:不能传参

    • 【我感觉】这东西就是饿汉模式实现了延迟加载,也是一种懒汉模式的实现吧

    • enum枚举

      • 原理
        利用枚举类的特性(使用枚举类时,自动调用其构造方法)实现
        优点:延迟加载、线程安全
        缺点:不能传参
      • 和静态代码块实现差不多呀。。。。

序列化与反序列化

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

第7章 拾遗补漏

线程的状态

  • 状态信息存储在Thread.State枚举类中

  • 线程的五种状态

    • NEW

      • 至今尚未启动的线程状态
    • RUNABLE

      • 正在Java虚拟机中执行的线程状态
    • BLOCKED

      • 受阻塞,且等待某个监视器锁的线程状态
    • WAITING

      • 无限等待另一个线程执行某一操作的线程状态
    • TIMED_WAITING

      • 有时间限制地等待另一个线程执行某一操作的线程状态
    • TERMINATED

      • 已退出的线程状态
  • 线程的生命周期

《Java多线程编程核心技术》知识梳理_第2张图片

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

线程组

  • 线程组实现和特性

    • 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

你可能感兴趣的:(《Java多线程编程核心技术》知识梳理)