JUC学习笔记

2 篇文章 0 订阅

JUC并发编程

进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这是就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序智能启动一个实例进程(如网易云音乐、360)

线程

  • 一个进程之内可以分为一到多个线程
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
  • Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。

进程 vs 线程

  • 进程基本上互相独立,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的继进程通信称为IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一共享变量
  • 线程更轻量,线程上下切换成本一般上要比进程上下文切换低
截屏2021-10-09下午10.00.23

并发与并行

单核 CPU 下,线程实际上还是 串行执行的,操作系统中有一个组件叫做任务调度器,将 CPU 的时间片(Windows 下时间片最小约为15毫秒)分给不同的线程使用,只是由于CPU在线程时间(时间片很短)的切换非常快,人类感觉是同时运行的,总结一句话就是:微观串行,宏观并行。

一般会将 线程轮流使用 CPU的做法称为并发(concurrent)

多核CPU下,每个核都可以调度运行线程,这时候线程是可以并行

  • 并发(concurrent)是同一时间应对多件事情的能力
  • 并行(parallel)是同一时间动手做多件事情的能力

应用

案例一:异步调用

从方法调用的角度来讲,如果

  • 需要等带结果返回,才能继续运行就是同步
  • 不需要等待结果,就能继续运行就是异步

1)设计

多线程可以让方法执行变为异步的(即不要干等)比如说读取磁盘文件时,假设读取操作花费了五秒钟,如果没有线程调度机制,这五秒调用者什么都干不了,其代码都得暂停

2)结论

  • 比如在项目中,视频文件需要转换格式等操作比较费时,这是开一个新线程处理视频转换,避免阻塞主线程

  • Tomcat 的异步 servlet 也是类似的,让用户线程处理耗时较长的操作,避免阻塞 Tomcat 的工作线程

  • UI程序中,开线程进行其他操作,避免阻塞UI线程

  • 单核CPU下,多线程不能实际提升程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用CPU,不至于一个线程总占用CPU,别的线程没法干活

  • 多核CPU可以并行跑多个线程,但能否提高程序运行效率还是要分情况的。

    • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(阿姆达定律)
    • 也不是所有的任务都需要拆分,任务的目的如果不同,谈拆分和效率没有意义
  • IO操作不占用CPU,只是我们一般拷贝文件使用的是阻塞IO,这时相当于线程虽然不用CPU,但需要一直等待IO结束,没能充分利用线程。所以才有后面的【非阻塞IO】和【异步IO】优化。

Java线程

创建和运行线程



public static void main(String[] args) {
  Thread t = new Thread("t1"){
    @Override
    public void run(){
      log.debug("t");
    }
  };
  //        t.setName("t2");
  t.start();

Runnable r = new Runnable() {
            @Override
            public void run() {
                log.debug("t1 running");
            }
        };
        Thread t = new Thread( r, "ThreadName" );
        t.start();  

  
new Thread(()-> log.debug("Thread Running")).start();
  

原理之Thread与Runnable的关系

分析Thread源码,结论如下:

  • 传入Runnable参数写法是对Thread的成员变量target赋值,run方法调用 target.run( )
  • 匿名内部类的写法是直接对run方法重写,执行的Thread.run( )

小结:

  • 用Runnable更容易与线程池等高级API配合
  • 用Runnable让任务脱离了Thread继承体系,更加灵活。
前瞻:第三种创建线程方式
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        log.debug("running...");
        Thread.sleep(1000);
        return 100;
    }
});
Thread t = new Thread(task,"t1");
t.start();

log.debug(task.get().toString()); // 该方法会等待返回值生成
查看进程线程的方法

Windows

  • tasklist 查看进程 -> tasklist | findstr java
  • taskkill 杀死进程
  • jps 查看Java进程

Linux

  • ps -ef 查看所有进程 -> ps -ef | grep java
  • ps -fT -p 查看某个进程的所有线程
  • kill 杀死进程
  • top 按大写H切换是否显示线程
  • top -H -p 查看某个进程的所有线程

Java

  • jps 查看所有Java进程
  • jstack 查看某个Java进程的所有线程状态
  • jconsole 来查看某个Java进程中线程的运行情况(图形界面)

原理之线程运行

线程上下文切换(Thread Context Switch)

因为以下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要执行
  • 线程自己调用了 sleep yield wait join park synchronized lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条JVM指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量表、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

常见方法

方法名static 功能说明注意
start( )启动一个新线程,在新的线程运行方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU时间片还没分给它)。每个线程对象的 start 方法只能调用一次,如果调用多次会出现 IllegalThreadStateException
run( )新线程启动后会调用的方法如果在构造 Thread 对象时,传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为
join( )等待线程结束
join( long n )等待线程运行结束,最多等待 n 毫秒
getId( )获取线程长整型的 idid唯一
getName( )获取线程名
setName( )设置线程名
getPriority( )修改线程优先级
setPriority( int )设置线程优先级java 中规定线程优先级是 1~10的整数,较大的优先级能提高该线程被CPU调度的几率;越大越优先
getState( )获取线程状态Java 中线程状态是用6个enum表示,分别为:NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED
isInterrupted( )判断是否被打断不会清除打断标记
isAlive( )线程是否存活(还没有运行完毕)
interrupt( )打断线程(仅标记,不是强制终止)如果被打断线程正在sleep wait join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记
interrupted( )static 判断当前线程是否被打断会清除打断标记
currentThread( )static 获取当前正在运行的线程
sleep( long n )static 让当前执行的线程休眠 n 毫秒,休眠时让出CPU时间片给其他线程
yield( )static 提示线程调度器让出线程对CPU的使用主要是为了测试和调试

interrupt()中断对LockSupport.park()的影响_anlian523的博客-CSDN博客_locksupport park 中断

注:

  1. sleep 会补充 park 的 _counter 变量(permit),使其为1,之后第一次park时不会阻塞!
start 与 run

截屏2021-10-11上午8.23.00

sleep 与 yield
sleep
  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态
  2. 其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会得到执行
    1. 建议用 TimeUnit 的 sleep 代替 Thread 的sleep来回的更好的可读性
yield
  1. 调用yield会让当前线程从 Running 进入 Runnable 状态,然后调度执行其它同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
  2. 具体的实现依赖于操作系统的任务调度器
线程优先级
  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用
案例:防止CPU占用100%
  • Thread.sleep( 1 )
  • wait 实现
join

为什么要使用 join?

截屏2021-10-11上午8.57.51 截屏2021-10-11上午8.58.41

分析

  • 因为主线程和线程 t1 是并行执行的,t1线程需要1s之后才能得出r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r = 0

解决办法

  • 用 sleep ?
    • 等待时间难以确定
  • 用 join,加在 t1.start( )之后即可
    • join( ):等待线程结束
interrupt
  • 打断正在 sleep wait join 的线程打断标记为 false
  • 打断正在运行的线程的打断标记为 true
截屏2021-10-11上午9.27.42
模式之两阶段终止

在一个线程T1中如何“优雅”终止T2?这里的【优雅】指的是T2一个料理后事的机会

错误思路

  • 使用线程对象的 stop( ) 方法停止线程
    • stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁
  • 使用 **System.exit( int )**方法停止线程
    • 目的仅是停止一个线程,但这种做法会让整个程序都终止

两阶段终止模式

截屏2021-10-11上午9.35.47
public class Test {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TwoPhaseTermination t = new TwoPhaseTermination();
        t.start();
        Thread.sleep(3500);
        t.stop();
    }
}


class TwoPhaseTermination {
    private Thread monitor;

    // 启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
//                System.out.println(current.getName());
                if (current.isInterrupted()) {
                    System.out.println("Main Done");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    System.out.println("执行监控记录");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("Sleep Interrupt");
                    current.interrupt(); // 重新设置打断标记
                }
            }
        });
        monitor.start();
    }

    // 停止监控线程
    public void stop() {
        monitor.interrupt();
    }

}
打断 park 线程

打断 park 线程,不会清空打断状态 ,LockSupport.park( )方法需要打断标记为false才能生效

private static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.debug("park...");
        LockSupport.park();
        log.debug("unpark...");
        log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
    t1.start();

    sleep(1);
    t1.interrupt();

}

private static void test4() {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log.debug("park...");
                LockSupport.park();
                log.debug("打断状态:{}", Thread.interrupted());
            }
        });
        t1.start();
        sleep(1);
  
        t1.interrupt();
    }

不推荐使用的方法

这些方法已过时,容易破坏同步代码块,造成线程死锁

  • stop( ):停止线程运行
  • suspend( ):挂起(暂停)线程运行 -> wait( )
  • resume( ):恢复线程运行 -> notify( )

守护线程与主线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。setDameon( )可以设置守护线程。

  • 垃圾回收线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待他们处理完当前请求

线程状态

操作系统 层面描述:五种状态

截屏2021-10-11上午11.36.15
  • 【初始状态】仅是语言层面创建了线程对象,还未与操作系统关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行
  • 【运行状态】指获取了CPU时间片运行中的状态
    • 当CPU时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程上下文的切换
  • 【阻塞状态】
    • 如果调用了阻塞API,如BIO读写文件,这时线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要他们一直不唤醒,调度器就一直不会考虑调度他们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态

Java API 层面描述:六种状态

截屏2021-10-11下午4.27.59
  • NEW 线程刚被创建,但是还没有调用 start( )方法
  • RUNNABLE 当调用了 start( ) 方法之后,注意,JavaAPI层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行的)

小结案例

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        log.debug("洗水壶");
        sleep(1);
        log.debug("烧开水");
        sleep(5);
    },"老王");

    Thread t2 = new Thread(() -> {
        log.debug("洗茶壶");
        sleep(1);
        log.debug("洗茶杯");
        sleep(2);
        log.debug("拿茶叶");
        sleep(1);
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("泡茶");
    },"小王");

    t1.start();
    t2.start();
}

存在的缺陷

  • 上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶呢?代码最好适应两种情况
  • 上面两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或者模拟小王把茶叶交给老王泡茶呢?
  • 等待后续学习解答

本章小结

本章重点在于掌握

  • 线程创建
  • 线程重要 API,如start,run,sleep,join,interrupt 等
  • 线程状态
  • 应用方面
    • 异步调用:主线程执行期间,其他线程异步执行耗时操作
    • 提高效率:并行计算,缩短计算时间
    • 同步等待:join
    • 统筹规划:合理使用线程,得到最优效果
  • 原理方面
    • 线程运行流程:栈、栈帧、上下文切换、程序计数器
    • Thread 两种创建方式的源码
  • 模式方面
    • 两阶段终止

共享模型之管程

  • 共享问题
  • 线程安全分析
  • Monitor
  • wait/notify
  • 线程状态转换
  • 活跃性
  • Lock
