未经允许所有内容都可都可都可转载,不对的地方欢迎指正

ThreadLocal

ThreadLocal是什么

  1. ThreadLocal叫线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
  2. 该变量属于当前线程独有的变量,ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部变量的副本。
  3. ThreadLocal变量,在不同的Thread中有不同的副本

1) 每个Thread内有自己的实例副本,该副本只能由当前Thread使用,这也是ThreadLocal命名由来。 2) 既然每个Thread有自己的实例副本,且其他Thread不可以访问,那就不存在多线程之间的共享问题。

ThreadLocal与Synchronized区别, 不一起使用!!!

  1. ThreadLocal 实际上是与线程绑定一个变量。
  2. ThreadLocal与Synchronized都用于解决多线程的并发访问。

1) Synchronized用于线程间的数据共享 2)ThreadLocal用于多线程间的数据隔离 3) Synchronized是利用锁机制,是变量或者代码块某一时刻只能被一个线程访问 4) ThreadLocal为每个线程都提供了变量的副本,使每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的共享。

  1. 向Thread里面存东西就是向他里面的Map中存东西,然后ThreadLocal把这个Map挂到当前线程底下,这样map就只属于这个线程了。

ThreadLocal的简单使用

  1. public class Test {
  2. private static ThreadLocal<String> threadLocal
  3. = new ThreadLocal<>();
  4. public static void main(String[] args) {
  5. new Thread(){
  6. @Override
  7. public void run() {
  8. threadLocal.set("thread_1");
  9. System.out.println(threadLocal.get());
  10. threadLocal.remove();
  11. System.out.println(threadLocal.get());
  12. }
  13. }.start();
  14. new Thread(){
  15. @Override
  16. public void run() {
  17. threadLocal.set("thread_2");
  18. System.out.println(threadLocal.get());
  19. threadLocal.remove();
  20. System.out.println(threadLocal.get());
  21. }
  22. }.start();
  23. }
  24. }

运行结果:

thread_1
null
thread_2
null

从上面的代码运行结果可以看到,两个线程分别获取了自己线程存放的变量,他们之间的变量获取并不会错乱。

ThreadLocal原理

ThreadLocal的set(T value)方法:

    public void set(T value) {
        // 1. 获取当前线程
        Thread t = Thread.currentThread();
        // 2. 获取当前线程中的属性threadLocalMap
        ThreadLocalMap map = getMap(t);
        // 如果当前线程的map不为空,更新map中的值,this,即当前localThread对象
        if (map != null)
            map.set(this, value);
        else
            // 如果当前线程map为空,创建ThreadLocalMap并给赋值给当前的线程
            createMap(t, value);
    }

    // getMap(Thread t)方法
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // 实际上 ThreadLocalMap是LocalThread的一个内部类
    static class ThreadLocalMap {...}
    // 同时也是Thread的一个属性
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

    // createMap(Thread t, T t)方法
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

综上:

  1. 当一个线程调用LocalThread的set方法时,会首先通过当前线程的属性ThreadLocalMap.
  2. 如果当前线程的ThreadLocalMap不为空,直接更新值。key值为LocalThread对象。
  3. 如果当前线程的ThreadLocalMap为空,创建ThreadLocalMap并赋值给当前线程。

ThreadLocalMap的源码

   static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            // 内部存储数据使用的是Entry
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocal中的get方法

    public T get() {
        // 1. 获取当前线程
        Thread t = Thread.currentThread();
        // 2. 获取当前线层的ThreadLocalMap属性
        ThreadLocalMap map = getMap(t);
        // 如果当前线程的ThreadLocalMap不为空,从threadLocalMap中获取值返回
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果ThreadLocalMap为空,如果当前线程的ThreadLocalMap属性为空,初始化并复制value为null返回。
        return setInitialValue();
    }

// setInitialValue方法
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
// initialValue方法
    protected T initialValue() {
        return null;
    }

ThreadLocal的remove方法

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

// ThreadLocalMap中的remove(LocalThread t)方法:
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove方法,获取了当前线程的ThreadLocalMap方法,并从中移除了当前的ThreadLocal的key。

ThreadLocal、Thread、ThreadLocalMap的关系

  1. 每个Thread都有一个ThreadLocalMap属性,该属性为ThreadLocal的内部类。
  2. ThreadLocalMap中存储的数据的是Entry,而Entry中存储对象的是K,V。K即ThreadLocal对象,Value即需要隔离的对象。
  3. Thread内部的ThreadLocalMap是由ThreadLocal维护的。正删改查均是。
  4. 对于不同的线程,没获取副本值时,别的线程并不能获取当前线程的副本值,因为是通过Thread获取器ThreadLocalMap私有属性,再获取副本变量的。这样就形成了副本的隔离。互不干扰。

为什么使用弱引用?

// 1.初始化一个LocalThread
private LocalThread<String> lt = new LocalThread("JackMa");

// 2.执行业务逻辑
public void method1(){
  ...
  // 3.业务逻辑执行完成,LocalThread设置为null.
}

// 4.业务逻辑某部门执行完成,即上述methond1执行完,线程并未结束

多线程 - 图1

  1. 如上图所示,ThreadLocalMap的生命周期是和线程相同的。
  2. 如果在某个线程执行过程中,LocalThread对象被设置为null。理论上他已经是垃圾对象,应该被回收。
  3. 但是由于线程对象不是垃圾,间接的ThreadLocalMap也不是垃圾。Entry同样不是垃圾。而entry的key如果是个强引用指向 LocalThread对象。此时LocalThread对象也就无法被回收。会造成内存泄漏。
  4. 可以看到value同样有这个的问题,如何解决的。 源码中的 get set remove 方法都会遍历table数组清除key为null的value.
  5. 因此,在单个线程使用完threadLocal时,一般需要调用threadLocal的remove方法,清除value.