共享带来的问题
临界区 Critical Section
  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块如果存在对共享资源的多线程读写操作,这段代码称为临界区
    • 例如:对静态变量的操作
竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

synchronized 解决方案

应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方法:synchronized 、 Lock
  • 非阻塞式的解决方案:原子变量

本阶段使用:synchronized 来解决上述问题,俗称【对象锁】,它采用互斥的方式让同一时刻至多有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换。

注意

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
使用
synchronized(对象){
  临界区
}
思考

synchronized 实际是用对象锁保证了临界区代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

  • 如果把 synchronized 放到循环外面会怎样?
    • 循环不可分割,相当于先执行了先拿到锁对象的线程,再执行了后者
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?
    • 相当于没加锁
  • 如果 t1 synchronized(obj1) 而 t2 没有加会怎样?如何理解?
    • 仍然相当于没加锁,因为 t2 并不会因为 t1 的锁而产生阻塞

方法上的 synchronized

截屏2021-10-12上午9.06.25
所谓“线程八锁”(习题)

其实就是考察 synchronized 锁住的是哪一个对象

情况1:1->2 或 2->1

截屏2021-10-12上午9.08.32

情况2:1s后 1->2 或 2-> 1s 后 1

截屏2021-10-12上午9.13.10

情况3:3在1之前的位置 + 情况2

截屏2021-10-12上午9.13.50

情况4:2 -> 1(无互斥)

image-20220412201004137

情况5:2->1(Number.class / this 无互斥)

截屏2021-10-12上午9.18.48

情况6:1s 1 ->2 或 2 -> 1s 1

截屏2021-10-12上午9.18.48

情况7:2 -> 1s 1

截屏2021-10-12上午9.21.52

情况8:1s 1 -> 2 或 2 -> 1s 1

截屏2021-10-12上午9.23.13

变量的线程安全分析

成员变量和静态变量是否线程安全?
  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态能否改变,又分为两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
  • 局部变量是线程安全的
  • 局部变量引用的对象未必安全
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全
常见的线程安全类
  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:

  • 它们每个方法是原子的
  • 注意它们多个方法的组合不是原子的,见后面分析
线程安全类方法的组合
截屏2021-10-12上午10.35.11
不可变类线程安全型

String、Integer 等都是不可变类,因为其内部的状态不可变,因此他们的方法都是线程安全的。

对于 replace、subsring 等方法都是在其中生成了新的对象返回。

实例分析

例1

ZtSRRm

例2

nTOLs2

例3

inzWNd

例4

7IM7iB

例5

gSmjxq

例6

n8XVBm

例7

cfzPtB 截屏2021-10-12下午2.45.06

习题

售票窗口

package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);

        // 所有线程的集合
        List<Thread> threadList = new ArrayList<>();
        // 卖出的票数统计
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                // 买票
                int amount = window.sell(random(5));
                // 统计买票数
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }

        for (Thread thread : threadList) {
            thread.join();
        }

        // 统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int random(int amount) {
        return random.nextInt(amount) + 1;
    }
}

// 售票窗口
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // 获取余票数量
    public int getCount() {
        return count;
    }

    // 售票,注意这里的 synchronized
    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}
package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;
import java.util.Random;

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

// 账户
class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    // 转账,注意这里的 synchronized
    public void transfer(Account target, int amount) {
        synchronized(Account.class) {
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

Monitor 概念

Java 对象头
截屏2021-10-12下午4.45.28 截屏2021-10-12下午4.48.08
Monitor(重量级锁)

Monitor被翻译为监视器管程,由操作系统提供。

每个Java对象都可以关联一个Monitor对象,如果使用 synchronized 给对向上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

Monitor结构如下

截屏2021-10-12下午4.52.10
  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj) ,就会进入 EntryList 从而变为BLOCKED状态
  • Thread-2 执行完同步代码块中的内容,然后唤醒 EntryList 中等带的线程来竞争锁。
  • 图中的 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但不满足进入 WAITING 状态的线程,后面将 wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的Monitor才有上述效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则
synchronized 字节码
截屏2021-10-12下午5.06.05

后面是异常解锁相关字节码

synchronized 原理 ※
轻量级锁

轻量级锁的使用场景:如果一个对象有多个线程访问,但线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,语法仍然是 synchronized

假设有两个同步块,利用一个对象加锁

截屏2021-10-12下午5.21.47
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
截屏2021-10-12下午5.37.16
  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的Mark Word,将 Mark Wrod 的值存入锁记录
截屏2021-10-12下午5.40.49
  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00,表示由该线程给对象加上轻量级锁
截屏2021-10-12下午5.43.01
  • 如果 cas 替换失败,有两种情况
    • 如果是其他线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添一条 Lock Record 作为重入的计数
截屏2021-10-12下午5.46.11
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
截屏2021-10-12下午5.48.29
  • 当退出 synchronized 代码块时(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争力),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
截屏2021-10-12下午5.54.19
  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntryList BLOCKED
截屏2021-10-12下午5.59.44
  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED线程
自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况

线程1(CPU1 上)对象Mark线程2(CPU2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步代码块10(重量锁)重量锁指针-
执行同步代码块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步代码块10(重量锁)重量锁指针自旋重试
执行完毕10(重量锁)重量锁指针自旋重试
成功(解锁)01(无锁)自旋重试
-10(重量锁)重量锁指针成功(加锁)
-10(重量锁)重量锁指针执行同步块
-

自旋重试失败的情况

线程1(CPU1 上)对象Mark线程2(CPU2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步代码块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针阻塞
-
  • 在Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • Java7之后不能控制是否开启自旋功能
偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。

Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

偏向锁是对轻量级锁的一种优化。

截屏2021-10-12下午8.25.11
偏向状态

看一下之前的对象头格式

截屏2021-10-12下午8.26.23

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword值为 0x05 即最后3位为 101,这时它的 thread、epoch、age都为0
  • 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以添加VM参数:-XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为0x01即最后3位为001(Normal),这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
  • 禁用偏向锁:-XX:-UseBiasedLocking,另外三种情况也会禁用偏向锁:
    • 调用 hashCode( )方法会撤销偏向锁,因为位置不够存放 hashcode。轻量级锁 hashcode 会存放在线程栈帧锁记录里,重量级锁会放在 monitor 对象中
    • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
    • 调用 wait / notify(重量级锁专属方法)
批量重偏向

如果对象被多个线程访问,但没有竞争(使用时间不同),这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID,这里针对的是一个类的对象

当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢?于是会在给这些对象加锁时重新偏向至加锁线程

撤销一个线程的偏向锁20次,会重偏向新线程。

批量撤销

撤销偏向锁阈值超过40次后,JVM会这样觉得,自己却是偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向(001-Normal)的,新建的对象也是不可偏向的。

https://www.bilibili.com/video/BV16J411h7Rd?p=86&spm_id_from=pageDriver

锁消除
截屏2021-10-12下午9.46.28

wait notify

原理之 wait/notify
截屏2021-10-12下午9.50.30
  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet变为WAITING状态
  • BLOCKED和WAITING的线程都处于【阻塞状态】,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放锁时唤醒
  • WAITING线程会在Owner线程调用 notifynotifyAll 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争
API 介绍
  • obj.wait( ):让进入 object 监视器的线程到 WaitSet 等待
  • obj.notify( ) :在 object 上正在 WaitSet 等待的线程中挑一个唤醒
  • obj.notifyAll( ) :让 object 上正在 WaitSet 等待的线程全部唤醒

它们都是线程间协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这些方法

正确使用姿势

sleep( long n )和 wait( long n )的区别

  1. sleep 是Thread方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起使用
  3. sleep 在睡眠同时,不会释放锁对象,但 wait 会释放
synchronized(lock){
  while(条件不成立){
    lock.wait();
  }
  // TODO
}
// 另一个线程
synchronized(lock){
  lock.notifyAll();
}
同步模式之保护性暂停(Guarded Suspension)

用一个线程等待另一个线程的执行结果

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
  • 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(见生产者/消费者)
  • JDK中,join的实现、Future的实现,都是基于此模式
  • 因为要等待另一方的结果,因此归类到同步模式
截屏2021-10-13上午10.58.29

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        GuardedObject g = new GuardedObject();
        new Thread(()->{
            g.complete(null);
        },"t0").start();

        new Thread(()->{
            System.out.println("wait result");
            List<String > result =(List<String>)g.get(1000);
            System.out.println(result);
        },"t1").start();
        new Thread(()->{
            System.out.println("begin");
            List<String> download = null;
            try {
                download = Downloader.download();
                g.complete(download);
            } catch (IOException e) {
                e.printStackTrace();
            }
        },"t2").start();

    }
  class GuardedObject{
    private Object response;

    // 设置超时时间
    public Object get(long timeout){
      synchronized (this) {
        // 记录时间
        long begin = System.currentTimeMillis();
        long passedTime = 0 ;
        // while 防止虚假唤醒步骤之一(同时代码块内也要做相关处理)
        while (this.response==null){
          long waitTime = timeout - passedTime;
          if(waitTime <= 0){
            break;
          }
          try {
            // 防止虚假唤醒步骤之二:每次醒来等待的最大时间都不同
            this.wait(waitTime);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          passedTime = System.currentTimeMillis() - begin;
        }
        return response;
      }
    }
    public void complete(Object o){
      synchronized (this){
        this.response = o;
        this.notifyAll();
      }
    }
  }
}
截屏2021-10-13下午2.50.49
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.ExecutionException;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 0; i < 3; i++) {
            new People().start();
        }
        Sleeper.sleep(1);

        for (Integer id : Mailboxes.getIds()) {
            new Postman(id,"内容" + id).start();
        }
    }
}

@Slf4j(topic = "c.People")
class People extends Thread {
    @Override
    public void run() {
        // 收信
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        log.debug("开始收信id:{}", guardedObject.getId());
        Object mail = guardedObject.get(5000);
        log.debug("收到信id:{},内容:{}", guardedObject.getId(), mail);
    }
}

@Slf4j(topic = "c.Postman")
class Postman extends Thread {
    private int id;
    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        // 发信
        GuardedObject guardObject = Mailboxes.getGuardObject(id);
        log.debug("送信 id:{}, 内容:{}", id, mail);
        guardObject.complete(mail);

    }
}

class Mailboxes {
    private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
    private static int id = 1;

    public static GuardedObject getGuardObject(int id) {
        // TODO 这里有问题
        return boxes.remove(id);
    }
  
    // 产生 GuardedObject 的唯一 id
    private static synchronized int generateId() {
        return id++;
    }

    public static GuardedObject createGuardedObject() {
        GuardedObject g = new GuardedObject(generateId());
        // map 线程安全,不需要 synchronized
        boxes.put(g.getId(), g);
        return g;
    }

    public static Set<Integer> getIds() {
        // map 线程安全,不需要 synchronized
        return boxes.keySet();
    }
}

class GuardedObject {
    // 标识 Guarded Object
    private int id;

    public int getId() {
        return id;
    }

    public GuardedObject(int id) {
        this.id = id;
    }

    private Object response;

    // 设置超时时间
    public Object get(long timeout) {
        synchronized (this) {
            // 记录时间
            long begin = System.currentTimeMillis();
            long passedTime = 0;
            // while 防止虚假唤醒步骤之一(同时代码块内也要做相关处理)
            while (this.response == null) {
                long waitTime = timeout - passedTime;
                if (waitTime <= 0) {
                    break;
                }
                try {
                    // 防止虚假唤醒步骤之二:每次醒来等待的时间都不同
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    public void complete(Object o) {
        synchronized (this) {
            this.response = o;
            this.notifyAll();
        }
    }
}
异步模式之生产者 / 消费者
  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果
  • 消息队列是由容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式
截屏2021-10-13下午3.41.05
@Slf4j(topic = "c.Test1")
public class Test1 {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                queue.put(new Message(finalI, "值" + finalI));
            }, "生产者" + i).start();
        }

        new Thread(() -> {
            while (true){
                Sleeper.sleep(1);
                queue.take();
            }
        }, "消费者").start();

    }
}

// 消息队列类,java 线程之间通信
@Slf4j(topic = "c.M")
class MessageQueue {
    // 队列集合、容量
    private LinkedList<Message> list = new LinkedList<>();
    private int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }

    // 取消息
    public Message take() {
        // 检查队列是否为空
        synchronized (list) {
            while (list.isEmpty()) {
                log.debug("队列空,消费线程等待");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 头部返回,尾部进入
            Message message = list.removeFirst();
            log.debug("已消费消息");
            list.notifyAll();
            return message;
        }
    }

    // 存消息
    public void put(Message message) {
        synchronized (list) {
            while (list.size() == capacity) {
                log.debug("队列满,生产者程等待");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.addLast(message);
            log.debug("已生产新的消息");
            list.notifyAll();
        }
    }
}

final class Message {
    private int id;
    private Object value;

    public int getId() {
        return id;
    }

    public Object getValue() {
        return value;
    }

    public Message(int id, Object value) {
        this.id = id;
        this.value = value;
    }
}

park & unpark

基本使用

LockSupport.park();  // 暂停当前线程
LockSupport.unpark( [Thread t] );   // 恢复某个线程的运行
  • wait notify notifyAll 必须配合 Object Monitor 一起使用,而 unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程, notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark ,而 wait & notify 不能先 notify

原理

每一个线程都有自己的一个 Parker 对象,由三部分组成 _counter _cond _mutex ,打个比喻

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷,_counter 就好比背包中的备用干粮(0为耗尽,1为充足)
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷休息
    • 如果备用干粮充足,那么不需要停留,继续前进
  • 调用 unpark,就好比补充干粮
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在继续运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需要停留继续前进
      • 因为背包空间有限,多次调用 unpark 只会补充一份备用干粮
截屏2021-10-13下午8.43.09
  1. 当前线程调用 Unsafe.park( ) 方法
  2. 检查 _counter,本情况为0,这时获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0
截屏2021-10-13下午8.46.55
  1. 调用 Unsafe.unpark(Thread_0)方法,设置 _counter 为1
  2. 唤醒 _cond 条件变量中的 Thread_0
  3. Thread_0 恢复运行
  4. 设置 _counter 为0

重新理解线程状态转换

截屏2021-10-11上午11.36.15 截屏2021-10-13下午8.53.08

假设有线程 Thread t

情况1 New --> RUNNABLE

  • 当调用 t.start( )方法时,由 NEW --> RUNNBALE

情况2 RUNNABLE <–> WAITING

t 线程用 synchronized(obj) 获得了锁对象后

  • 调用 obj.wait( ) 方法时,t线程从 RUNNABLE --> WAITING
  • 调用 obj.notify( ) ,obj.notifyAll( ),t.interrupt( )时
    • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 WITING --> BLOCKED
      • 竞争锁:notifyAll导致

情况3 RUNNABLE <–> WAITING

  • 当前线程调用 t.join( ) 方法时,当前线程从RUNNABLE --> WAITING
    • 注意事项当前线程t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 Interrupt( ) 时,当前线程从 WAITING --> RUNNABLE

情况4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park( ) 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark( 目标线程 )或调用了目标线程的 interrupt( ),会让目标线程从 WAITING --> RUNNABLE

情况5 RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized( obj ) 获取了锁对象后

  • 调用 obj.wait(long n) 方法时,t 线程从RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了n毫秒,或调用obj.notify( ), obj.notifyAll( ), t.interrupt( )时
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况6 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 注意是 当前线程t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt( ) 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n),当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从TIMED_WAITING --> RUNNABLE

情况8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark( 目标线程 )或调用了线程的 interrupt( ),或是等待超时,会让目标线程从TIMED_WAITING --> RUNNABLE

情况9 RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED
  • 持有 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有BLOCKED的线程重新竞争,如果其中 t 线程竞争成功,从BLOCKED --> RUNNABLE,其它失败的线程仍然BLOCKED

情况10 RUNNABLE --> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED

多把锁

截屏2021-10-13下午9.30.05

将锁的粒度细分

  • 好处:可以增强并发度
  • 坏处:如果一个线程同时获得多把锁,就容易发生死锁

活跃性

死锁

t1线程获得A对象锁,接下来想获得B对象的锁
t2线程获得B对象锁,接下来想获得A对象的锁

定位死锁
  • jconsole / jps -> jstack
public class TestDeadLock {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c1, c5).start();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while (true) {
            // 尝试获得左手筷子
            synchronized (left) {
                // 尝试获得右手筷子
                synchronized (right) {
                    eat();
                }
            }
        }
    }

    Random random = new Random();
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick{
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
活锁

活锁出现在两个线程相互改变对方的结束条件,最后谁也无法结束,

截屏2021-10-14上午8.03.05
饥饿

由于优先级太低,始终得不到CPU调度执行,也不能够结束

ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 synchronized 一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();
try{
  // 临界区
}finally{
  // 释放锁
  reentrantLock.unlock();
}
可重入

同一个线程如果首次获得了这把锁,那么因为它是这把锁的持有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

public static void main(String[] args) {
    m1();
}
public static void m1() {
    lock.lock();
    try {
        System.out.println("in m1");
        m2();
    } finally {
        lock.unlock();
    }
}
public static void m2() {
    lock.lock();
    try {
        System.out.println("in m2");
    } finally {
        lock.unlock();
    }
}
可打断
@Slf4j(topic = "c.001")
public class Test001 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                log.debug("尝试获取锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁,返回");
                return;
            }
            try {
                log.debug("获取到锁");
            } finally {
                lock.unlock();
            }
        },"t1");

        lock.lock();
        t1.start();

        Sleeper.sleep(1);
        log.debug("打断 t1");
        t1.interrupt();
    }
}
锁超时

立刻失败

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        log.debug("尝试获得锁");
        try {
            if (! lock.tryLock(2, TimeUnit.SECONDS)) {
                log.debug("获取不到锁");
                return;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.debug("获取不到锁");
            return;
        }
        try {
            log.debug("获得到锁");
        } finally {
            lock.unlock();
        }
    }, "t1");

    lock.lock();
    log.debug("获得到锁");
    t1.start();
    sleep(1);
    log.debug("释放了锁");
    lock.unlock();
}
* 哲学家问题的改进
public class TestDeadLock {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5,c1).start();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while (true) {
            if(left.tryLock()){
                try{
                    if(right.tryLock()){
                        try{
                            eat();
                        }finally {
                            right.unlock();
                        }
                    }
                }finally {
                   left.unlock();
                }
            }
        }
    }

    Random random = new Random();
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick extends ReentrantLock {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
公平锁

ReentrantLock 默认是不公平的,但是公平锁一般没有必要,会降低并发度

条件变量

synchronized 中也有条件变量,就是我们所讲的那个 WaitSet 休息室,当条件不满足时进入 WaitSet 等待;Reentrantock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等待消息
  • ReentrantLock 支持多件休息室,有专门等烟的休息室、专门早餐的休息室、唤醒时也是按照休息室来唤醒

使用流程 await( )

  • 前需要获得锁
  • 执行后,会释放锁
  • 执行方法的线程被唤醒(或打断、超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
@Slf4j(topic = "c.Test24")
public class Test24 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {

        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
            ROOM.lock();
            try {
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送外卖的").start();

        sleep(1);
        new Thread(() -> {
            ROOM.lock();
            try {
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送烟的").start();
    }
}
同步模式之顺序控制

顺序运行

// 方式之一:wait/notify
@Slf4j(topic = "c.002")
public class Test002 {
    static final Object lock = new Object();
    static boolean t2runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lock){
                while (!t2runned){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug("t1 running");
                }
            }
        },"t1");
        Thread t2 = new Thread(()->{
            synchronized (lock){
                log.debug("t2 running");
                t2runned = true;
                lock.notify();
            }
        },"t2");

        t1.start();
        t2.start();
    }
}
// 方式之二:park/unpark
@Slf4j(topic = "c.002")
public class Test002 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1 running");
        }, "t1");
        Thread t2 = new Thread(() -> {
            log.debug("2 running");
            LockSupport.unpark(t1);
        }, "t2");
        t1.start();
        t2.start();
    }
}

交替执行

// 方式之一:wait/notifyAll
@Slf4j(topic = "c.002")
public class WaitNotify {
    // 等待标记 1-a-2 2-b-3 3-c-1
    private int flag;
    // 循环次数
    private int loopNumber;

    public WaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
    public void print(String str, int waitFlag, int nextFlag) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this){
                while (flag!=waitFlag){
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                // 唤醒所有线程
                this.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        WaitNotify w = new WaitNotify(1,5);
        new Thread(()->{
            w.print("a",1,2);
        }).start();
        new Thread(()->{
            w.print("b",2,3);
        }).start();
        new Thread(()->{
            w.print("c",3,1);
        }).start();
    }
}
// 方式之二:ReentrantLock - Condition
class AwaitSignal extends ReentrantLock{
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str,Condition current,Condition next){
        for (int i = 0; i < loopNumber; i++) {
            lock();
            try{
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}

@Slf4j(topic = "003")
public class Test003 {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();
        new Thread(()->{
            awaitSignal.print("a",a,b);
        }).start();
        new Thread(()->{
            awaitSignal.print("b",b,c);
        }).start();
        new Thread(()->{
            awaitSignal.print("c",c,a);
        }).start();

        Thread.sleep(1000);
        awaitSignal.lock();
        try {
            a.signal();
        }finally {
            awaitSignal.unlock();
        }
    }
}
// 方式之三:park/unpark
class ParkUnpark{
    private int loopNumber;

    public ParkUnpark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str, Thread next){
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(next);
        }
    }
}