ThreadLocal使用场景

  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  2. 线程间数据隔离。
  3. 进行事物操作,用于存储线程事物信息。
  4. 数据库连接,Session回话管理。
  5. Spring事物

1) Spring框架在事物开始时,会给当前线程绑定一个 Jdbc Connection 2) 在整个事物过程中都是使用该线程绑定的connection来执行数据库操作,实现事物隔离性。 3) Spring框架中就是使用ThreadLocal来实现这种隔离。

多线程常见术语

进程

  1. 应用程序由数据和指令组成,指令要运行,数据要读写,就必须将指令加载进CPU,在指令运行过程中,还需要用到磁盘,网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  2. 当一个应用程序被运行,就必须将指令加载进内存,就开启了一个进程。
  3. 进程可以视为一个应用程序的实例,有的应用程序可以运行多个实例进程,有的只能启动一个实例进程。

线程

  1. 一个进程可以分为一个或者多个线程。
  2. 一个线程就是一个指令流,指令流中的一条条指令以一定的顺序交给CPU执行。

进程和线程的对比

  1. 进程间的通信比较麻烦,需要通过网络和一定协议。比如http,ssh等。
  2. 线程间的通信比较简单,因为他们共享进程内的内存,比如java中的多线程可以访问一个共享变量。

并行

  1. 同一时间做多件事情的能力
  2. 多核CPU上不同线程的同时运行。
  3. 一般应用程序既又并行,又有并发。

并发

  1. 同一时间应对一件事情的能力。
  2. 线程轮流使用CPU的做法称为并发。
  3. 在同一个CPU上,多个线程抢占CPU资源,轮流执行。由于任务调度器将CPU的时间切片分的很小,人类感觉就是同时运行的。总结一句话,围观串行,宏观并行。

查看进程线程的方法

  1. 查看所有进程: ps -ef
  2. 查看某个进程的所有线程 ps -fT -p PID, ps -H -p PID
  3. 杀死进程 kill PID
  4. 查看所有java进程 jps
  5. 查看某个java进程的所有线程状态 jstack PID
  6. jconsole连接

线程上下文切换

  1. CPU的时间片用完。
  2. 垃圾回收,会STW.
  3. 线程自己调用了sleep、yield、wait、join、park、synchronized、lock方法。
  4. 当Context Switch发生时,需要由操作系统保存当前线程的状态。并恢复另一个线程的状态。java中对用的概念就是程序计数器。他的所用是记住下一条JVM指令的执行地址。线程私有。频繁地发生上下文切换会影响性能。

守护线程

  1. 默认情况下,java进程需要等所有线程执行结束,才会结束。
  2. 默认情况下,如果线程A启动了线程B(非守护线程),在B没有执行完成的情况下,A也不会结束。
  3. 如果B为守护线程,A自己执行到最后一条指令后,不会等待B执行完成,A会执行结束。 同时B也会被强制结束。
  4. 设置守护线程的方法 setDaemon(true).
  5. 垃圾回收线程就是一种守护线程。
  6. Tomcat总的Acceptor和Poller线程都是守护线程。
  7. setDaemon(true)必须在start之前设置,否则会抛出illegalThreadStateException异常。不能把正在运行的常规线程设置有守护线程。
  8. 在Daemon线程中产生的线程也是守护线程。

Java中的6种线程State

java中的线程状态用6个enum表示

  1. NEW 被new出来,没有调用过start方法。
  2. RUNNABLE,当调用了start后,会变为RUNNABLE状态。
  3. BLOCKED,等待monitor lock.
  4. WAITING, 等待状态,调用了Object的wait方法。
  5. TIMED_WAITING,调用了sleep方法后,等待对应的时间。
  6. TERMINATED, 终止状态,已经执行完成。

临界区 就是这个

  1. 一个程序运行多个线程本身没有问题
  2. 问题出在多线层访问共享资源,多个线程对共享资源读取也没有问题,问题出在多个线程对共享资源的写操作,发生指令交错,就会出现问题。
  3. 一个代码块内,如果存在对共享资源的多线程读写操作,这个代码块就称为临界区。

线程安全解决方案

  • 阻塞式解决方案

    synchronized:

    1. 采用互斥的方法让同一个时刻只能有一个线程持有锁对象。
    2. 其他线程采用互斥锁就能锁住
    3. 保证有锁的线层安全的执行临界区中的代码,不用担心上下文切换。

    ReentranLock: 不同于synchronized:

    1. 可中断
    2. 可设置超时时间
    3. 可设置为公平锁
    4. 支持多个条件变量

    1) 可以通过ReentranLock对象.newCondition()设置多个条件变量。 2) 这样就可以防止虚假唤醒,通过singlenal唤醒await的特定条件的线程集合。 相同点:

    1. 都支持可重入,可重入是指同一个线程首次获取了这把锁,因为他是这把锁的拥有者,因此有权利再次拥有这把锁。不可重入的意思是第二次获取就会被锁住,获取不到。
  • 非阻塞式解决方案

    JUC的原子变量

变量的线程安全

  • 成员变量和静态变量

    1. 如果没有共享,则线程安全。
    2. 如果共享了,只有读操作,线程安全。
    3. 如果共享且有写操作,这段代码是临界区,需要考虑线程安全。
  • 局部变量

    1. 局部变量本身是线程安全的。
    2. 但局部变量引用的对象需要区分

    1) 如果局部变量引用的对象逃离了方法的作用范围,需要考虑线程安全。 2) 局部变量引用的对象没有逃离方法的作用范围,线程安全。

常见的线程安全类

  1. String 、Integer、StringBuffer、Bandom、Vector、HashTable、java.util.concureent包下的类,即JUC下的类。
  2. 此处的线程安全指的是,多个线程调用他们同一个实例的某个方法时,是线程安全的。
  3. 可理解为:线程安全的类,他的每个方法是原子的。
  4. 但是他们的多个方法组合时,并不是原子的。