@Slf4j(topic = "004")
public class Test004 {
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
        ParkUnpark pu = new ParkUnpark(5);
        t1 = new Thread(()->{
            pu.print("a",t2);
        });
        t2 = new Thread(()->{
            pu.print("b",t3);
        });
        t3 = new Thread(()->{
            pu.print("c",t1);
        });
        t1.start();
        t2.start();
        t3.start();
        LockSupport.unpark(t1);
    }
}

本章小结

本章我们需要掌握的是

  • 分析多线程访问共享资源时,哪些代码片段属于临界区
  • 使用 synchronized 互斥解决临界区的线程安全问题
    • 掌握 synchronized 锁对象语法
    • 掌握 synchronized 加载成员方法和静态方法的语句
    • 掌握 wait/notify 同步方法
  • 使用 lock 互斥解决临界区的线程安全问题
    • 掌握 lock 使用细节:可打断、锁超时、公平锁、条件变量
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用
  • 了解线程活跃性问题:死锁、活锁、饥饿
  • 应用方面
    • 互斥:使用 synchronized 或 Lock 达到共享资源互斥的效果
    • 同步:使用 wait/notify 或 Lock的条件变量来达到线程间通信效果
  • 原理方面
    • monitor、synchronized、wait/notify 原理
    • synchronized 进阶原理
    • park & unpark 原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步模式之顺序控制

共享模型之内存

上一章讲解的Monitro主要关注的是访问共享变量时,保证临界区代码的原子性。这一章我深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题

Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。

JMM体现在以下几个方面

  • 原子性:保证指令不会受到线程上下文切换的影响
  • 可见性:保证指令不会受CPU缓存的影响
  • 有序性:保证指令不会受CPU指令并行优化的影响

可见性

退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t线程无法停止:

截屏2021-10-14下午8.50.41

为什么?原因如下:

  1. 初始状态,t 线程刚开始从主存中读取了 run 的值到工作内存
截屏2021-10-14下午8.52.16
  1. 因为 t 线程要频繁从主存中读取 run 的值,JIT编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存 run 的访问,提高效率
截屏2021-10-14下午8.54.26
  1. 1s之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
截屏2021-10-14下午8.55.05

解决方法

方式之一

// 易变 volatile
// 它可以用来修饰[成员变量]和[静态成员]变量,避免从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
volatile static boolean run = true 
  ...

方式之二

截屏2021-10-14下午9.01.18
可见性 vs 原子性

前面的例子体现的实际就是可见性,它保证的是多个线程之间,一个线程对 volatile 变量的修改对另外一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。

字节码理解:

截屏2021-10-14下午9.05.32

比较之前举的线程安全的例子:两个线程一个 i++ 一个 i–,只能保证看到最新值,不能解决指令交错

注意:

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。

如果在前面示例的死循环中加入 System.out.println( ) 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量修改了,那么因为其源码中有使用 synchronized 关键字

终止模式之两阶段终止模式 - 改进 & 犹豫模式

Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

@Slf4j(topic = "c.005")
public class TwoPhraseTermination {
    private volatile boolean stop = false;
    private boolean starting = false; // 犹豫模式改进
    private Thread monitorThread;

    public void start() {
        // 犹豫模式改进 
        synchronized (this) {
            if (starting) {
                return;
            }
            starting = true;
        }
        // 犹豫模式改进 
        monitorThread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "monitor");
        monitorThread.start();
    }

    public void stop() {
        stop = true;
        monitorThread.interrupt();
    }

    public static void main(String[] args) throws InterruptedException {
        TwoPhraseTermination t = new TwoPhraseTermination();
        t.start();

        Thread.sleep(10000);
        log.debug("停止监控");
        t.stop();
        t.stop();
        t.stop();
        t.stop();
    }
}
Balking 应用
@Service
@Slf4j
public class MonitorService {

    private volatile boolean stop;
    private volatile boolean starting;
    private Thread monitorThread;

    public void start() {
        // 缩小同步范围,提升性能
        synchronized (this) {
            log.info("该监控线程已启动?({})", starting);
            if (starting) {
                return;
            }
            starting = true;
        }
        // 由于之前的 balking 模式,以下代码只可能被一个线程执行,因此无需互斥
        monitorThread = new Thread(() -> {
            while (!stop) {
                report();
                sleep(2);
            }
            // 这里的监控线程只可能启动一个,因此只需要用 volatile 保证 starting 的可见性
            log.info("监控线程已停止...");
            starting = false;
        });

        stop = false;
        log.info("监控线程已启动...");
        monitorThread.start();
    }

    private void report() {...}

    private void sleep(long seconds) {...}

    public synchronized void stop() {
        stop = true;
        // 不加打断需要等到下一次 sleep 结束才能退出循环,这里是为了更快结束
        monitorThread.interrupt();
    }

}

它经常用来实现线程安全的单例

public final class Singleton{
	private Singleton(){}
  private static Sigleton INSTANCE = null;
  public static synchronized Singleton getInstace(){
      if(INSTANCE!=null){
        	return INSTANCE;
      }
    	INSTANCE = new Singleton();
  	  return INSTANCE;
  }
}

对比一下保护性暂停模式:保护性暂停模式用于在一个线程等待另一个线程的执行结果,当条件不满足时线程等待。

有序性

JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;

可以看到,至于是先执行 i 还是 j,对最终的结果不会产生影响。所以,上面代码真正执行时,可以使

i = ...;
j = ...;
//  或  //
j = ...;
i = ...;

这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,为什么要有指令重排这项优化呢?可以从CPU执行指令的原理来理解。

截屏2021-10-14下午9.58.54
诡异的结果
截屏2021-10-15上午9.58.11
  1. 线程1先执行,这时 ready = false,所以进入 else 分支结果为1
  2. 线程2先执行 num = 2,但没来得及执行 ready = true,线程1执行,还是进入else分支,结果为1
  3. 线程2执行到 ready = true,线程1执行,这回进入if分支,结果为4(因为 num 已经执行过了)
  4. 但结果还可能是0:线程2执行 ready = true,切换到线程1,进入if分支,相加为0,在切回线程2执行 num = 2
原理之 volatile

volatile 的底层原理是内存屏障(Memory Barrier/Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

image-20220725182205283

如何保证可见性
  • 写屏障(sfence)保证在该写屏障之前的,对共享变量的改动,都同步到主存当中

  • public void actor2(I_Result r){
      num = 2;
      ready = true; // ready是 volatile 赋值带写屏障
    }
    
  • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

  • public void actor1(I_Result r){
      // 读屏障
      // ready 是 volatile 读取值带读屏障
      if(ready){
        r.r1 = num + num;
      }else{
        r.r1 = 1;
      }
    }
    
Double-check locking 问题

以著名的 double-check locking 单例模式为例

截屏2021-10-15上午10.24.39

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstacne( ) 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外

但在多线程环境下,以上代码是有问题的,getInstance方法对应的字节码如下:

截屏2021-10-15上午10.30.05

其中

  • 17表示创建对象,将对象引用入栈
  • 20表示复制一份对象引用
  • 21表示利用一个对象引用,调用构造方法
  • 24表示利用一个对象引用,赋值给 static INSTANCE

也许JVM会优化为:先执行24,再执行21

截屏2021-10-15上午10.35.48

关键在于:getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值

这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的将是一个未初始化完毕的单例

解决办法

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在JDK5以上的版本 volatile 才会真正有效

hanppens-before

hanppens-before 规定了对共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量
static int x;
static Object m = new Object();
new Thread(()->{
  synchronized(m){
    x = 10;
  }
},"t1").start();
new Thread(()->{
  synchronized(m){
    System.out.println(x)
  }
},"t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
截屏2021-10-15上午10.59.33
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
截屏2021-10-15上午11.03.03
  • 线程结束前对变量的写,对其他线程得知它结束后的读可见(比如其它线程调用 t1.isAlive( ) 或 t1.join( ) 等待它结束)
截屏2021-10-15上午11.03.26
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted / t2.isInterrupted)
截屏2021-10-15上午11.06.06
  • 对变量默认值(0, false, null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb -> y 并且 y hb -> z 那么有 x hb -> z ,配合 volatile 的防指令重排,有下面的例子
volatile static int x;
static int y;
new Thread(()->{
  y = 10;
  x = 20;
},"t1").start();
new Thread(()->{
  // x = 20 对 t2 可见,同时 y=10 也对 t2 可见
  System.out.println(x);
},"t2").start();

习题

balking 模式习题
PrFP2P
线程安全单例习题

单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用 getInstance)时的线程安全,并思考注释中的问题

  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用对象时才会创建

实现1:

x0LU5b

https://www.bilibili.com/video/BV16J411h7Rd?p=155&spm_id_from=pageDriver

本章小结

本章重点讲解了JMM中的

  • 可见性-由JVM缓存优化引起
  • 有序性-由JVM指令重排优化引起
  • happens-before规则
  • 原理方面
    • CPU指令执行
    • volatile
  • 模式方面
    • 两阶段终止模式的 volatile 改进
    • 同步模式之 balking

共享模型之无锁

  • CAS 与 volatile
  • 原子整数
  • 原子引用
  • 原子累加器
  • Unsafe

CAS 与 volatile

前面看到的 AtomicInteger 的解决方法,内部并没有采用锁来保护共享变量的线程安全,那么它是如何实现的?

public void withdraw(Integer amount){
  	while(true){
      int prev = balance.get();
      int next = prev - amount;
      if(balance.compareAndSet(prev, next)){
        brea;
      }
  	}
	   ↓↓
		// getAndAdd( -1 * amount );
    
}
截屏2021-10-15下午8.22.04

注意:

其实CAS底层是 lock cmpxchg 指令(X86架构),在单核CPU和多核CPU下都能够保证【比较-交换】的原子性

  • 在多核状态下,某个核执行到 lock 的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程不会被线程的调度机制所打断,保证了多个线程对内存模型操作的准确性,是原子的。
volatile

获取共享变量时,为了保证变量的可见性,需要使用 volatile 修饰

它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意:

volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

CAS必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】

为什么无锁效率高
  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的情况下,发生上下文切换,进入阻塞,打个比喻:
截屏2021-10-15下午8.32.49
CAS 特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核CPU场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算修改了也没关系,可以尝试。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发、无阻塞并发
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的关键因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子整数

J.U.C并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以 AtomicInteger 为例

AtomicInteger i = new AtomicInteger(5);
i.incrementAndGet(); // ++i   return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
i.getAndIncrement(); // i++   return unsafe.getAndAddInt(this, valueOffset, 1);

i.getAndAdd(5);
i.addAndGet(5);

i.updateAndGet(x -> x * 10)
i.getAndUpdate(x -> x * 10)
//原理///
while(true){
  	int prev = i.get();
  	int next = prev * 10; // 此处可以改成 IntUnaryOperator的 applyAsInt 方法实现自定义计算
  	// int next = operator.applyAsInt(prev);
  	if(i.compareAndSet(prev, next)){
	      break;
    }
}
/

原子引用

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference
截屏2021-10-15下午9.09.06

主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种A->B->A的改动,如果主线程希望:

只有有其他线程【动过了】共享变量,那么自己的CAS就算失败,这时,仅比较值是不够的,需要再加一个版本号

AtomicStampedReference
截屏2021-10-15下午9.23.06

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个变化过程,如:A->B->A->C,通过AtomicStampedReference,我们可以知道,引用变量途中更改了几次。

但有时候,并不关心引用变量更改了几次,只是单纯地关心是否更改过,所以就需要AtomicMarkableReference

AtomicMarkableReference
截屏2021-10-15下午9.30.23

原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

有如下方法

截屏2021-10-16上午11.15.25

字段更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个字段(Field)进行原子操作只能配合 volatile 修饰的字段使用,否则会出现异常

原子累加器

  • LongAdder
  • DoubleAdder
截屏2021-10-16上午11.27.57 截屏2021-10-16上午11.27.38

性能提升的原因如下:有竞争时,设置多个累加单元,Thread-0累加Cell[0],而Thread-1累加Cell[1]…最后将结果汇总,这样他们在累加时操作不同的Cell变量,因此减少了CAS重试失败,从而提高性能

* 源码之 LongAdder

LongAdder 是并发大师@Author Doug Lea 的作品,设计的非常精巧

LongAdder 类有几个关键字段

// 累加单元数组,懒惰初始化
transient volatile Cell[] cells;
// 基础值,如果没有竞争,则用CAS累加这个字段
transient volatile long base;
// 在 cells 创建或扩容时,置为1,表示加锁
transient volatile int cellsBusy;

CAS 锁

截屏2021-10-16上午11.52.07
* 原理之伪共享

其中 Cell 即为累加单元

// 防止缓存行为伪共享
@sun.misc.Contended
static final class Cell{
  	volatile long value;
  	Cell(long x){value = x;}
  // 重要的方法,用CAS方式进行累加,prev表示旧值,next表示新值
  	final boolean cas(long prev,long next){
    	return UNSAFE.compareAndSwapLong(this, vauleOffset, prev ,next);
    // 省略不重要代码
  }
}
截屏2021-10-16下午6.25.35 截屏2021-10-16下午6.25.58

因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率

而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中

CPU要保证数据一致性,如果某个CPU核心修改了数据,其它的CPU核心对应的整个缓存行必须失效

截屏2021-10-16下午6.31.55

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24字节(16字节对象头和8字节value),因此缓存行可以存下2个Cell对象。这样问题来了:

  • Core-0要修改 Cell[0]
  • Core-1要修改 Cell[1]

无论谁修改成功,都会导致对方的Core缓存行失效。

@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的 padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

截屏2021-10-16下午6.39.31
LongAdder 源码解析
image-20220412212012703

https://www.bilibili.com/video/BV16J411h7Rd?p=180&spm_id_from=pageDriver

// increment 方法为入口
public void increment() {
  	add(1L);
}

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
      boolean uncontended = true;
      if (as == null || (m = as.length - 1) < 0 ||
          (a = as[getProbe() & m]) == null ||
          !(uncontended = a.cas(v = a.value, v + x)))
        longAccumulate(x, null, uncontended);
    }
}