Monitor

  1. monitor对象为虚拟机实现的对象,程序中无法获取。
  2. 每个java对象可以关联一个monitor对象。若使用synchronized给对象上锁之后,该对象头中的markword就会指向monitor对象。
  3. 多个线程共用一把锁,即多对一,一个锁对象的一个monitor,多个线程和monitor的关系也是多对一。
  4. 一个monitor对象记录的信息:

1) waitSet: 通过调用wait方法,等待被notify的线程对象集合,状态为waiting. 2) EntryList: 指向执行到synchronized后等待锁的多个线程,他们的状态是Blocked。 3) owner: 指向当前正在执行的线程对象。状态为runnable.

多线程 - 图2

JVM对重量级锁的优化

重量级锁

  1. 每个锁对象都对应了一个monitor对象
  2. 有owner时对象头中记录的信息为10
  3. 没有owner时对象头中记录的信息为01
  4. 线程的上下文切换锁住的对象,需要记录信息。
  5. 线程的上下文切换,与锁住的对象关联的monitor对象,需要切换owner对象。

轻量级锁

  1. 使用场景:一个对象虽然有多个线程访问,但时间都是错开的,即没有竞争。
  2. 实际语法没有变,JVM底层优化,不创建monitor对象。
  3. 实现原理:

1) 每个线层的栈帧都会记录一个包含锁记录的结构。内部存储锁对象的markdown. 2) 当前线程执行临界区中的代码获取锁时,尝试用栈帧中的锁记录结构(00)通过cas操作替换对象的对象头中的markword信息(01)。 替换成功: 获取锁成功。 替换失败:如果是本线程持有轻量锁,添加一条LockRecord作为重入计算。 如果是其他线程已经持有了该对象的轻量级锁。进入膨胀锁。

膨胀锁

  1. 当线程通过栈帧中存储的锁数据结构的00和锁对象的头信息01交换的时候,如果锁对象的头信息非01,cas操作就会失败。
  2. 失败后会为锁对象重新申请重量级锁monitor,并让锁对象指向重量级锁。
  3. 自身进入monitor对象中的monitor中,Blocked.
  4. 当之前已经获取锁对象的线程解锁时,之前肯定是轻量级锁,需要通过cas操作恢复值给对象头,必定失败。此时会进入重量级锁的释放流程。即将monitor中的owner置空,唤醒EntryList中的Blocked线程。

自旋优化

  1. 重量级锁在竞争的时候,可以通过自旋来进行优化,如果当前线程已经自旋成功,就可以获取锁避免阻塞(这里的阻塞指monitor被置空后,在entryList中的线层被唤醒的整个等待时间)。
  2. 自旋成功:即当monitor对象中的owner被置为null时,获取到monitor对象并将owner中的对象设置为自身。
  3. jdk6之后的自旋时自适应的,比如对象刚刚的一次自旋成功过,JVM会认为自旋成功的几率较高,会多自旋,反之减少自旋甚至不自旋。
  4. 自旋占用CPU时间,JDK7之后不能开启是否开启自旋。

偏向锁

  1. 轻量级锁在没有竞争时,每次重入仍然需要cas.
  2. 只有第一次使用cas将线程id设置到对象头。
  3. 之后发现线程id是自己的就表示没有竞争,不用重新cas.
  4. 只要不发生竞争,整个对象就归该线程所有。
  5. 偏向锁的状态默认是打开的,对象头markword中的值为101.第一个1表示开启偏向。
  6. 偏向锁时延迟加载的,可以禁用 -xx:-UseBiasedLocking
  7. 当有其他线程进入竞争,偏向锁会进入重量锁。

同步模式之保护性暂停

  1. 即一个线程等待另一个线程的执行结果。
  2. 要点: 有一个结果需要从一个线程传递到另一个线程。
  3. 如果有一个结果不断从一个线程产生到另一个线程。那么可以使用消息队列。即生产者消费者。
  4. JDK中,join的实现,Future的实现,就是采用此模式。
  5. 因为要等待另一个线程的结果,因此归类到同步模式。

线程活跃性

  1. 死锁

1) 发生的条件 最少两个线程,两把锁。 2) t1线程持有A对象锁,需要获取B对象锁。 3) t2线程持有B对象锁,需要获取A对象锁。

  1. 定位死锁

1) 使用jps查询java进程号,通过jstack PID打印栈信息。 2) 通过jconsole工具连接,直接点击检测死锁。

  1. 活锁

1) 出现在两个线程相互改变对象的结束条件,最后谁也无法结束。

  1. 饥饿

1) 一个线程的优先级太低,始终无法获取到CPU的调度执行。

共享锁和独占锁

  1. 共享锁
    1. 允许多个线程同时获得锁,如semaphore,CountDownLath,ReadLock等。
    2. 使用AQS的acquireShared和releaseShared实现。
  2. 独占锁
    1. 每次只能一个线程持有锁,如ReentrantLock,synchronized,writeLock等。
    2. 使用AOS的acquire和release实现。

Synchronized和Lock的区别

  1. synchronized是一个关键字,lock是一个类。
  2. 加锁方式:synchronized作用在方法上,锁定的是当前类类的,作用在同步代码块中,锁定的是指定对象。
  3. 释放锁:synchronized释放锁,JVM自动释放。 lock需要在finally中手动释放。
  4. 可重入: 都支持。
  5. 锁状态: synchronized无法判断,lock可以判断。
  6. 中断: synchronzed不可中断。 lock可中断
  7. 公平: synchronized不能设置公平锁,lock可以。
  8. 底层实现不同
    1. synchronized底层是monitor重量锁。
    2. lock底层实现是状态值+cas+双向链表,即AQS.
      1. int状态值:用于锁状态变更。
      2. 双向链表:存储等待线程。
      3. 获取锁的过程,本质是通过cas来获取状态值修改。
      4. 如果没有获取到,将线程放入等待链表中。
      5. lock释放过程,改变状态值,调整等待链表。