截屏2021-10-16下午6.56.57

Nxe2so

截屏2021-10-16下午6.59.40

截屏2021-10-16下午7.16.11

截屏2021-10-16下午7.16.11

截屏2021-10-16下午9.48.52
Unsafe

Unsafe 对象提供了非常底层的、操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得。

Field u = Unsafe.class.getDeclaredField("theUnsafe");
u.setAccessible(true);
Unsafe unsafe = (Unsafe) u.get(null); //静态成员变量用null
Unsafe CAS 操作
// 获取域的偏移地址
long nameOffset = unsafe.objectFieldOffset(Student1.class.getDeclaredField("name"));
long ageOffset = unsafe.objectFieldOffset(Student1.class.getDeclaredField("age"));
// 执行cas操作
Student1 s = new Student1();
unsafe.compareAndSwapInt(s,ageOffset,0,111);
unsafe.compareAndSwapObject(s, nameOffset, null,"你好");
// 验证
System.out.println(s);

本章小结

  • CAS 与 volatile
  • API
    • 原子整数
    • 原子引用
    • 原子数组
    • 字段更新器
    • 原子累加器
  • Unsafe
  • 原理方面
    • LongAdder 源码
    • 伪共享

-

共享变量之不可变

日期转换的问题

问题提出

下面代码在运行时,由于 SimpleDateFormat 不是线程安全的

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for(int i = 0;i<10;i++){
  	new Thread(()->{
      try{
        System.out.println(sdf.parse("1951-04-21"));
      }catch(Exception e){
        System.out.println(e);
      }
    }).start();
}
解决
// 替换为JDK8的新日期格式化对象 DateTimeFormatter
DateTimeFormatter t = DateTimeFormatter.ofPattern("yyyy-MM-dd");

不可变设计

String 就是一个例子

截屏2021-10-16下午10.37.39
final 的使用

发现该类、类中所有属性都是 final 的

  • 属性用 final 修饰保证了该属性是只读的,不能修改
  • 类用 final 修饰保证了该类中的方法不能覆盖,防止子类无意间破坏不可变性
保护性拷贝

通过创建副本对象来避免共享的手段称之为【保护性拷贝】(defensive copy)

享元模式 Flyweight Pattern

当需要重用数量有限的同一类对象时

体现

在JDK中 Boolean、Byte、short、Integer、Long、Character 等包装类提供了 valueOf( )方法,例如Long的valueOf会缓存-128~127之间的Long对象,在这个范围之间会重用对象,大于这个范围,才会创建新的Long对象:

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

注意:

  • Btye、Short、Long 缓存范围都是-128~127
  • Character 缓存范围是 0~127
  • Integer 默认范围是 -128~127,最小值不能变,最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变
  • Boolean 缓存了 TRUE 和 FALSE
简单实现

例如:一个线上商城应用,QPS达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压跨数据库。

public class Test3 {
    public static void main(String[] args) {
        Pool pool = new Pool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Connection conn = pool.borrow();
                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                pool.free(conn);
            }).start();
        }
    }
}

@Slf4j(topic = "c.Pool")
class Pool {
    // 1. 连接池大小
    private final int poolSize;
    // 2. 连接对象数组
    private Connection[] connections;
    // 3. 连接状态数组 0 表示空闲, 1 表示繁忙
    private AtomicIntegerArray states;
  
  	private Semaphore semaphore;
    // 4. 构造方法初始化
    public Pool(int poolSize) {
      	this.semaphore = new Semaphore(poolSize);
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConnection("连接" + (i+1));
        }
    }
    // 5. 借连接
    public Connection borrow() {
        while(true) {
            for (int i = 0; i < poolSize; i++) {
                // 获取空闲连接
                if(states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        log.debug("borrow {}", connections[i]);
                        return connections[i];
                    }
                }
            }
            // 如果没有空闲连接,当前线程进入等待
            synchronized (this) {
                try {
                    log.debug("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 6. 归还连接
    public void free(Connection conn) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == conn) {
                states.set(i, 0);
                synchronized (this) {
                    log.debug("free {}", conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

以上实现没有考虑:

  • 连接的动态增长和收缩
  • 连接保活(可用性检测)
  • 等待超时处理
  • 分布式 hash

对于关系型数据库,有比较成熟的连接池实现,如C3P0、Druid等
对于更通用的对象池,可以考虑使用Apache common pool,例如Redis连接池可以参考 jedis 中关于连接池的实现

无状态

在web阶段学习时,设计Servlet时为了保证其线程安全,都会有这样的建议,不要为Servlet设置成员变量,这种没有任何成员变量的类是线程安全的:因为成员变量保存的数据称为状态信息,因此没有成员变量就称之为【无状态】

final 原理

设置 final 变量

理解了 volatile ,再对比 final 的实现就比较简单了

public class TestFinal{
  final int a = 20;
}

字节码

截屏2021-10-17上午10.55.10

发现 fianl 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其他线程读到他的值时不会出现为0的情况

本章小结

  • 不可变类的使用
  • 不可变类的设计
  • 原理方面
    • final
  • 模式方面
    • 享元模式

-

共享模型之工具

线程池

自定义线程池
截屏2021-10-18上午8.28.21
@Slf4j(topic = "c.TestPool")
public class TestPool {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
            // 1. 死等
//            queue.put(task);
            // 2) 带超时等待
//            queue.offer(task, 1500, TimeUnit.MILLISECONDS);
            // 3) 让调用者放弃任务执行
//            log.debug("放弃{}", task);
            // 4) 让调用者抛出异常
//            throw new RuntimeException("任务执行失败 " + task);
            // 5) 让调用者自己执行任务
            task.run();
        });
        for (int i = 0; i < 4; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
    void reject(BlockingQueue<T> queue, T task);
}

@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
    // 任务队列
    private BlockingQueue<Runnable> taskQueue;

    // 线程集合
    private HashSet<Worker> workers = new HashSet<>();

    // 核心线程数
    private int coreSize;

    // 获取任务时的超时时间
    private long timeout;
    private TimeUnit timeUnit;
  
		// 拒绝策略
    private RejectPolicy<Runnable> rejectPolicy;

    // 执行任务
    public void execute(Runnable task) {
        // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
        // 如果任务数超过 coreSize 时,加入任务队列暂存
        synchronized (workers) {
            if(workers.size() < coreSize) {
                Worker worker = new Worker(task);
                log.debug("新增 worker{}, {}", worker, task);
                workers.add(worker);
                worker.start();
            } else {
//                taskQueue.put(task);
                // 1) 死等
                // 2) 带超时等待
                // 3) 让调用者放弃任务执行
                // 4) 让调用者抛出异常
                // 5) 让调用者自己执行任务
                taskQueue.tryPut(rejectPolicy, task);
            }
        }
    }

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
        this.rejectPolicy = rejectPolicy;
    }

    class Worker extends Thread{
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run() {
            // 执行任务
            // 1) 当 task 不为空,执行任务
            // 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
//            while(task != null || (task = taskQueue.take()) != null) {
            while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    log.debug("正在执行...{}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers) {
                log.debug("worker 被移除{}", this);
                workers.remove(this);
            }
        }
    }
}
@Slf4j(topic = "c.BlockingQueue")
class BlockingQueue<T> {
    // 1. 任务队列
    private Deque<T> queue = new ArrayDeque<>();

    // 2. 锁
    private ReentrantLock lock = new ReentrantLock();

    // 3. 生产者条件变量
    private Condition fullWaitSet = lock.newCondition();

    // 4. 消费者条件变量
    private Condition emptyWaitSet = lock.newCondition();

    // 5. 容量
    private int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    // 带超时阻塞获取
    public T poll(long timeout, TimeUnit unit) {
        lock.lock();
        try {
            // 将 timeout 统一转换为 纳秒
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()) {
                try {
                    // 返回值是剩余时间
                    if (nanos <= 0) {
                        return null;
                    }
                  	// nanos每次唤醒后减少->虚假唤醒
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞获取
    public T take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞添加
    public void put(T task) {
        lock.lock();
        try {
            while (queue.size() == capcity) {
                try {
                    log.debug("等待加入任务队列 {} ...", task);
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    // 带超时时间阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit) {
        lock.lock();
        try {
            long nanos = timeUnit.toNanos(timeout);
            while (queue.size() == capcity) {
                try {
                    if(nanos <= 0) {
                        return false;
                    }
                    log.debug("等待加入任务队列 {} ...", task);
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }

    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            // 判断队列是否满
            if(queue.size() == capcity) {
                rejectPolicy.reject(this, task);
            } else {  // 有空闲
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}
ThreadPoolExcutor
截屏2021-10-18上午8.39.54
线程池状态

ThreadPoolExcutor 使用 int 的高3位来表示线程池状态,低29位表示线程数量

状态名高3位接收新任务处理阻塞队列任务说明
RUNNING111YY
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在进行的任务,并抛弃阻塞队列任务
TIDYING010--任务全部执行完毕,活动线程为0即将进入中介
TERMINATED001--终结状态

从数字上比较:TERMINATED>TIDYING>STOP>SHUTDOWN>RUNNING

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

截屏2021-10-18上午8.57.38
构造方法
public ThreadPoolExcutor(int corPooSize,
                        int maximumPooSize,
                        int long keepAliveTime,
                        TimeUnit, unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExcutionHandler handler)
  • corePoolSize:核心线程数目(最多保留的线程数)
  • maximumPoolSize:最大线程数目
  • keepAliveTime:生存时间-针对救急线程
  • unit:时间单位-针对救急线程
  • workQueue:阻塞队列
  • threadFactory:线程工厂-可以为线程创建时起个好名字
  • handler:拒绝策略

工作方式:

截屏2021-10-18上午9.02.14 纠正一个错误
  • 如果线程到达 maximumPoolSize 仍然有新任务,这时会执行拒绝策略。JDK提供了四种实现,其他著名框架也提供了实现。
    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,默认
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
    • Dubbo实现:在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题。
    • Netty实现:创建一个新线程执行任务
    • ActiveMQ实现:带超时等待(60s)尝试放入队列
    • PinPoint实现:拒绝策略链
  • 当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。

根据这个构造方法,JDK Executors 类中提供了众多的工厂方法来创建各种用途的线程池

newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
 		return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory);
}
  • 核心线程数 = 最大线程数(没有救急线程),因此无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务
  • 适用于任务量已知,相对耗时的任务
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • 核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,意味着:
    • 全部线程都是救急线程(60s回收)
    • 就几线程可以无限创建
  • 队列采用了 SynchronousQueue ,其特点是:没有容量,没有线程来取是放不进去的(放的线程会阻塞)
  • 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。
  • 适合任务数比较密集,但每个任务执行时间较短的情况
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会创建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor( )线程个数始终为1,不能修改
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1)初始时为1,以后还可以修改
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorepoolSize 等方法进行修改
提交任务
截屏2021-10-18上午10.05.36
关闭线程池
/*
	线程池状态变为 SHUTDOWN
	- 不会接收新任务
	- 但已提交任务会执行完
	- 此方法不会阻塞调用线程的执行
*/
void shutdown();
截屏2021-10-18上午10.38.51
/*
	线程池状态变为 STOP
	- 不会接收新任务
	- 会将队列中的任务返回
	- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();
// 不在RUNNING状态的线程池,此方法就返回true
boolean isShutdown();
// 线程池状态是否是 TERMINATED
boolean isTerminated();
// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以调用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
异步模式之工作线程
定义

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,能提升效率

饥饿

固定大小线程池会有饥饿现象

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就对了
  • 比如工人A处理了点餐任务,接着工人B把菜做好,然后上菜,他们配合挺好
  • 但现在同时来了两个客人,这时候工人A和B都去处理点餐了,这时没人做饭了->“死锁”
创建多少线程池合适
  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存

CPU 密集型运算

通常采用 CPU 核数 + 1 能够实现最优的CPU利用率, +1 是保证当前线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证CPU时钟周期不被浪费

I/O 密集型运算

CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用CPU资源,但执行IO操作、远程RPC调用时、数据库操作时,这时候CPU就闲下来了,可以利用多线程提高它的利用率。

经验公式如下:

线程数 = 核数 * 期望 CPU利用率 * 总时间(CPU计算时间+等待时间)/ CPU计算时间

例如4核CPu计算时间是50%,其它等待时间是50%,期望CPU被100%利用,套用公式: 4*100%*100%/50%=8

4核CPu计算时间是10%,其它等待时间是90%,期望CPU被100%利用,套用公式:

ScheduledExecutorService

在【任务调度线程池】功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将影响到之后的任务。

ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
捕获异常
  • try - catch
  • Future - get( )获取结果(封装有异常信息)
具体应用
// 如何让每周四18:00:00定时执行任务?
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);

LocalDateTime now = LocalDateTime.now();
LocalDateTime p = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);

if(now.compareTo(p)>0){
  p = p.plusWeeks(1);
}

long initialDelay = Duration.between(now, p).toMillis();
long period = 1000 * 60 * 60 * 24 * 7;

pool.scheduleAtFixedRate(() -> {
  System.out.println("running");
}, initialDelay, period, TimeUnit.MILLISECONDS);
Tomcat 线程池

Tomcat 在哪里用到了线程池呢?

截屏2021-10-19上午11.22.34
  • LimitLatch:用来限流,可以控制最大连接个数,类似于JUC中的 Semaphore
  • Acceptor:负责【接受新的Socket连接】
  • Poller:只负责监听 SocketChannel 是否有【可读的IO事件】
    • 一旦可读,封装一个任务对象(SocketProcessor),提交给Executor线程池处理
  • Executor:线程池中的工作线程最终负责【处理请求】

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

  • 如果线程总数达到 maximumPoolSize
    • 这时不会立刻抛出 RejectedExecutionException 异常
    • 而是再次尝试将任务放入队列,如果仍然失败,才抛出 RejectedExecutionException
Connector 配置
配置项默认值说明
acceptorThreadCount1acceptor 线程数量
pollerThreadCount1poller 线程数量
minSpareThreads10核心线程数,即 corePoolSize
maxThreads200最大线程数,即 maximumPoolSize
executor-Executor 名称,用来引用下面的 Executor
Executor 线程配置(优先级更高)
配置项默认值说明
threadPriority5线程优先级
daemontrue是否守护线程
minSpareThreads25核心线程数,即 corePoolSize
maxThreads200最大线程数,即 maximumPoolSize
maxIdleTime60000线程生存时间,单位是毫秒,默认值1分钟
maxQueueSizeInteger.MAX_VALUE队列长度
prestartminSpareThreadsfalse核心线程是否在服务器启动时启动
截屏2021-10-19上午11.45.18
Fork/Join
概念

Fork/Join 是JDK1.7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的CPU密集型运算

所谓任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序,斐波那契数列、都可以用分值思想进行求解

Fork/Join 在分支的基础上加入了多线程,可以把每个人物的分解和合并交给不同的线程来完成,进一步提升了运算效率

Fork/Join 默认会创建与CPU核心数相同的线程池

使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecusiveAction(没有返回值)

// begin - end 求和
@Override
protected Integer compute() {
    if (begin == end) {
        return begin;
    }
    if (end - begin == 1) {
        return end + begin;
    }
    int mid = (end + begin) / 2;
    MyTask009 t1 = new MyTask009(begin, mid);
    t1.fork();
    MyTask009 t2 = new MyTask009(mid + 1, end);
    t2.fork();
    return t1.join() + t2.join();
}

J.U.C

AQS原理
概述

全称 AbstractQueuedSynchronizer,是阻塞式锁和同步器工具的框架

特点:

  • 用 stat 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    • getState - 获取 state 状态
    • setState - 设置 state 状态
    • compareAndSetState - 乐观锁机制设置 state 状态
    • 独占模式只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于FIFO的等待队列,类似于 Monitor 的 EntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

子类主要实现这样一些方法(默认抛出UnsupportedOperationException)

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively
ReentrantLock 原理
截屏2021-10-19下午4.37.49
非公平锁实现原理
加锁解锁流程
public ReentrantLock(){
  	// NonfairSync 继承自 AQS
	  sync = new NonfairSync(); 
}

没有竞争时

截屏2021-10-19下午4.39.33

第一个竞争出现时

截屏2021-10-19下午4.41.58

Thread - 1 执行了:

  1. CAS 尝试将 state 由 0 改为 1 ,结果失败
  2. 进入 tryAcquire( ) 逻辑,这时 state 已经是1,结果仍然失败
  3. 接下来进入 addWaiter 逻辑,构造 Node 队列
    • 图中黄色三角表示该 Node 的 WaitStatus 状态,其中0为默认正常状态
    • Node的创建是懒惰的
    • 其中第一个Node称为Dummy(哑元)或哨兵,用来占位,并不关联线程
截屏2021-10-19下午4.46.36

当前线程进入acquireQueued逻辑(会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞)

  1. 如果自己是紧邻着 head(第二个Node),那么再次 tryAcquire 尝试获得锁,当然这时 state 仍未1,失败
  2. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的waitStatus改为-1,这次返回false
截屏2021-10-19下午4.56.52
  1. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued,再次 tryAcquire 尝试获得锁,当然这时 state 仍为 1,失败
  2. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱node的WaitStatus已经是-1,这次返回true
  3. 进入 parkAndCheckInterrupt,Thread-1 park(灰色表示)
截屏2021-10-19下午4.59.16

再次有多个线程经历上述过程竞争失败,变成这个样子 ↓

截屏2021-10-19下午5.07.05

Thread - 0 释放锁,进入 tryRelaese 流程,如果成功

  • 设置 exclusiveOwnerThread 为 null
  • state = 0
截屏2021-10-19下午5.08.22

当前队列不为 null,并且 head 的 WaitStatus = -1,进入 unparkSuccessor 流程

找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1

回到 Thread-1 的 acquireQueued 流程

截屏2021-10-19下午5.14.06

如果加锁成功(没有竞争),会设置

  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,从而可被垃圾回收

如果此时有其他线程来竞争(非公平的体现),例如这时有Thread-4来了

image-20211019173056437

如果不巧又被Thread-4占了先

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
可重入原理

https://www.bilibili.com/video/BV16J411h7Rd?p=242

image-20211019200547471 截屏2021-10-19下午8.04.42
可打断原理
不可打断模式

在此模式下,即使它被打断,仍会驻留在AQS队列中,等获得锁后方能继续运行(是继续运行!只是打断标记被设置为 true)

image-20211019201117115 image-20211019201409718 image-20211019201426543
可打断模式
image-20211019202147783 image-20211019202217540
公平锁实现原理

非公平

image-20211019205441375

公平

截屏2021-10-19下午8.58.04 截屏2021-10-19下午8.58.47
条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

await 流程

开始 Thread-0 持有锁,进入 ConditionObject 的 addConditionWaiter 流程,创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

image-20211019210144268

接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

image-20211019210407570

unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

image-20211019210726921

park 阻塞 Thread-0

image-20211019210851953
signal 流程

假设 Thread-1 要来唤醒 Thread-0

image-20211019211017726

进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在Node

image-20211019211234082

执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为0,Thread-3的 waitStatus 改为 -1

image-20211019211639290

Thread-1 释放锁,进入 unlock 流程,略

读写锁
ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用读写锁让读-读可以并发,提高性能。类似于数据库中的 select … from … lock in share mode

提供一个数据容器类内部分别使用读锁保护数据的 read( ) 方法,写锁保护数据的 write( ) 方法

ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rw.writeLock();

注意事项:

  • 读锁不支持条件变量
  • 重入时升级不支持:即持有读锁的情况下区获取写锁,会导致获取写锁永久等待
  • 重入时降级支持:即持有写锁的情况下去获取读锁
应用之缓存
缓存更新策略

先清缓存

image-20211019214943284

先更新数据库

image-20211019215149280
public class TestGenericDao {
    public static void main(String[] args) {
        GenericDao dao = new GenericDaoCached();
        System.out.println("============> 查询");
        String sql = "select * from emp where empno = ?";
        int empno = 7369;
        Emp emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);
        emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);
        emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);

        System.out.println("============> 更新");
        dao.update("update emp set sal = ? where empno = ?", 800, empno);
        emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);
    }
}

class GenericDaoCached extends GenericDao {
    private GenericDao dao = new GenericDao();
    private Map<SqlPair, Object> map = new HashMap<>();
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();

    @Override
    public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
        return dao.queryList(beanClass, sql, args);
    }

    @Override
    public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
        // 先从缓存中找,找到直接返回
        SqlPair key = new SqlPair(sql, args);;
        rw.readLock().lock();
        try {
            T value = (T) map.get(key);
            if(value != null) {
                return value;
            }
        } finally {
            rw.readLock().unlock();
        }
        rw.writeLock().lock(); // 唯一线程进入
        try {
            // 解锁后,多个线程进入
            T value = (T) map.get(key);
            if(value == null) {
                // 缓存中没有,查询数据库
                value = dao.queryOne(beanClass, sql, args);
                map.put(key, value);
            }
            return value;
        } finally {
            rw.writeLock().unlock();
        }
    }

    @Override
    public int update(String sql, Object... args) {
        rw.writeLock().lock();
        try {
            // 先更新库
            int update = dao.update(sql, args);
            // 清空缓存
            map.clear();
            return update;
        } finally {
            rw.writeLock().unlock();
        }
    }

    class SqlPair {
        private String sql;
        private Object[] args;

        public SqlPair(String sql, Object[] args) {
            this.sql = sql;
            this.args = args;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            SqlPair sqlPair = (SqlPair) o;
            return Objects.equals(sql, sqlPair.sql) &&
                    Arrays.equals(args, sqlPair.args);
        }
        @Override
        public int hashCode() {
            int result = Objects.hash(sql);
            result = 31 * result + Arrays.hashCode(args);
            return result;
        }
    }

}

注意:

  • 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但下面的问题没有考虑
    • 适合读多写少,如果写操作比较频繁,以上实现性能低
    • 没有考虑缓存容量
    • 没有考虑缓存过期
    • 只适合单机
    • 并发性还是低,目前只会用一把锁
    • 更新方法太简单粗暴,清空了所有 key
读写锁原理
图解流程

读写锁用的是同一个 Sync 同步器,因此等待队列,state 等也是同一个

t1 w.lock t2 r.lock

  1. t1成功上锁,流程与ReentrantLock加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高16位
image-20211020105641918 image-20211020141729564
  1. t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1)流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败
  • -1:失败
  • 0:成功,但后继节点不会继续唤醒
  • 正数:成功,数值是几表示有几个后继节点需要唤醒,读锁返回1
image-20211020142142624 image-20211020142158527 image-20211020142247424 image-20211020142721168
  1. 这时会进入 sync.doAcquireShared(1)流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍然处于活跃状态
image-20211020143259980
  1. t2 会看看自己的节点是不是老二,如果是,还会在此调用 tryAcquireShared(1) 来尝试获取锁
  2. 如果没有成功,会继续执行,把前驱结点的 WaitStatus 改为 -1(shouldParkAfterFailedAcquire方法,这次返回false,下次true),再 for(;😉 循环一次尝试 tryAcquireShared(1)如果还不成功,那么在 parkAndCheckInterrupt( )处 park
image-20211020145044977

t3 r.lock , t4 w.lock

在这种情况下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子

image-20211020145240700

t1 w.unlock

这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1)成功,变成下面的样子

image-20211020145459298

接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doacquireShared 内 parkAndCheckInterrupt( ) 处恢复运行

这回再来一次 for(;😉 执行 tryAcquireShared 成功则让读锁计数加一

image-20211020150125316

这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被设置为头结点

image-20211020150740597

这回再来一次 for(;;)执行 tryAcquireShared 成功则让读锁计数加一

image-20211020151828283

这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被设置为头结点

image-20211020151955135

下一个节点已经不是 Shared 状态了

t2 r.unlock, t3 r.unlock

t2 进入 sync.releaseShared(1)中,调用 tryReleaseShared(1)让计数减一,但由于计数还不为零

image-20211020152243879

t3 进入了 sync.releaseShared(1) 中,调用 tryReleaseShared(1)让计数减一,这回计数为零了,进入 doReleaseShared( )将头结点从 -1 改为 0 并唤醒老二,即

image-20211020152522964

之后 t4 在 acquireQuqued 中 parkAndCheckInterrupt 处 恢复运行,再次 for(;;)这次自己是老二,并且没有其他竞争,tryAcquire(1)成功,修改投机诶单,流程结束

image-20211020152832961
StampedLock

Since JDK 8,是为了进一步优化读性能,它的特点是在使用读锁,写锁时都必须配合【戳】使用

加解读锁

long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观锁,StampedLock 支持 tryOptimisticRead( )方法(乐观读),读取完毕后需要做一次戳校验如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
    // 锁升级
}
  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入
Semaphore

信号量,用来限制能同时访问共享资源的线程上限。

应用
  • 使用 Semaphore 限流,在访问高峰期,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
  • 用 Semaphore 实现简单连接池,对比【享元模式】下的实现(用 wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接池数是相等的
// 5. 借连接
public Connection borrow() {
    // 获取许可
    try {
        semaphore.acquire(); // 没有许可的线程,在此等待
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    for (int i = 0; i < poolSize; i++) {
        // 获取空闲连接
        if(states.get(i) == 0) {
            if (states.compareAndSet(i, 0, 1)) {
                log.debug("borrow {}", connections[i]);
                return connections[i];
            }
        }
    }
    // 不会执行到这里
    return null;
}
// 6. 归还连接
public void free(Connection conn) {
    for (int i = 0; i < poolSize; i++) {
        if (connections[i] == conn) {
            states.set(i, 0);
            log.debug("free {}", conn);
            semaphore.release();
            break;
        }
    }
}
原理

Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一

刚开始,permits( state ) 为3,这时5个线程来获取资源

image-20211020194818174

假设其中 Thread-1, Thread-2, Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入AQS队列park阻塞

image-20211020195853030

这时 Thread-4 释放了 permits,状态如下

image-20211020200149982

接下来 Thread-0 竞争成功,permits 再次设置为0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是0,因此 Thread-3 在尝试不成功后再次进入 park 状态

image-20211020200656457
CountdownLatch

用来进行线程同步协作,等待所有线程完成倒计时

其中构造参数用来初始化等待计数值,await( )用来等待计数归零,countDown( )用来让计数减一

* 应用之同步等待多线程准备完毕
image-20211020205910554
* 应用之同步等待多个远程调用结束
image-20211020210009490
↓改进↓
image-20211020210431933
↓改进↓
image-20211020210822956
CyclicBarrier

循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置【计数个数】,每个线程执行到某个需要“同步”的时刻调用await( )方法进行等待,当等待的线程数满足【计数个数】时,继续执行

注意:

【线程池线程数】需要与【计数个数】相同

线程安全集合类概述

image-20211020214407452

线程安全集合可以分为三大类:

  • 遗留的线程安全集合如 Hashtable,Vector
  • 使用 Collections 装饰的线程安全集合(Synchronized版本),如:
    • Collections.syncrhonizedCollection
    • Collections.synchronizedList
    • Collections.synchronizedMap
    • Collections.synchronizedSet
    • Collections.synchronizedNavigableMap
    • Collections.synchronizedNavigableSet
    • Collections.synchronizedSortedMap
    • Collections.synchronizedSortedSet
  • java.util.concurrent.*

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现他们有规律,里面包含三类关键词 Blocking、CopyOnWrite、Concurrent

  • Blocking 大部实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
    • 弱一致性
      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这是内容是旧的
      • 求大小弱一致性,size 操作未必是100%准确
      • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失效,抛出 ConcurrentModificationException,不再继续遍历

ConcurrenthashMap JDK8
public class TestWordCount {
    public static void main(String[] args) {
        demo(
                // 创建 map 集合
                // 创建 ConcurrentHashMap 对不对?
                () -> new ConcurrentHashMap<String, LongAdder>(8,0.75f,8),

                (map, words) -> {
                    for (String word : words) {
                        // 如果缺少一个 key,则计算生成一个 value , 然后将  key value 放入 map
                        //                  a      0
                        LongAdder value = map.computeIfAbsent(word, key -> new LongAdder());
                        // 执行累加,初始值为0
                        value.increment(); // 2

                        /*// 检查 key 有没有
                        Integer counter = map.get(word);
                        int newValue = counter == null ? 1 : counter + 1;
                        // 没有 则 put
                        map.put(word, newValue);*/
                    }
                }
        );
    }

    private static <V> void demo(Supplier<Map<String, V>> supplier, BiConsumer<Map<String, V>, List<String>> consumer) {
        Map<String, V> counterMap = supplier.get();
        // key value
        // a   200
        // b   200
        List<Thread> ts = new ArrayList<>();
        for (int i = 1; i <= 26; i++) {
            int idx = i;
            Thread thread = new Thread(() -> {
                List<String> words = readFromFile(idx);
                consumer.accept(counterMap, words);
            });
            ts.add(thread);
        }

        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(counterMap);
    }
}
CurrentHashMap 原理

https://www.bilibili.com/video/BV15b4y117RJ?p=90&t=1.2

image-20220726142507491

image-20220417192532020

重要属性和内部类

// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;

// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}

// hash 表
transient volatile Node<K,V>[] table;

// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;

// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}

// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}

// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}

// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}

重要方法

// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)

构造器分析

可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
  if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
      throw new IllegalArgumentException();
  if (initialCapacity < concurrencyLevel) // Use at least as many bins
      initialCapacity = concurrencyLevel; // as estimated threads
  long size = (long)(1.0 + (long)initialCapacity / loadFactor);
  // tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ... 
  int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size);
	this.sizeCtl = cap; 
}

get 流程

image-20211021160148102

put 流程

image-20220419164751428 image-20220419164827808 image-20220419164851511 image-20220419164621905 image-20220419164639558

size 计算流程

siez 计算实际发生在 put,remove 改变集合元素的操作之中,注意:计算结果仍然有误差

  • 没有竞争发生,向 baseCount 累加计数
  • 有竞争发生,新建 counterCells,向其中一个 cell 累加计数
    • counterCells 初始化有两个 cell
    • 如果计数竞争比较激烈,会创建新的cell来累加计数

transfer 流程

https://www.bilibili.com/video/BV16J411h7Rd?p=289&t=36.5

ConcurrenthashMap JDK7

它维护了一个 segment 数组,每个 segment 对应一把锁

  • 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与JDK8中是类似的
  • 缺点:Segments 数组默认大小为16,这个是固定不可变的,并且不是懒惰初始化

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:

image-20211021205013105

构造器分析

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
// ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
// segmentShift 默认是 32 - 4 = 28
        this.segmentShift = 32 - sshift;
// segmentMask 默认是 15 即 0000 0000 0000 1111
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
// 创建 segments and segments[0]
        Segment<K, V> s0 =
                new Segment<K, V>(loadFactor, (int) (cap * loadFactor),
                        (HashEntry<K, V>[]) new HashEntry[cap]);
        Segment<K, V>[] ss = (Segment<K, V>[]) new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

put 流程

put操作的步骤:

  • 首先,计算key的hash值
  • 其次,根据hash值找到需要操作的Segment的数组位置
  • Segment为空,调用ensureSegment()方法;否则,直接调用查询到的Segment的put方法插入值
public V put(K key, V value) {
    Segment<K,V> s;
    // concurrentHashMap不允许key/value为空
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    // 计算出 segment 下标
    int j = (hash >>> segmentShift) & segmentMask;

    // 获得 segment 对象, 判断是否为 null, 是则创建该 segment
    if ((s = (Segment<K,V>)UNSAFE.getObject
            (segments, (j << SSHIFT) + SBASE)) == null) {
        // 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null,
        // 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性
        s = ensureSegment(j);
    }
    // 进入 segment 的put 流程
    return s.put(key, hash, value, false);
}

segment 继承了可重入所(ReentrantLock),其put方法如下

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 尝试加锁
    HashEntry<K,V> node = tryLock() ? null :
            // 如果不成功, 进入 scanAndLockForPut 流程
            // 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程
            // 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
            scanAndLockForPut(key, hash, value);

    // 执行到这里 segment 已经被成功加锁, 可以安全执行
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        // 再利用 hash 值,求应该放置的数组下标
        int index = (tab.length - 1) & hash;
        // 返回数组中对应位置的元素(链表头部)
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                // 如果已经存在值,覆盖旧值
                K k;
                if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                // 新增
                // 1) 之前等待锁时, node 已经被创建, next 指向链表头
                if (node != null) // 非空,则表示为新创建的值
                    node.setNext(first);
                else
                    // 2) 创建新 node
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                // 3) 如果超过了该 segment 的阈值,这个 segment 需要扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    // 将 node 作为链表头,头插法
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock(); // 最终释放锁
    }
    return oldValue;
}

rehash 流程(扩容方法)

发生在 put 中,因为此时已经获得了锁,所以不需要考虑线程安全

private void rehash(HashEntry<K, V> node) {
        HashEntry<K, V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        int newCapacity = oldCapacity << 1;
        threshold = (int) (newCapacity * loadFactor);
        HashEntry<K, V>[] newTable =
                (HashEntry<K, V>[]) new HashEntry[newCapacity];
        int sizeMask = newCapacity - 1;
        for (int i = 0; i < oldCapacity; i++) {
            HashEntry<K, V> e = oldTable[i];
            if (e != null) {
                HashEntry<K, V> next = e.next;
                int idx = e.hash & sizeMask;
                if (next == null) // Single node on list
                    newTable[idx] = e;
                else { // Reuse consecutive sequence at same slot
                    HashEntry<K, V> lastRun = e;
                    北京市昌平区建材城西路金燕龙办公楼一层 电话:400 - 618 - 9090
                    int lastIdx = idx;
// 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用
                    for (HashEntry<K, V> last = next;
                         last != null;
                         last = last.next) {
                        int k = last.hash & sizeMask;
                        if (k != lastIdx) {
                            lastIdx = k;
                            lastRun = last;
                        }
                    }
                    newTable[lastIdx] = lastRun;
// 剩余节点需要新建
                    for (HashEntry<K, V> p = e; p != lastRun; p = p.next) {
                        V v = p.value;
                        int h = p.hash;
                        int k = h & sizeMask;
                        HashEntry<K, V> n = newTable[k];
                        newTable[k] = new HashEntry<K, V>(h, p.key, v, n);
                    }
                }
            }
        }
// 扩容完成, 才加入新的节点
        int nodeIndex = node.hash & sizeMask; // add the new node
        node.setNext(newTable[nodeIndex]);
        newTable[nodeIndex] = node;
// 替换为新的 HashEntry table
        table = newTable;
    }

get 流程

get 时并未加锁,用了UNSAFE方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get后发生就从新表取内容

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    // 1. hash 值
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    // 2. 根据 hash 找到对应的 segment
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        // 3. 找到segment 内部数组相应位置的链表,遍历
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

size 计算流程

  • 计算元素个数前,先不加锁计算两次,如果前后结果一样,认为个数正确返回
  • 如果不一样,进行重试,次数超过3,将所有 segment 锁住,重新计算个数返回
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
        final Segment<K, V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum; // sum of modCounts
        long last = 0L; // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (; ; ) {
                if (retries++ == RETRIES_BEFORE_LOCK) {
// 超过重试次数, 需要创建所有 segment 并加锁
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K, V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }
LinkedBlockingQueue
基本的入队出队

入队

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
        static class Node<E> {
            E item;
            /**
             * 下列三种情况之一
             * - 真正的后继节点
             * - 自己, 发生在出队时
             * - null, 表示是没有后继节点, 是最后了
             */
            Node<E> next;

            Node(E x) {
                item = x;
            }
        }
    }