JMM

什么是JMM

  1. java memory model ,java内存模型。
  2. 定义了主存,工作内存抽象概念。
  3. 底层对应着CPU寄存器,CPU指令优化、缓存、硬件内存等。

多线程 - 图3

如上图

  1. 在JVM运行的时候,会有一个主内存,各个线程会有各自的工作内存。
  2. 各个线程不能直接操作主内存和别的线程的工作内存,只能通过把主内存中的数据拷贝到自己的工作内存中,从而对数据进行操作。

JMM三大特性

  • 原子性

    1. 保证指令不会受到线程上下文切换的影响
    2. 解决的是多线程对同一临界区域代码块共享变量的修改问题。
    3. 原子性与可见性的区别

    1) 可见性保证的是,多线程中一个线程对volitile变量的修改,需要对其他线程可见。只有一个线程在写变量,其他线程都是在读取。 2) 原子性的保证需要通过加锁实现,场景为多个线程都会修改变量。

  • 可见性

    1. 保证指令不受CPU缓存的影响
    2. 当一个线程频繁从主存中读取某个变量的值,JIT编译器会将变量的值存储到自己的工作内存中,减少对主存的访问,提高效率。——提高了效率,也是万恶之源
    3. 此时如果一个线程改变了这个变量的值,即主存中的值发生了改变
    4. 当前线程还是在自己的工作内存中获取。即感知不到变量的值发生了变化。拿到的永远是旧值。
    5. 解决办法:

    1) 使用volatile来修饰,可以避免线程从自己的工作缓存中查找变量值,必须到主存中获取变量的值。线程操作volatile都是直接操作主存。 2) 打印该变量的值,System.out.Print 加了synchronized保证了原子性。

    1. 可见性保证的是,在多线程间,一个线程对共享变量的修改对其他线程可见。但是不能保证原子性。
  • 有序性

    1. 保证指令不受CPU指令重排(并行优化)的影响。
    2. JVM在不影响正确性的前提下,可以调整语句的执行顺序。
    3. 多线程场景下,指令重排会影响正确性。
    4. 为什么要进行指令重排?
      1. 指令可以划分为更小的单元:
        1. 取指令
        2. 指令译码
        3. 执行指令
        4. 内存访问
        5. 数据回写
      2. 一个单元即一次执行,为了提高效率,每次多个单元同时执行。如下图,两个执行如果串行,如果每个单元需要1s, 2个指令10个单元需要10s.同时执行的话需要6s就完成了。
    5. 如何禁止指令重排?
      1. 临界区代码加锁
      2. 使用volitile

多线程 - 图4

volatile

  1. 底层原理
    1. volitile底层原理是内存屏障
    2. 对volitile对象的写指令后会加入写屏障
    3. 对volitile对象的读指令前会加入读屏障
  2. 如何保证可见性
    1. 写屏障:保证在共享屏障前,对共享变量的改动,都同步到主存中。
    2. 读屏障:保证在该屏障之后,对共享变量的改动,都读取的是主存中的最新数据。

多线程 - 图5