初始化链表 last = head = new Node(null); Dummy 节点用来占位,item为null

image-20211021210339215

当一个节点入队 last = last.next = node;

image-20211021210415975

再来一个节点入队 last = last.next = node;

image-20211021210546605

出队

Node<E> h = head;
Node<E> first = h.next; h.next = h; // help GC
head = first; E x = first.item;
first.item = null;
return x;

h = head

image-20211021210640163

first = h.next

image-20211021210658309

h.next = h

image-20211021210844010

head = first

image-20211021210913926
E x = first.item;
first.item = null;
return x;
image-20211021211016422
加锁分析

高明之处在于用了两把锁和 dummy 节点

  • 同一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
  • 用两把锁,同一时刻,可以允许两个线程同时运行(一个生产者与一个消费者)运行
    • 消费者与消费者线程仍然串行
    • 生产者与生产者线程仍然串行

线程安全分析

  • 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
  • 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点),这时候,仍然是两把锁锁两个对象,不会竞争
  • 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,有阻塞
// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();

put 操作

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
				// count 用来维护元素计数
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
						// 满了等待
            while (count.get() == capacity) {
							// 倒过来读就好: 等待 notFull
                notFull.await();
            }
						// 有空位, 入队且计数加一
            enqueue(node);
            c = count.getAndIncrement();
						// 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
				// 如果队列中有一个元素, 叫醒 take 线程
        if (c == 0)
				// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
            signalNotEmpty();
    }

take 操作

public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
        if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
            signalNotFull();
        return x;
    }

性能比较

主要列举LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

  • Linked 支持有界,Array 强制有界
  • Linked 链表实现,Array 数组实现
  • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
  • Linked 每次入队会生成新的 Node,而 Array 的Node是提前创建好的
  • Linked 两把锁,Array一把锁
ConcurrentLinkedQuque

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是

  • 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
  • dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
  • 只是【锁】使用了CAS来实现

事实上,ConcurrentLinkedQueue 应用还是非常广泛的

例如之前的 Tomcat 的 Connector 结构,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用

image-20211021212907059
CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲

底层实现采用了写入时拷贝的思想,增删改操作底层会将数组拷贝一份,更改操作在新数组上执行,这时不影响其他的线程读并发,读写分离

image-20211021213108203

以上源码是JDK11,在JDK8中使用的是可重入锁而不是 synchronized

其它读操作并未加锁

image-20211021213314903

适合【读多写少】的应用场景

get 弱一致性

image-20211021213524434

弱一致性并非完全不好

  • 数据库的MVCC都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡
ThreadLocal
ThreadLocal、Thread、ThreadLocalMap联系
image-20220720232029705

image-20220726152618312

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值