DCL中volatile的使用

  • 代码如下:

    public final class Test {
      private static volatile Test instance = null;
    
      private Test(){
      }
    
      public static Test getInstance(){
          // 第一个check,当instance被实例化后,后面都不为null,如果去掉这个if,多线程场景下都要进入synchronized。
          if (instance == null){
              synchronized (Test.class){
                  // 第一个check不在同步块中,如果不判断,有可能进入第一层check时, instance为null成立。
                  // 进入synchronized后,instance有可能已经被其他线程实例化,所以需要再次判空
                  if (instance == null){
                      // volatile防止指令重排返回的是半实例化对象
                      instance = new Test();
                  }
              }
          }
          return instance;
      }
    }
    
  • 如果不加volatile可能发生问题的代码instance = new Test();

  1. 虽然上述代码在synchronized中,synchronized可以保证原子性、可见性、有序性。
  2. 但是synchronized保证有序性的意思并非说synchronized可以防止指令重排。他的意思是如果一个变量的所有操作都在synchronized中,可以保证即使发生指令重排,也不会破坏代码的执行结果。
  3. 上述代码显然在同步块外面有个操作if (instance == null){
  4. instance = new Test(); 底层字节码中做了两件事情
    1. invokespecial 表示利用一个对象引用,调用构造方法。
    2. putinstance 表示利用一个对象引用,赋值给instance。
    3. 如果putinstance和invokespecial发生指令重排,其他线程判断 instance为空时会返回false,最终导致返回半初始化对象。
  5. 加了volitile后,怎么解决的重排序。volitile保证有序性也是基于读写屏障
    1. 对于volitile修饰的变量,会在读操作前加入读屏障。
      1. 读屏障之后的操作,会去主存中获取值。
      2. 读屏障之后的执行,不能指令重排到读屏障之前。
    2. 对于volitile修饰的变量,会在写操作之后加入写屏障。
      1. 写屏障之前的操作,都会同步到主存。
      2. 写屏障之前的操作,不能指令重排到写屏障之后。
    3. 就是基于volitile不能发生指令重排,即 invokespecial 必须在 putinstance之前,不能重排,解决了可能返回半成品对象的问题。

as-if-serial

  1. 不管怎么指令重排,单线程场景下执行结果不能发生改变。
  2. 为了遵守as-if-serial原则,编译器和处理器不会对存在依赖关系的操作进行指令重排。因为这种操作为影响程序执行结果。
  3. 对于没有依赖关系的操作,就可能会被编译器和处理器做重排序。

happens-before

  1. happens-before规定了对共享变量的写操作对其他线程可见,是可见性和有序性规则的一套总结。
  2. 抛开heppens-before规则,JMM并不能保证一个线程对共享资源的写,对于其他线程的读可见。
  3. 遵守happends-before的相关实现
    1. 锁规则: 线程解锁之前在临界区对变量的写,在其他线程加锁后再临界区的读可见。
    2. volatile规则: 线程对volatile变量的写,对之后其他线程的读可见。
    3. 线程启动规则: 线程start之前对变量的写,对接下来其他线程对该变量的读可见。
    4. 线程终止规则: 线程结束前对变量的写,对其他线程得知它结束后对变量的读可见。
    5. 传递性:A先于B,B先于C,则A必定先于C。

Thread

wait和notiy

  1. 当前线程(monitor对象中的owner线程)中,通过主动调用锁对象的wait方法,即可进入锁对象中关联的monitor对象的waitset中,状态变为waiting状态。
    1. 线程必须获得锁,才能通过对象锁调用wait,notify,notifyall方法。
    2. 线程通过锁对象调用wait方法,会让当前线程进入锁对象关联的monitor对象的waitset中。
    3. 线程通过锁对象调用notify方法。会在waitset中唤醒一个线程。
    4. 线程通过锁对象调用notifyall方法,会唤醒waitset中的所有线程。
  2. BLOCKED和WAITING状态的线程都会处于阻塞状态,不占用CPU时间片。
  3. BLOCKED的线程,即在entryList中的线程会在owner释放锁时被自动唤醒。
  4. WAITING的线程,即在waitSet中的线程会在owner调用nofity或者nofityall时被唤醒。但并不意味着立刻获得锁,仍需要进入entryList中重新竞争。
  5. wait和notify都是Object的方法。

wait和sleep区别

  1. sleep是Thread的方法,而wait是Object的方法。
  2. sleep不强制和synchronized一起使用,而wait依赖锁对象关联的monitor锁,必须和synchronized一起使用。
  3. sleep不会释放锁对象,而wait会释放锁。
  4. sleep后线程的状态是TIMED_WAITING状态。wait后线程状态是WAITING状态。
  5. 其他线程可以通过正字sleep的线程对象调用它的interrupt方法打断sleep.这时sleep方法会抛出InterruptedException.

park、unpark

  1. 使用方法: LockSupport.part(线程对象),LockSupport.unpark(线程对象)
  2. 与wait/notify的区别
    1. wait,notify必须和锁对象Obj的Monitor对象一起使用。而park不用。
    2. park,unpark是以线程为单位阻塞和唤醒的,而notify,notifyall是随机唤醒,不精确。
    3. 可以先unpark,但是不能先notify.

yield

  1. 执行后线程进入直接进入就绪状态,马上释放了CPU执行权
  2. 但是依然保留了cpu的执行资格。
  3. 所以可能CPU下次执行调度还会让这个线程获取到执行权继续执行。

run和start

  1. start方法,启动一个线程,不能多次启动。
  2. run方法,没有启动线层,是方法调用。

Thread和Runable的区别

  1. Thread实现了Runable。
  2. 由于java的单线程,多实现,Runable有更好的扩展性。
  3. 使用方式
    1. new Thread(){…}.start();
    2. new Thread(new Runable{…}).start();
  4. Runable更适合相同的代码去处理统一资源的情况。多线程共享数据。

JOIN

  1. 如果没有指定时间。等待join的线程执行完成后,当前线程再执行。
  2. 如果执行了时间,指定时间为最大等待时间
    1. 如果在最大等待时间没有完成,不再等待。
    2. 如果提前完成,不再等待。

setPriority

  1. 修改线程优先级
  2. java中规定的优先级是1-10的整数。如果设置的大于10小于1会抛出IllegalArgumentException。
  3. 较大的优先级能提高线程被CPU调用的几率。
  4. 默认优先级为5.

interrupt()、isInterrupt()、interrupted().

  1. interrupt(),在一个线程中调用另一个线程的interrupt方法,即会向另一个线程发出信号,线程被打断。至于被打断线程如何处理打断,由被打断线程自己代码实现。
  2. isInterrupt(),用来判断当前的中断状态,true或者false.
  3. interrupted(),判断当前线程是否处于阻塞状态,并清除标记。

线程池

为什么用线程池?

核心: 线程复用,管理最大并发数。

  1. 降低资源消耗: 通过复用已经创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度:当任务到达时,任务可以不需要等待线程创建,就能立即执行。 3.提供线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源。还会降低系统稳定性。

类图关系

image.png

创建线程池七大参数

public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {
  1. corePoolSize

    i核心线程数,线程池中常驻的线程数。

  2. maxmumPoolSize

    线程池中能容纳同时执行的最大线程数,必须大于等于1否则抛出异常

  3. long keepAliveTime

    空闲线程存活时间,当线程池中的线程数超过corePoolSize时,空闲时间超过keepAliveTime时,空闲线程会被销毁,调用的是Interrupte方法。

  4. unit.

    keepAliveTime的时间单位。

  5. workQueue

    阻塞队列,当核心线程都在执行任务,新进来的任务会放入阻塞队列。

  6. threadFactory

    创建线程的工厂,一般使用默认即可。

  7. rejectHander

    拒绝策略,当核心线程数都在执行任务,阻塞队列已经满了,对新进入的任务如何拒绝。

线程池执行原理

  1. 当正在执行的任务数没有超过coolPoolSize,即核心线程数没有占满,新增的任务由核心线程执行。
  2. 当核心线程数已满,阻塞队列未满,新增加的线程加入后放入阻塞队列。
  3. 当阻塞队列已满,线程数未达到最大线程数,增加的任务会新创建线程执行。
  4. 当阻塞队列已满,且达到了最大线程数,增加的任务人执行线程池的拒绝策略:AbortPolicy、DiscardPolicy、DiscardOldPolicy、CallerRunsPolicy等。

拒绝策略类图关系

image.png

拒绝策略

  1. AbortPolicy(默认): 直接抛出RejectExecutionExeption.阻止正常运行。
  2. CallerRunsPolicy(返回调用者):将任务返回调用者执行。
  3. DiscardPolicy(丢弃): 直接丢弃任务,不跑异常,也不执行。
  4. DiscardOldestPolicy(丢弃最久): 直接抛弃阻塞队里中等待最久的任务,然后将新任务加入阻塞队列。

线程池的创建方式

  • Executors.newFixedThreadPool(n);

    1. 构造方法: return new ThreadPoolExecutor(nThreads, nThreads,<br />0L, TimeUnit.MILLISECONDS,<br />new LinkedBlockingQueue<Runnable>());
    2. 核心线程数等于最大线程数。执行任务的线程数时固定的。不会新创建线程。
    3. 线程存活时间为0L,不涉及(核心=最大)。
    4. 阻塞队列为linkedBlockingQueue,即最大为Integer默认值。
  • Executors.newSingleThreadExecutor();

    1. 构造方法:return new FinalizableDelegatedExecutorService<br />(new ThreadPoolExecutor(1, 1,<br />0L, TimeUnit.MILLISECONDS,<br />new LinkedBlockingQueue<Runnable>()));
    2. 核心线程数等于最大线程数,都是1,即永远只有一个线层在执行任务。
    3. 线程存活时间为0.不涉及(核心=最大)。
    4. 阻塞队列为linkedBlockingQueue,即最大为Integer默认值。
  • Executors.newCachedThreadPool();

    1. 构造方法:return new ThreadPoolExecutor(0, Integer.MAX_VALUE,<br />60L, TimeUnit.SECONDS,<br />new SynchronousQueue<Runnable>());
    2. 核心线程数为0,最大线程数为int最大值。
    3. 线程核心数为0,所有线程都是外包人员,项目结束后,60S需要撤离。(2021.10.3 我就是和非核心线程啊啊啊啊)
    4. 使用的队列为SynchronousQueue,相当于阻塞队列不存储元素。结合 2. 每个任务进入后都会创建新的线层。
  • Executors.newScheduledThreadPool(n);

    1. 构造方法public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {<br />return new ScheduledThreadPoolExecutor(corePoolSize);<br />} public ScheduledThreadPoolExecutor(int corePoolSize) {<br />super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,<br />new DelayedWorkQueue());<br />}
    2. 核心线程数为指定值,最大线程数为Integer最大值。
    3. 外包人员在项目结束后立即撤离。
    4. 阻塞队列使用的是DelayedWorkQueue,该队列默认大小是16. 队列满了后会创建新线程。

为什么不建议使用JDK自带的创建方式 就这四种,但是都不能用

  1. FixedThreadPool和SingleThreadPool,核心线程数都等于最大线程数,阻塞队列都使用了LinkedBlockingQueue,最大容量为Integer最大值。大量任务场景下,请求堆积,有可能发生OOM.
  2. Cached和Scheduled, 最大线程数都设置了Integer的最大值。大量任务场景下,会创建大量的线程。造成资源浪费,系统卡顿、OOM.

IO密集型与CPU密集型

  1. IO密集型
    1. CPU使用不高,大多数时间在处理耗时的IO操作。这类操作不占用CPU.
    2. 例如文件读写、DB读写、网络请求等。
  2. CPU密集型
    1. CPU使用率较高,逻辑处理多,IO很少或者响应都非常迅速。
    2. 例如计算型代码,json转化等。

如何创建线程池,设置核心线程数大小?

  1. 创建线程池通过 new ThreadPoolExecutors(核心线程数、最大线程数、等待时间、时间单位、线程工厂、阻塞队列、拒绝策略)来实现。
  2. 核心线程数
    1. IO密集型:大部分时间在处理IO交互,而线程在IO的时间段内不会占用CPU来处理,这时可以将CPU交给其他线程使用。因此在IO密集型任务中,可以多配置一些核心线程数。具体计算方法是暴力 2N,还有个公式记不住。
    2. CPU密集型:主要消耗的是CPU资源,如果比CPU大太多,会引起频繁地上下文切换,降低效率。设置为 CPU的核心数+1. +1是为了防止线程偶发中断,或者任务暂停导致CPU空闲。
  3. 最大线程数
    1. 一般情况下看使用场景,如果设置全局线程池,可以适当的比核心线程数大点。
    2. 非全局的与核心线程数保持一致。
  4. 等待时间、时间单位
    1. 全局场景等待几秒回收。
    2. 非全局场景,只在特定时间内并发激烈,设置为0,使用完成立即回收。
  5. 线程工厂一般使用默认即可。
  6. 阻塞队列
    1. 由于任务一般生产消费是比较频繁的,使用特定大小的linkedBlockingQueue。
  7. 拒绝策略
    1. 不重要的、允许任务丢失的场景下,使用discardPolicy或者discardOldPolicy来实现。
    2. 任务比较重要,不允许丢失,使用callerrunsPolicy返归调用者或者直接使用AbortPolicy抛出异常。

shutdown和shutdownNow的区别

  1. shutdown
    1. 正在执行的任务: 执行完成
    2. 阻塞队列中的任务: 执行完成
    3. 新任务被提交: 执行拒绝策略
  2. shutdownNow
    1. 正在执行的任务: 打断
    2. 阻塞队列中的任务:不执行,返回。
    3. 新任务被提交:执行拒绝策略。

线程池使用完成要不要shutdown

  1. 如果线程池创建时核心线程数设置的不是0,且核心线程数在空闲是不会被回收。默认AllowCoreThreadTimeOut=false. 这些核心线程数一直会阻塞在获取任务上。即核心线程不会结束、GC无法回收,会导致系统资源浪费,有可能导致内存溢出。
  2. 如果没有核心线程数,或者核心线程可以被回收。且keepalive时间合理,不是很长,没有OOM风险。
  3. 综上,如果分全局的线程池,一般是需要shutdown的。

线程池中线程复用的原理

  1. 线程池将线程和任务进行了解耦,线程是线程、任务是任务。
  2. 在线程池中,同一个线程可以不断从阻塞队列中获取新任务来执行。
  3. 核心原理为线程池对Thread进行了封装,并不是每次执行都需要thread.start()创建新线程。
  4. 而是让每个线程去执行一个循环任务。而这个循环任务中不停的检测是否有任务需要被执行。
  5. 如果有,直接调用任务的run方法。做到线程复用。

线程池的五种状态

  1. RUNNING: 在running状态下,线程池可以接受新任务和执行已经添加的任务。
    1. 线程池的初始化状态就是running.
    2. 比如调用了Executors.newFixedThreadPool(),一旦创建,状态就是running.
  2. SHUTDOWN: 线程池出在shutDown状态时,不再处理新任务,但能处理已经添加的任务。
    1. 调用shutdown方法时,会将线程状态由RUNNING变为shuting.
  3. STOP: 线程池处在SHUTDOWN状态时,不接受新任务,不处理已经添加的任务,并且会中断正在执行的任务。
    1. 调用shutdownNow方法时,会将线程状态由RUNNING或者SHUTDOWN变为STOP.
  4. tidying: 中文整理的意思,当前所有任务已经终止,记录的任务数量为0,线层池会变为tidying状态。会执行函数terminated.
    1. 当线程在shutdown状态下,执行完阻塞队列的任务后,会由SHUTDOWN变为TIDYING.
    2. 当处在stop状态下,执行池中执行的任务为空时,会由stop变为tidying.
  5. terminated: 当terminated执行完成后,变为terminated。

多线程 - 图8

线程辅助类

CountDownLatch(闭锁)

  • 解决问题

    多线程场景场景下,一个线程要等待多个线程执行完成后再执行。

  • 原理

    1. new CountDownLatch(计数器大小),初始化了技术器的大小
    2. 多个线程执行的时候,countDown(),相当于给计数器减一。
    3. await 会等到 计数器大小减到0之后,执行后面的代码
    4. 注意如果线程本身会抛出异常,countDown必须在异常之前,建议写在final中。
    5. 当所有线程都调用过 await后,CountDownLatch 维护的 volitile int status 已经减到1,所以只能用一次,第二次使用不会报异常,但是也不会生效。
  • 使用方法

    1. new 出计数器,初始化大小为先执行的线程的数量
    2. 每个先执行的线程执行完后,调用 countDown方法,计数器减一
    3. 在后执行的线程中调用await方法。先执行的方法执行完成后,后执行的方法会自动执行
  • 示例代码

    
      @Test
      public void testCountDownLatch() {
          // 实例化一个线程池
          ExecutorService executorService = Executors.newFixedThreadPool(3);
          // 初始化数量为3的计数器
          CountDownLatch countDownLatch = new CountDownLatch(3);
          // 启动三个线程
          System.out.println(countDownLatch.getCount());
          for (int i = 0; i < 3; i++) {
              int temp = i;
              executorService.submit(new Runnable() {
                  @Override
                  public void run() {
                      try {
                          System.out.println(Thread.currentThread().getName() + temp);
                      } finally {
                          countDownLatch.countDown();
                      }
                  }
              });
          }
          System.out.println(countDownLatch.getCount());
          try {
              System.out.println("await begin.");
              countDownLatch.await();
              System.out.println(countDownLatch.getCount());
              System.out.println("await end.");
          } catch (InterruptedException e) {
              System.out.println(e.getMessage());
          }
          System.out.println(countDownLatch.getCount());
      }
    
  • 流程图

多线程 - 图9

CyclicBarrier(同步屏障)

  • 解决问题

    让一组线程到达一个屏障时被阻塞,直到最后一个线程到达时,所有被拦截的线程继续执行

  • 使用方法

    1. 默认构造方法为 new CyclicBarrier(int parties), 参数表示屏障拦截的线程数量。
    2. 每个线程调用await()方法告诉CyclicBarrier自己已经到了屏障,然后当前线程被阻塞。
    3. 当所有线程都调用了 await()方法后,屏障得知所有线程都已到达屏障,所有线程开始继续执行。
  • 示例代码

      @Test
      public void CyclicBarrierTest() throws Exception {
          CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
          for (int i = 0; i < 3; i++) {
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      System.out.println(Thread.currentThread().getName() + "  at barrier.");
                      try {
                          cyclicBarrier.await();
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                      System.out.println("await end.");
                  }
              }).start();
          }
          System.out.println("main end.");
      }
    

    多线程 - 图10

    Semaphore(信号量)

  • 解决问题

    1. 控制并发线程数。控制系统中某个资源被同时访问的线程个数。
    2. 类似于锁,又比锁强大,锁一般锁住的是一个资源,Semaphore可以锁住多个资源。
    3. 某些场景下,线程数原高于数据库连接池数量,可以用来限制多线程同时阻塞在数据库连接池。
  • 使用方法

    1. new Semaphore(int x),参数为允许同时执行的线程数量
    2. 每个线程执行前调用 acquire().
    3. 每个线程执行完调用release().
  • 示例代码

      @Test
      public void semapHoreTest() throws Exception {
          Semaphore semaphore = new Semaphore(2);
          for (int i = 0; i < 111; i++) {
              int ii = i;
              new Thread(() -> {
                  try {
                      semaphore.acquire();
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "----" + ii);
                  semaphore.release();
              }).start();
          }
          Thread.sleep(10000);
      }
    

    多线程 - 图11

    Exchanger(交换器)

  • 解决问题

    解决问题:两个线程之间交换数据

  • 使用方法

    1. new Exchanger()
    2. 两个线程中分别 .exchange(要交换的数据)
    3. 分别得到对方数据。
  • 示例代码

      @Test
      public void exeChangerTest() throws Exception {
          Exchanger<String> exchanger = new Exchanger<>();
          new Thread() {
              @Override
              public void run() {
                  try {
                      String test1 = exchanger.exchange("test1");//test1 test2                    
                      System.out.println("test1 " + test1);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }.start();
          new Thread() {
              @Override
              public void run() {
                  try {
                      String exchange = exchanger.exchange("test2");
                      System.out.println("exchange" + exchange);//exchangetest1                
                  } catch (InterruptedException e) {
                      System.out.println(e.getMessage());
                  }
              }
          }.start();
          Thread.sleep(10000);
      }
    

    CAS

    什么是CAS

    1. compare-and-set,也有compare-and-swap叫法。是比较并交换的意思。
    2. 它是一条cpu并发原语,用于判断内存中某个值是否为预期值,如果是则改为新的值,这个过程是原子的

CAS原理

  1. 主要包括两个操作,compare和swap.
  2. 如何保证两个操作的原子性: CAS是一种系统原语,原语属于操作系统用语,由若干指令组成,用于完成一个特定的功能,并且原语执行必须是连续的,在执行过程中不允许被中断。
  3. 综上,CAS是一条CPU原子指令,原子性由操作系统来保证。
  4. java中的实现时UnSafe类。

ABA问题

  • 代码

    public final class Test {
      private AtomicInteger ai = new AtomicInteger(100);
    
      @org.junit.Test
      public void test() {
          Thread thread1 = new Thread(() -> {
              boolean re = ai.compareAndSet(100, 101);
              System.out.println("thread1 从 100 修改到101" + re);
          });
          Thread thread2 = new Thread(() -> {
              boolean re = ai.compareAndSet(101, 100);
              System.out.println("thread2 从 101 修改到100" + re);
          });
          Thread thread3 = new Thread(() -> {
              boolean re = ai.compareAndSet(100, 101);
              System.out.println("thread3 从 100 修改到999" + re);
          });
          CompletableFuture.runAsync(thread1).thenRun(thread2).thenRun(thread3);
      }
    }
    
    1. 如上,当atomicInteger的值为100,线程1将值改为101.
    2. 当值为101,线程2将值改为100.
    3. 当值为100,线程3将值改为101.

    此处的问题

    1. 3要比较的100是否是原始的100,明显不是,这种场景就要区分。
    2. 如果只关注数值,不关注是否修改过。就没有问题。
    3. 如果既要关注数值,也要关注是否被修改过,就有问题。
  • 解决方式

    public final class Test2 {
      private AtomicStampedReference ai = new AtomicStampedReference<Integer>(100,0);
    
      @org.junit.Test
      public void test() {
          Thread thread1 = new Thread(() -> {
              // 从100改为101,比较的100的版本号是0,新版本号是1
              boolean re = ai.compareAndSet(100, 101,0,1);
              System.out.println("thread1 从 100 修改到101" + re);
          });
          Thread thread2 = new Thread(() -> {
              // 从100改为101,比较的101的版本号是1,新版本号是2
              boolean re = ai.compareAndSet(101, 100,1,2);
              System.out.println("thread2 从 101 修改到100" + re);
          });
          Thread thread3 = new Thread(() -> {
              // 从100改为101,比较的100的版本号是0,新版本号是1
              boolean re = ai.compareAndSet(100, 101,0,1);
              System.out.println("thread3 从 100 修改到999" + re);
          });
          CompletableFuture.runAsync(thread1).thenRun(thread2).thenRun(thread3);
      }
    }
    
    1. 如上代码,解决ABA问题的思路,既要比较值,也要比较版本。
    2. java中使用AtomicStampedReference来解决。

自旋开销问题

  1. CAS出现冲突后就开始自旋操作,如果资源竞争非常激烈,自旋长时间不能成功就会给CPU带来非常大的开销。
  2. 可以考虑限制自旋次数,避免过度消耗CPU. 也可以考虑延迟执行。

cas和volatile

  1. cas是一条原子指令,可以保证比较和设置值具有原子性。
  2. volatile保证cas每次操作共享资源时,读写都直接操作主存,保证了可见性和有序性。
  3. 乐观锁的实现原理就是cas+valatile+自旋

乐观锁悲观锁

  1. 结合cas和volatile和自旋,可以实现无锁并发,使用线程数较少,即竞争不激烈的场景。
  2. CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改变量,就算改了也没关系,我自旋。
  3. synchronized是基于悲观锁的思想: 最悲观的估计,得防止其他线程来修改共享变量,我上了你们都别改,等我改完了你们才有机会。
  4. CAS体现的是无锁并发,无阻塞并发。
    1. 因为没有使用锁,线程不会从RUNNABLE和BLOCKING的切换,也就没有上下文切换的影响。这个是效率提升的原因。
    2. 如果竞争激烈,每次cas都失败,需要不断的重试不断的争抢CPU的执行权,效率会降低。

AQS-这个源码很重要,欠着先

什么是AQS

  1. 全称是AbstractQueuedSynchronizer.
  2. 是阻塞式锁和相关同步器工具的框架。

AQS原理

  1. 用state属性来表示资源的状态,分为独占模式和共享模式。
  2. 子类需要定义如何维护这个state状态,控制如何获取锁和释放锁。
    1. getState 获取state状态。
    2. setState 设置state状态。
    3. compareAndSetState cas机制设置state状态。
    4. 独占模式只有一个线程能访问资源,而共享模式可以允许多个线程访问资源。
  3. 提了了基于FIFO的等待队列,类似Monitor的EntryList。
  4. 条件变量来实现等待、唤醒机制。支持多个条件变量,类似Monitor的WaitSet。
  5. 子类主要实现如下方法,默认抛出UnsupportedOperationException.
    1. tryAcquire
    2. tryRelease
    3. tryAcquireShared
    4. tryReleaseShared