Java——并发编程

笔记的环境

logback.xml配置

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration
        xmlns="http://ch.qos.logback/xml/ns/logback"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
        </encoder>
    </appender>
    <logger name="c" level="debug" additivity="false">
        <appender-ref ref="STDOUT"/>
    </logger>
    <root level="ERROR">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

依赖

java 复制代码
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

Java线程

创建和运行进程

  • 方法一:直接使用Thread

    java 复制代码
    Thread t1 = new Thread() {
        @Override
        public void run() {
            // 要执行的任务
        }	
    };
    // 启动线程
    t1.start();

    例如:

    java 复制代码
    // t1为线程名称,推荐创建线程时提供名称
    Thread t1 = new Thread("t1") {
    	// run 方法内实现了要执行的任务
        @Override
        public void run() {
            log.debug("running");
        }
    };
    // 启动线程
    t1.start();
  • 方法二:使用 Runnable 配合 Thread

    把【线程】和【任务】(要执行的代码)分开

    • Thread 代表线程
    • Runnable 可运行的任务(线程要执行的代码)
    java 复制代码
    Runnable runnable = new Runnable() {
      @Override
        public void run() {
            // 要执行的任务
        }
    };
    // 创建线程对象
    Thread t = new Thread(runnable);
    // 启动线程
    t.start();

    例如:

    java 复制代码
    Runnable r = new Runnable() {
        @Override
        public void run() {
            log.debug("running");
        }
    };
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    Thread t2 = new Thread(r, "t2");
    t2.start();
  • 方法三:FutureTask 配合 Thread

    FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

    java 复制代码
    // 创建任务对象
    FutureTask task = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            log.debug("running...............");
            Thread.sleep(1000);
            return 100;
        }
    });
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    Thread t3 = new Thread(task,"t3");
    t3.start();
    // 主线程阻塞,同步等待 task 执行完毕的结果
    log.debug("{}",task.get());

查看进程线程的方法

Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist ------ 查看进程
  • taskkill ------ 杀死进程

Linux

  • ps -fe ------ 查看所有进程
  • ps -fT -p <PID> ------ 查看某个进程(PID)的所有线程
  • kill ------ 杀死进程
  • top ------ 按大写H切换是否显示线程
  • top -H -p <PID> ------ 查看某个进程(PID)的所有线程

Java

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

jconsole 远程监控配置

win + r 调出运行窗口输入jconsole回车

  • 需要以如下方式运行你的 java 类

    java 复制代码
    java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
    Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
    Dcom.sun.management.jmxremote.authenticate=是否认证 java类
  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名

如果要认证访问,还需要做如下步骤

  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

线程运行原理

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

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

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

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

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

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

常见的方法

方法名 static 功能说明 注意
start() 启动一个新线程,在新的线程运行 run 方法中的代码 start方法只是让线程进入就绪 ,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run() 新线程启动后会调用的方法 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待n毫秒
getId() 获取线程长整型的id id唯一
getName() 获取线程名
setName(String name) 修改线程名
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的使用 主要是为了测试和调试

不推荐的方法

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

方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行

start 与 run

  • 调用run

    java 复制代码
    @Slf4j(topic = "c.Test4")
    public class Test4 {
        public static void main(String[] args) {
            Thread t1 = new Thread("t1") {
                @SneakyThrows
                @Override
                public void run() {
                    log.debug("running...........");
                    sleep(5000);
                }
            };
            t1.run();
            log.debug("do other things.....");
        }
    }

    运行结果

    程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

  • 调用 start

    java 复制代码
    @Slf4j(topic = "c.Test4")
    public class Test4 {
        public static void main(String[] args) {
            Thread t1 = new Thread("t1") {
                @SneakyThrows
                @Override
                public void run() {
                    log.debug("running...........");
                    sleep(5000);
                }
            };
            t1.start();
            log.debug("do other things.....");
        }
    }

    运行结果

    程序在 t1 线程运行,FileReader.read() 方法调用是异步的

  • 小结

    • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
    • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep 与 yield

  • sleep

    1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
    2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
    3. 睡眠结束后的线程未必会立刻得到执行
    4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
  • yield

    1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
    2. 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

    java 复制代码
    @Slf4j(topic = "c.Test8")
    public class Test8 {
        public static void main(String[] args) {
            Runnable task1 = () -> {
                int count = 0;
                for (;;){
                    System.out.println("----->1 " + count++);
                }
            };
            Runnable task2 = () -> {
                int count = 0;
                for (;;){
                    //Thread.yield();
                    System.out.println("          ----->2 " + count++);
                }
            };
    
            Thread t1 = new Thread(task1, "t1");
            Thread t2 = new Thread(task2, "t2");
            t1.setPriority(Thread.MIN_PRIORITY);
            t2.setPriority(Thread.MAX_PRIORITY);
            t1.start();
            t2.start();
        }
    }

join 方法详解

为什么需要 join

下面的代码执行,打印 r 是什么?

java 复制代码
@Slf4j(topic = "c.Test9")
public class Test9 {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

分析

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

解决方法

  • 用 sleep 行不行?为什么?
  • 用 join,加在 t1.start() 之后即可

有时效的join

  • join方法可以加一个long类型的参数,最多等待n毫秒
  • 如果n毫秒后该线程还没有结束,就终止等待
  • 如果在n毫秒之前该线程结束了,join会提前结束

interrupt方法详解

  • 打断sleep的线程,会清空打断状态

    java 复制代码
    @Slf4j(topic = "c.Test10")
    public class Test10 {
        public static void main(String[] args) throws InterruptedException{
            Thread t1 = new Thread(() -> {
                log.debug("sleep>>>>>>");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1");
    
            t1.start();
            TimeUnit.SECONDS.sleep(1);
            log.debug("interrupt");
            t1.interrupt();
            log.debug("打断标记:{}",t1.isInterrupted());
        }
    }

    运行结果


  • 打断正常运行的线程,不会清空打断状态

    java 复制代码
    @Slf4j(topic = "c.Test11")
    public class Test11 {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                while (true) {
                    // 获得当前线程的打断标记
                    boolean interrupted = Thread.currentThread().isInterrupted();
                    if (interrupted) {
                        log.debug("被打断了,退出  循环");
                        break;
                    }
                }
            }, "t1");
            t1.start();
            TimeUnit.SECONDS.sleep(1);
            log.debug("interrupt");
            t1.interrupt();
            log.debug("打断标记:{}",t1.isInterrupted());
        }
    }

    运行结果


  • 打断park线程

    打断park线程,不会清空打断状态

    java 复制代码
    @Slf4j(topic = "c.Test13")
    public class Test13 {
        public static void main(String[] args) throws InterruptedException {
            test3();
        }
        private static void test3() throws InterruptedException{
            Thread t1 = new Thread(() -> {
                log.debug("park...");
                LockSupport.park();
                log.debug("unpark...");
                log.debug("打断状态:{}", Thread.interrupted());
                LockSupport.park();
                log.debug("unpark...");
            }, "t1");
            t1.start();
            TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
        }
    }

    运行结果

    注意: 如果打断标记已经是true, 则 park 会失效

主线程与守护线程

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

  • java 复制代码
    @Slf4j(topic = "c.Test14")
    public class Test14 {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        break;
                    }
                }
                log.debug("结束");
            }, "t1");
            // 设置该线程为守护线程
            t1.setDaemon(true);
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            log.debug("结束");
        }
    }
  • 运行结果

注意

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

线程状态

五种状态

这是从 操作系统 层面来描述的

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

六种状态

这是从 Java API 层面来描述的(根据 Tread.State 枚举,分为六种状态)

  • NEW: 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE: 当调用了 start() 方法后,注意 Java API 层面的RUNNABLE 状态涵盖了 操作系统 层面的 可运行状态 、 运行状态 和 阻塞状态 (由于 BIO 导致的线程阻塞,在Java里无法区分,任然认为是可运行)
  • BLOCKED、WAITING、TIMED_WAITING: 都是 Java API 层面对 阻塞状态 的细分
  • TERMINATED: 当线程代码运行结束

六种状态演示

java 复制代码
@Slf4j(topic = "c.TestState")
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("running.......");
        }, "t1");

        Thread t2 = new Thread(() -> {
            while (true) {
            }

        }, "t2");
        t2.start();

        Thread t3 = new Thread(() -> {
            log.debug("running......");
        }, "t3");
        t3.start();

        Thread t4 = new Thread(() -> {
            synchronized (TestState.class) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t4");
        t4.start();

        Thread t5 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t5");
        t5.start();

        Thread t6 = new Thread(() -> {
            synchronized (TestState.class) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t6");
        t6.start();

        TimeUnit.SECONDS.sleep(1);

        // t1线程没有start ------ NEW
        log.debug("t1 state {}", t1.getState());
        // t2线程一直在运行属于 ------  RUNNABLE
        log.debug("t2 state {}", t2.getState());
        // t3线程运行结束 ------ TERMINATED
        log.debug("t3 state {}", t3.getState());
        // t4线程sleep中 ------ TIMED_WAITING
        log.debug("t4 state {}", t4.getState());
        // t5需要等待t2运行结束(由于t2是死循环会一直等待(没有加时限) ------ WAITING
        log.debug("t5 state {}", t5.getState());
        // t6拿不到锁 ------ BLOCKED
        log.debug("t6 state {}", t6.getState());
    }
}

运行结果

共享模型------管程

共享带来的问题

小故事

  • 老王(操作系统)有一个强大的算盘(CPU),现在想把它租出去,赚一点外快
  • 小南、小女(线程)来使用这个算盘进行一些计算,并按照时间给老王支付费用
  • 但小南不能一天24小时使用算盘,他经常眯一会(sleep),又或是去吃饭上厕所(阻塞IO操作),有时候还需要一根烟,没烟时思路全无(wait)这些情况统称为 阻塞
  • 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
  • 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
  • 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
  • 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
  • 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了一个笔记本(主存),把一些中间结果先记在本上
  • 计算流程是这样的
  • 但是由于分时系统,有一天还是发生了事故
  • 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
    老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...] 不甘心地到一边待着去了(上下文切换)
  • 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
  • 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写入了笔记本
  • 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0

临界区 Critical Section

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

竞态条件 Race Condition

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

synchronized解决方案

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

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

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

注意: 虽然Java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:

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

synchronized

语法:

java 复制代码
synchronized(对象){
	//临界区
}

方法上的synchronized

  • 成员方法

    java 复制代码
    class Test{
    	public synchronized void test(){
    	}
    }
    
    等价于
    class Test{
    	public void test(){
    		synchronized(this){
    		}
    	}
    }	
  • static方法

    java 复制代码
    class Test{
    	public sysnchronized static void test(){
    	}
    }
    
    等价于
    
    class Test{
    	public static void test(){
    		synchronized(Test.class){
    		}
    	}
    }

理解

可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

用图表示


不加 synchronized 的方法:不加 synchronized 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

线程八锁

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

  • 情况一:12或21

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
    
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n1.b();
            },"t2").start();
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public synchronized void a() {
            log.debug("1");
        }
    
        public synchronized void b() {
            log.debug("2");
        }
    }
  • 情况二:1s后12 或 2 1s后1

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
    
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n1.b();
            },"t2").start();
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public synchronized void a() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("1");
        }
    
        public synchronized void b() {
            log.debug("2");
        }
    }
  • 情况三:3 1s后 12 或 23 1s后 1 或 32 1s后1

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
    
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n1.b();
            }, "t2").start();
    
            new Thread(() -> {
                log.debug("begin");
                n1.c();
            }, "t3").start();
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public synchronized void a() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("1");
        }
    
        public synchronized void b() {
            log.debug("2");
        }
    
        public void c() {
            log.debug("3");
        }
    }
  • 情况四:2 1s后 1

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
            Number n2 = new Number();
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n2.b();
            }, "t2").start();
    
            
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public synchronized void a() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("1");
        }
    
        public synchronized void b() {
            log.debug("2");
        }
    }
  • 情况五:2 1s后 1

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n1.b();
            }, "t2").start();
    
    
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public static synchronized void a() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("1");
        }
    
        public synchronized void b() {
            log.debug("2");
        }
    }
  • 情况六: 1s后 12 或 2 1s后 1

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n1.b();
            }, "t2").start();
    
    
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public static synchronized void a() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("1");
        }
    
        public static synchronized void b() {
            log.debug("2");
        }
    }
  • 情况七: 2 1s后1

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
            Number n2 = new Number();
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n2.b();
            }, "t2").start();
    
    
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public static synchronized void a() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("1");
        }
    
        public  synchronized void b() {
            log.debug("2");
        }
    }
  • 情况八: 1s 后12 或 2 1s 后1

    java 复制代码
    @Slf4j(topic = "c.Test8Locks")
    public class Test8Locks {
        public static void main(String[] args) {
            Number n1 = new Number();
            Number n2 = new Number();
            new Thread(() -> {
                log.debug("begin");
                n1.a();
            }, "t1").start();
    
            new Thread(() -> {
                log.debug("begin");
                n2.b();
            }, "t2").start();
    
    
        }
    }
    
    @Slf4j(topic = "c.Number")
    class Number {
        public static synchronized void a() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("1");
        }
    
        public static synchronized void b() {
            log.debug("2");
        }
    }

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只是读操作,则线程安全
    • 如果有读写操作 ,则这段代码是是临界区,需要考虑线程安全

局部变量

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

  • 局部变量

    java 复制代码
    public static void test1(){
    	int i = 10;
    	i++;
    }

    每个线程调用test1()方法时局部变量i,会在每个线程的栈内存中被创建多份,因此不存在共享

  • 局部变量引用

    java 复制代码
    public class TestThreadSafe {
        static final int THREAD_NUMBER = 2;
        static final int LOOP_NUMBER = 200;
    
        public static void main(String[] args) {
            ThreadSafeSubClass test = new ThreadSafeSubClass();
            for (int i = 0; i < THREAD_NUMBER; i++) {
                new Thread(() -> {
                    test.method1(LOOP_NUMBER);
                }, "Thread" + i).start();
            }
        }
    }
    
    /**
     * 成员变量 ------ 线程不完全
     * 如果线程2 还未 add,线程1 remove 就会报错:
     * 分析:
     * 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
     * method3 与 method2 分析相同
     */
    class ThreadUnsafe {
        ArrayList<String> list = new ArrayList<>();
    
        public void method1(int loopNumber) {
            for (int i = 0; i < loopNumber; i++) {
                // { 临界区, 会产生竞态条件
                method2();
                method3();
                // } 临界区
            }
        }
    
        private void method2() {
            list.add("1");
        }
    
        private void method3() {
            list.remove(0);
        }
    }
    
    /**
     * 局部变量引用 ------ 线程安全
     * 分析:
     * list 是局部变量,每个线程调用时会创建其不同实例,没有共享
     * 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
     * method3 的参数分析与 method2 相同
     */
    class ThreadSafe {
    
        public void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                // { 临界区, 会产生竞态条件
                method2(list);
                method3(list);
                // } 临界区
            }
        }
    
        private void method2(ArrayList<String> list) {
            list.add("1");
        }
    
        private void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }
    
    /**
     * 局部变量引用 ------ 暴露引用 ------ 线程不安全
     * 方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
     * 情况1:有其它线程调用 method2 和 method3
     * 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,
     */
    class ThreadSafeTest {
    
        public void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                // { 临界区, 会产生竞态条件
                method2(list);
                method3(list);
                // } 临界区
            }
        }
    
        public void method2(ArrayList<String> list) {
            list.add("1");
        }
    
        public void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }
    
    class ThreadSafeSubClass extends ThreadSafeTest {
        @Override
        public void method3(ArrayList<String> list) {
            new Thread(() -> {
                list.remove(0);
            }).start();
        }
    }

    从上面可以看出 private 或 finally 提供 安全 的意义所在,开闭原则中的 闭

常见线程安全类

  • String
  • 包装类
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

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

  • 它们的每个方法是原子的
  • 注意 它们多个方法的组合不是原子的

Monitor 概念

Java对象头

以 32 位虚拟机为例

  • 普通对象

  • 数组对象

  • 其中 Mark Word 结构为

  • 64 位虚拟机 Mark Word

Monitor工作原理

Monitor 被翻译为 监视器管程

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

Monitor 结构如下

  • 刚开始 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

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

java 复制代码
static final Object obj = new Object();
public static void method1(){
	synchronized(obj){
		// 同步块 A
		method2();
	}
}
public static void method2(){
	synchronized(obj){
		// 同步块 B
	}
}
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
  • 如果 cas 失败,有两种情况
    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

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

java 复制代码
static Object obj = new Object();
public static void method1(){
	synchronized(obj){
		// 同步锁
	}
}
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

      -当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋优化

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

自旋重试成功的情况

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

自旋重试失败的情况

线程1(CPU1上) 对象 Mark 线程2(CPU上)
访问同步块,获取 monitor 10(重量锁)重量锁指针 ------
成功(加锁) 10(重量锁)重量锁指针 ------
执行同步块 10(重量锁)重量锁指针 ------
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞

  • 在Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自选成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核CPU自旋才能发挥优势
  • Java 7 之后不能控制是否开启自选功能

偏向锁

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

Java 6 中引入偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争

例如:

java 复制代码
static final Object obj = new Object():
public static void m1(){
	synchronized(obj){
		// 同步块 A
		m2();
	}
}
public static void m2(){
	synchronized(obj){
		// 同步块 B
		m3();
	}
]
public static void m3(){
	synchronized(obj){
		// 同步块 C
	}
}



一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0

  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟

  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值


撤销偏向锁

  • 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销

    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor 中记录 hashCode

    在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking

  • 其它线程使用对象: 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

  • 调用了 wait/notify

  • 批量重偏向: 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID

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

  • 批量撤销: 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

wait ------ notify

  • 由于条件不满足,小南不能继续进行计算
  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁放开,其它人可以由老王随机安排进屋
  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ](调用了 notify 方法)
  • 小南于是可以离开休息室,重新进入竞争锁的队列

原理

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入 EntryList 重新竞争

API介绍

  • obj.wait(): 让进入 object 监视器的线程到 waitSet等待
  • obj.notify(): 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll(): 让 object 上正在 waitSet 等待的线程全部唤醒

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

java 复制代码
@Slf4j(topic = "c.Test15")
public class Test15 {
    final static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();
        // 主线程两秒后执行
        TimeUnit.SECONDS.sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // 唤醒obj上一个线程
            // obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }

}
  • notify 的一种结果
  • notifyAll 的结果
  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

正确姿势

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

  • sleep 是 Thread 方法,而 wait 是Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放锁对象
  • 它们状态 TIMED_WAITING

正确使用 wait ------ notify 的套路

java 复制代码
synchronized(lock){
	while(条件不成立){
		lock.wait():
	}
	// 干活
}

// 另一个线程
synchronized(lock){
	lock.notifyAll();
}

Park & Unpark

基本使用

  • 它们是 LockSupport 类中的方法

    java 复制代码
    // 暂停当前线程
    LockSupport.park();
    // 恢复某个线程的运行
    LockSupport.unpark(暂停线程对象)
  • 先park 再 unpark

    java 复制代码
    Thread t1 = new Thread(() -> {
        log.debug("start...");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("park...");
        LockSupport.park();
        log.debug("resume...");
    }, "t1");
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    log.debug("unpark...");
    LockSupport.unpark(t1);
  • 运行结果


特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

原理:

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

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

多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法是准备多个房间(多个对象锁)


例如

java 复制代码
@Slf4j(topic = "c.BigRoom")
public class BigRoom {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"小南").start();

        new Thread(()->{
            try {
                bigRoom.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"女").start();

    }
    public void sleep() throws InterruptedException {
        synchronized(this){
            log.debug("sleeping 2 小时");
            TimeUnit.SECONDS.sleep(2);
        }
    }
    public void study() throws InterruptedException {
        synchronized (this){
            log.debug("study 1 小时");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

运行结果


改进

java 复制代码
@Slf4j(topic = "c.BigRoom")
public class BigRoom {
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"小南").start();

        new Thread(()->{
            try {
                bigRoom.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"女").start();

    }
    public void sleep() throws InterruptedException {
        synchronized(bedRoom){
            log.debug("sleeping 2 小时");
            TimeUnit.SECONDS.sleep(2);
        }
    }
    public void study() throws InterruptedException {
        synchronized (studyRoom){
            log.debug("study 1 小时");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

运行结果

活跃性

死锁

  • 有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

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

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

    例:

    java 复制代码
    @Slf4j(topic = "c.TestDeadLock")
    public class TestDeadLock {
    
        public static void main(String[] args) {
            test1();
        }
        private static void test1(){
            Object A = new Object();
            Object B = new Object();
            Thread t1 = new Thread(() -> {
                synchronized (A) {
                    log.debug("Lock A");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        log.debug("Lock B");
                        log.debug("操作");
                    }
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                synchronized (B) {
                    log.debug("Lock B");
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (A) {
                        log.debug("Lock A");
                        log.debug("操作");
                    }
                }
            }, "t2");
    
            t1.start();
            t2.start();
        }
    }

    运行结果:两个线程都获得一把锁,但是无法获得另一把锁,无法继续向下执行

  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

  • 避免死锁要注意加锁顺序

  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

活锁

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

    java 复制代码
    @Slf4j(topic = "c.TestLiveLock")
    public class TestLiveLock {
        static volatile int count = 10;
    
        public static void main(String[] args) {
            new Thread(() -> {
                // 期望减到 0 退出循环
                while (count > 0) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        count--;
                        log.debug("count:{}", count);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }, "t1").start();
    
            new Thread(() -> {
                // 期望减到 0 退出循环
                while (count < 20) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        count++;
                        log.debug("count:{}", count);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }, "t2").start();
        }
    }

饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题

先来看看使用顺序加锁的方式解决之前的死锁问题

顺序加锁的解决方案

ReentrantLock

相对于 synchronized 它具备如下特点

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

与 synchronized 一样,都支持可重入

基本语法

java 复制代码
// 获取锁
reenttantLock.lock()
try{
	// 临界区
}finally{
	// 释放锁
	reentrantLock.unlock():
}	

可重入

这里是引用可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

java 复制代码
@Slf4j(topic = "c.Test19")
public class Test19 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try{
            log.debug("enter main");
            m1();
        }finally {
            lock.unlock();
        }
    }
    public static void m1(){
        lock.lock();
        try{
            log.debug("enter m1");
            m2();
        }finally {
            lock.unlock();
        }
    }
    public static void m2(){
        lock.lock();
        try{
            log.debug("enter m2");
        }finally {
            lock.unlock();
        }
    }
}

可打断

示例

java 复制代码
@Slf4j(topic = "c.Test21")
public class Test21 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                // 如果没有竞争那么此方法就会获取 lock 锁对象
                // 如果有竞争就进入阻塞队列,可以被其它线程用 interrupt 方法打断
                log.debug("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁,返回");
                return;
            }
            try {
                log.debug("获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");
        log.debug("获得锁");
        lock.lock();
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.debug("打断 t1");
        t1.interrupt();
    }
}

锁超时

立刻失败

java 复制代码
@Slf4j(topic = "c.Test20")
public class Test20 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获得锁");
            // 如果获取不到锁立刻结束等待
            if(!lock.tryLock()){
                log.debug("获取锁失败");
                return;
            }
            /*
             可以添加等待的时间,如果等待时间到了,还没有获得锁,立刻结束等待
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("获取锁失败");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            try {
                log.debug("获得锁成功");
            }finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        log.debug("获得锁");
        t1.start();
    }
}

公平锁

  • ReentrantLock 默认是不公平的
  • 改为公平锁: ReentrantLock lock = new ReentrantLock(true);
  • 公平锁一般没有必要,会降低并发度

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

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

使用流程

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

使用例子

  • 代码

    java 复制代码
    @Slf4j(topic = "c.Test22")
    public class Test22 {
        static volatile boolean hasCigarette = false;
        static volatile boolean hasTakeout = false;
        static ReentrantLock root = new ReentrantLock();
        // 等待烟的休息室
        static Condition waitCigaretteSet = root.newCondition();
        // 等待外卖的休息室
        static Condition waitTakeoutSet = root.newCondition();
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                try {
                    root.lock();
                    log.debug("有烟没?【{}】", hasCigarette);
                    while (!hasCigarette) {
                        try {
                            log.debug("没烟,先歇会");
                            waitCigaretteSet.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("等到了它的烟,开始干活");
                } finally {
                    root.unlock();
                }
            }).start();
            new Thread(() -> {
                try {
                    root.lock();
                    log.debug("外卖送到没有?【{}】", hasTakeout);
                    while (!hasTakeout) {
                        try {
                            log.debug("外卖还没到再睡会");
                            waitTakeoutSet.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("等到了它的早餐,可以开始干活");
                } finally {
                    root.unlock();
                }
            }).start();
            TimeUnit.SECONDS.sleep(1);
            sendBreakfast();
            TimeUnit.SECONDS.sleep(1);
            sendCigarette();
        }
    
        private static void sendCigarette() {
            root.lock();
            try {
                log.debug("送烟来了");
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
                root.unlock();
            }
        }
    
        private static void sendBreakfast() {
            root.lock();
            try {
                log.debug("送早餐来了");
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
                root.unlock();
            }
        }
    }
  • 运行结果

共享模型------内存

Java 内存模型

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

JMM 体现在以下几个方面

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

可见性

退不出的循环

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

java 复制代码
@Slf4j(topic = "c.Test28")
public class Test28 {
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // ......
            }
        }, "t1");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.debug("停止t");
        run = false; // 线程t1不会如预想的停下来
    }
}

为什么呢?分析一下:

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

解决方法
volatile(易变关键字)

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

java 复制代码
@Slf4j(topic = "c.Test28")
public class Test28 {
    // 易变
    volatile static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (run) {
                // ......
            }
        }, "t1");
        t1.start();

        TimeUnit.SECONDS.sleep(1);
        log.debug("停止t1");
        run = false; // 线程t1不会如预想的停下来
    }
}

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

指令重排

有序性

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

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

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

java 复制代码
i = ...;
j = ...;

也可以是

java 复制代码
j = ...;
i = ...; 

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

指令并行优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段

专业术语:

  • instruction fetch (IF)
  • instruction decode (ID)
  • execute (EX)
  • memory access (MEM)
  • register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过 重排序组合 来实现 指令级并行 ,这一技术在 80's 中叶到 90's 中叶占据了计算架构的重要地位。

支持流水线的处理器

现代 CPU 支持 多级指令流水线 ,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线 。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

问题

java 复制代码
int num = 0;

boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
    num = 2;
    ready = true;
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

  • 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  • 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  • 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
  • 结果还有可能是 0
    这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

禁用指令重排

使用 volatile 关键字

java 复制代码
int num = 0;

volatile boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
    num = 2;
    ready = true;
}

volatile 原理

volatile e 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

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

如何保证可见性

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

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

    java 复制代码
    public void actor1(I_Result r) {
        // 读屏障
        // ready 是 volatile 读取值带读屏障
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

如何保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

    java 复制代码
     public void actor2(I_Result r) {
         num = 2;
         ready = true; // ready 是 volatile 赋值带写屏障
         // 写屏障
     }
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

    java 复制代码
    public void actor1(I_Result r) {
        // 读屏障
        // ready 是 volatile 读取值带读屏障
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序

happens ------ before 规则

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下

happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

    java 复制代码
    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 变量的写,对接下来其它线程对该变量的读可见

    java 复制代码
    volatile static int x;
    new Thread(() -> {
        x = 10;
    }, "t1").start();
    new Thread(() -> {
        System.out.println(x);
    }, "t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见

    java 复制代码
    static int x;
    x = 10;
    new Thread(()->{
        System.out.println(x);
    },"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)

    java 复制代码
    static int x;
    Thread t1 = new Thread(()->{
        x = 10;
    },"t1");
    t1.start();
    t1.join();
    System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)

    java 复制代码
    static int x;
    public static void main(String[] args) {
        Thread t2 = new Thread(()->{
            while(true) {
                if(Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        },"t2");
        t2.start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            x = 10;
            t2.interrupt();
        },"t1").start;
        while(!t2.isInterrupted()) {
            Thread.yield();
        }
        System.out.println(x);
    }
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子

    java 复制代码
    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();

共享模型------无锁

问题提出

  • 有如下需求,保证 account.withdraw 取款方法的线程安全

    java 复制代码
    package com.xiaowu.test;
    
    import java.util.ArrayList;
    import java.util.List;
    
    interface Account {
        // 获取余额
        Integer getBalance();
    
        // 取款
        void withdraw(Integer amount);
    
        /**
         * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
         * 如果初始余额为 10000 那么正确的结果应当是 0
         */
        static void demo(Account account) {
            List<Thread> ts = new ArrayList<>();
            long start = System.nanoTime();
            for (int i = 0; i < 1000; i++) {
                ts.add(new Thread(() -> {
                    account.withdraw(10);
                }));
            }
            ts.forEach(Thread::start);
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            long end = System.nanoTime();
            System.out.println(account.getBalance()
                    + " cost: " + (end - start) / 1000_000 + " ms");
        }
    }
  • 原有实现并不是线程安全的

    java 复制代码
    class AccountUnsafe implements Account {
    
        // 余额
        private Integer balance;
    
        public AccountUnsafe(Integer balance) {
            this.balance = balance;
        }
    
        @Override
        public Integer getBalance() {
            return this.balance;
        }
    
        @Override
        public void withdraw(Integer amount) {
            this.balance -= amount;
        }
    }

    执行测试代码

    java 复制代码
    public static void main(String[] args) {
        Account account = new AccountUnsafe(10000);
        Account.demo(account);
    }

    运行结果

  • 加锁实现

    java 复制代码
    class AccountSafeLock implements Account {
    
        // 余额
        private Integer balance;
    
        public AccountSafeLock(Integer balance) {
            this.balance = balance;
        }
    
        @Override
        public Integer getBalance() {
            synchronized (this){
                return this.balance;
            }
        }
    
        @Override
        public void withdraw(Integer amount) {
            synchronized (this){
                this.balance -= amount;
            }
        }
    }
  • 无锁实现

    java 复制代码
    class AccountCas implements Account {
    
        // 余额
        private AtomicInteger balance;
    
        public AccountCas(int balance) {
            this.balance = new AtomicInteger(balance);
        }
    
        @Override
        public Integer getBalance() {
            return this.balance.get();
        }
    
        @Override
        public void withdraw(Integer amount) {
            while (true) {
                // 获取余额的最新值
                int prev = balance.get();
                // 修改后的余额
                int next = prev - amount;
                // 真正修改
                if (balance.compareAndSet(prev, next)) {
                    break;
                }
            }
        }
    }

CAS 与 volatile

CAS

注意:

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

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

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

java 复制代码
@Override
public void withdraw(Integer amount) {
    // 需要不断尝试,直到成功为止
    while (true) {
        // 比如拿到了旧值 1000
        int prev = balance.get();
        // 在这个基础上 1000-10 = 990
        int next = prev - amount;
         /*
         compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
         - 不一致了,next 作废,返回 false 表示失败
         比如,别的线程已经做了减法,当前值已经被减成了 990
         那么本线程的这次 990 就作废了,进入 while 下次循环重试
         - 一致,以 next 设置为新值,返回 true 表示成功
         */
        if (balance.compareAndSet(prev, next)) {
            break;
        }
    }
}

其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。

volatile

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

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

注意

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

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

为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
  • 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

CAS的特点

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

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

原子整数

JUC 并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

AtomicInteger 为例

java 复制代码
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

原子引用

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference

AtomicReference

java 复制代码
@Slf4j(topic = "c.Test30")
public class Test30 {
    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
    }
}

class DecimalAccountCas implements DecimalAccount {
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

interface DecimalAccount {
    // 获取余额
    BigDecimal getBalance();

    // 取款
    void withdraw(BigDecimal amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(DecimalAccount account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(BigDecimal.TEN);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(account.getBalance());
    }
}

ABA 问题

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况。

java 复制代码
@Slf4j(topic = "c.Test31")
public class Test31 {
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        other();
        TimeUnit.SECONDS.sleep(2);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }

}

AtomicStampedReference

如果主线程希望: 只要有其它线程 动过了 共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号

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

java 复制代码
@Slf4j(topic = "c.Test32")
public class Test32 {
    // 保护的变量,和版本号
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        TimeUnit.SECONDS.sleep(2);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }

}

AtomicMarkableReference

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

java 复制代码
@Slf4j(topic = "c.Test33")
public class Test33 {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        // 参数2 mark 可以看作一个标记,表示垃圾袋满了
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
        log.debug("主线程 start...");
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());
        new Thread(() -> {
            log.debug("打扫卫生的线程 start...");
            bag.setDesc("空垃圾袋");
            while (!ref.compareAndSet(bag, bag, true, false)) {
            }
            log.debug(bag.toString());
        }).start();
        TimeUnit.SECONDS.sleep(1);
        log.debug("主线程想换一只新垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("换了么?" + success);
        log.debug(ref.getReference().toString());
    }

}

class GarbageBag {
    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return super.toString() + " " + desc;
    }
}

原子数组

java 复制代码
public class Test34 {
    public static void main(String[] args) {
        // 不安全的数组
        demo(
                () -> new int[10],
                (array) -> array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array))
        );
        // 原子数组
        demo(
                () -> new AtomicIntegerArray(10),
                (array) -> array.length(),
                (array, index) ->array.getAndIncrement(index),
                arrays-> System.out.println(arrays)
        );
    }

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数3,自增方法,回传 array, index
     * 参数4,打印数组的方法
     */
    // supplier 提供者 无中生有 ()->结果
    // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer) {
        List<Thread> ts = new ArrayList<>();
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {
            // 每个线程对数组作 10000 次操作
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }
        ts.forEach(t -> t.start()); // 启动所有线程
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // 等所有线程结束
        printConsumer.accept(array);
    }
}

字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常: Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

java 复制代码
@Slf4j(topic = "c.Test35")
public class Test35 {
    public static void main(String[] args) {
        Student student = new Student();
        AtomicReferenceFieldUpdater updater =
                // 参数一:保存字段的对象的类    参数二:该字段的类别      参数三:需要更新的字段名
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        updater.compareAndSet(student, null, "张三");
        System.out.println(student);
    }
}

class Student {
    volatile String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

原子累加器(LongAdder)

比较 AtomicLong 与 LongAdder

java 复制代码
public class Test36 {
    public static void main(String[] args) {
        demo(
                () -> new AtomicLong(0),
                (adder) -> adder.getAndIncrement()
        );

        demo(
                () -> new LongAdder(),
                (adder) -> adder.increment()
        );
    }

    /*
            () -> 结果    提供累加器对象
            (参数) ->     执行累加操作
     */
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();
        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start) / 1000_000);
    }
}

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

LongAdder 原理

LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧

LongAdder 类有几个关键域

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

    java 复制代码
    // 不要用于实践!!!
    public class LockCas {
    	// 0 没加锁
    	// 1 加锁
        private AtomicInteger state = new AtomicInteger(0);
        public void lock() {
            while (true) {
                if (state.compareAndSet(0, 1)) {
                    break;
                }
            }
        }
        public void unlock() {
            log.debug("unlock...");
            state.set(0);
        }
    }
  • 缓存行伪共享

    其中 Cell 即为累加单元

    java 复制代码
    // 防止缓存行伪共享
    @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, valueOffset, prev, next);
        }
        // 省略不重要代码
    }

    得从缓存说起:缓存与内存的速度比较

    从 cpu 到 大约需要的时钟周期
    寄存器 1 cycle (4GHz 的 CPU 约为0.25ns)
    L1 3~4 cycle
    L2 10~20 cycle
    L3 40~45 cycle
    内存 120~240 cycle
    • 因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
    • 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
    • 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
    • CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

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

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

    无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

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

Unsafe

  • 概述

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

    java 复制代码
    public class UnsafeAccessor {
        static Unsafe unsafe;
        static {
            try {
                Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
                theUnsafe.setAccessible(true);
                unsafe = (Unsafe) theUnsafe.get(null);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new Error(e);
            }
        }
        static Unsafe getUnsafe() {
            return unsafe;
        }
    }
  • 获取unsafe对象

    java 复制代码
    /**
     * 此代码段演示了如何通过反射机制访问Java中的Unsafe类的实例。
     * Unsafe类是一个特殊的类,它提供了直接访问操作系统和JVM内存的权限,
     * 以及执行一些本不应该在普通Java代码中执行的操作,如直接内存操作和原子变量操作等。
     * 由于其危险性和潜在的不安全性,通常不推荐在生产环境中使用。
     */
    // 获取Unsafe类的"theUnsafe"字段
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    // 允许访问私有字段
    theUnsafe.setAccessible(true);
    // 获取Unsafe实例
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);
    // 打印Unsafe实例
    System.out.println(unsafe);
  • Unsafe CAS 操作

    java 复制代码
    public class TestUnsafe {
        public static void main(String[] args) throws Exception {
            /**
             * 此代码段演示了如何通过反射机制访问Java中的Unsafe类的实例。
             * Unsafe类是一个特殊的类,它提供了直接访问操作系统和JVM内存的权限,
             * 以及执行一些本不应该在普通Java代码中执行的操作,如直接内存操作和原子变量操作等。
             * 由于其危险性和潜在的不安全性,通常不推荐在生产环境中使用。
             */
            // 获取Unsafe类的"theUnsafe"字段
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            // 允许访问私有字段
            theUnsafe.setAccessible(true);
            // 获取Unsafe实例
            Unsafe unsafe = (Unsafe) theUnsafe.get(null);
            // 打印Unsafe实例
            System.out.println(unsafe);
    
            /**
             * 使用Unsafe类进行CAS操作的示例代码。
             * 本段代码主要演示了如何通过Unsafe类直接操作对象的字段,实现了原子性的更新操作。
             *
             * 首先,获取指定对象字段的偏移地址,然后对该字段进行CAS(Compare and Swap)操作,
             * 保证了操作的原子性和线程安全性。
             */
            // 1. 获取域的偏移地址
            long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
            long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
    
            Teacher t = new Teacher();
            // 2. 执行 CAS 操作
            // 对"id"字段执行CAS操作,将初始值0替换为1
            unsafe.compareAndSwapInt(t, idOffset, 0, 1);
            // 对"name"字段执行CAS操作,将初始值null替换为"小吴在敲Bug"
            unsafe.compareAndSwapObject(t, nameOffset, null, "小吴在敲Bug");
    
            // 3.验证
            System.out.println(t);
        }
    }
    
    @Data
    class Teacher {
        volatile int id;
        volatile String name;
    }
  • unsafe 对象------模拟实现原子整数

    使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现

    java 复制代码
    /**
     * 自定义的原子整型类,提供原子性操作功能。
     */
    class MyAtomicInteger {
        // 原子整型的当前值
        private volatile int value;
        // value字段在对象内存中的偏移量,用于Unsafe类的原子操作
        private static final long valueOffset;
        // Unsafe类的实例,提供底层内存访问和原子操作
        private static final Unsafe UNSAFE;
    	public MyAtomicInteger(int value) {
    		this.value = value;
    	}
        // 静态初始化块,用于初始化Unsafe实例和valueOffset
        static {
            try {
                Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
                // 设置为可访问以获取Unsafe实例
                theUnsafe.setAccessible(true);
                // 获取Unsafe实例
                UNSAFE = (Unsafe) theUnsafe.get(null);
                // 计算value字段的内存偏移量
                valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
            } catch (Exception e) {
                // 初始化失败抛出Error
                throw new Error(e);
            }
        }
    
        /**
         * 获取当前原子整型的值。
         *
         * @return 当前值
         */
        public int getValue() {
            return value;
        }
    
        /**
         * 将当前原子整型的值减去指定数量。
         *
         * @param amount 要减去的数量
         */
        public void decrement(int amount) {
            // 循环直到更新成功
            while (true) {
                // 读取当前值
                int prev = this.value;
                // 计算新值
                int next = prev - amount;
                // 使用CAS操作尝试更新值,如果失败则重试
                if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)) {
                    // 更新成功,退出循环
                    break;
                }
            }
        }
    }

共享模型------不可变

日期转换的问题

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

    java 复制代码
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try {
                log.debug("{}", sdf.parse("1951-04-21"));
            } catch (Exception e) {
                log.error("{}", e);
            }
        }).start();
    }
  • 有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果

  • 使用同步锁解决问题

    这样虽能解决问题,但带来的是性能上的损失,并不算很好:

    java 复制代码
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            synchronized (sdf) {
                try {
                    log.debug("{}", sdf.parse("1951-04-21"));
                } catch (Exception e) {
                    log.error("{}", e);
                }
            }
        }).start();
    }
  • 不可变对象解决问题

    java 复制代码
    // 不可变对象
    DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            TemporalAccessor parse = stf.parse("1951-04-21");
            log.debug("{}",parse);
        }).start();
    }

不可变设计

另一个更为熟悉的 String 类也是不可变的,以它为例,说明下不可变设计的要素

java 复制代码
public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    // ...
}

final 的使用

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

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

保护性拷贝

  • 使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:

    java 复制代码
    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
  • 发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出了修改:

    java 复制代码
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

    结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】

无状态

在 web 阶段,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的

因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】

共享模型------工具

线程池概念

什么是线程池

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的ThreadFactory创建一个新线程。


为什么使用线程池

使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力。


线程池的优点:

  • 资源复用与效率提升: 线程池通过复用已创建的线程,避免了频繁地创建和销毁线程的开销。这种复用机制极大地提高了系统的资源利用率,减少了不必要的线程管理成本,从而提升了整体运行效率。
  • 系统稳定性增强: 通过限制线程数量,线程池能够防止系统因线程过多而导致的资源耗尽问题。这种控制机制确保了系统的稳定性和可靠性,降低了因线程管理不当而引发的性能下降或崩溃的风险。
  • 统一的任务管理和调度: 线程池提供了一个统一的接口来管理和调度任务,使得开发者能够更加方便地控制任务的执行顺序、优先级以及并发度。这种统一的管理方式简化了任务调度的复杂性,提高了系统的可维护性和可扩展性。
  • 响应速度提升: 由于线程池中的线程是预先创建的,因此当新任务到达时,可以立即从线程池中获取线程来执行任务,无需等待线程的创建过程。这种即时响应机制缩短了任务的等待时间,提升了系统的响应速度,特别是对于需要快速响应的场景非常有利。
  • 任务队列缓冲: 线程池通常配备任务队列,用于缓存待执行的任务。当线程池中的线程都在忙碌时,新任务可以在队列中等待,确保所有任务都能得到处理。这种缓冲机制避免了任务丢失或阻塞的问题,提高了系统的容错性和可靠性。
  • 配置灵活性和可扩展性: 线程池提供了丰富的配置选项,可以根据实际应用场景调整线程数量、任务队列大小等参数。这使得线程池能够灵活地适应不同的工作负载和性能需求,提供了良好的可扩展性。

使用线程池的缺点:

  • 线程数量限制: 线程池通过限制线程数量来避免资源耗尽。然而,这种限制可能导致一些问题。当任务数量远超过线程池大小时,任务可能会在队列中等待执行,如果队列容量也有限,则可能导致任务被拒绝执行。这在高并发或任务处理时间较长的场景中尤为明显,可能会引发性能瓶颈或任务延迟。
  • 资源竞争与同步问题: 线程池中的线程共享相同的资源,这可能导致资源竞争和同步问题。例如,多个线程可能同时访问和修改共享数据,如果没有适当的同步机制,可能导致数据不一致或其他并发问题。解决这些问题通常需要额外的同步代码或锁机制,增加了编程的复杂性和出错的可能性。
  • 任务优先级与调度问题 :线程池通常按照任务到达的顺序或某种默认的调度策略来执行任务。然而,在某些情况下,我们可能需要根据任务的优先级或其他因素来动态调整任务的执行顺序。线程池可能不直接支持这种复杂的调度需求,需要开发者自行实现额外的调度逻辑。
  • 配置与管理复杂性: 线程池的配置和管理可能相对复杂。开发者需要根据应用程序的特性和需求来选择合适的线程池大小、队列容量、拒绝策略等参数。不当的配置可能导致性能下降或资源浪费。此外,线程池的监控和调优也是一个挑战,需要开发者具备相应的经验和技能。
  • 不支持所有类型的任务: 线程池适用于执行大量短生命周期的任务,但对于长生命周期的任务或需要特殊处理的任务,线程池可能不是最佳选择。例如,如果任务需要长时间运行或涉及复杂的资源管理,使用线程池可能会导致资源浪费或管理困难。

自定义线程池

  • 自定义拒绝策略接口

    java 复制代码
    @FunctionalInterface // 拒绝策略
    interface  RejectPolicy<T>{
        void reject(BlockingQueue<T> queue,T task);
    }
  • 自定义任务队列

    java 复制代码
    /**
     * 自定义任务队列
     *
     * @param <T>
     */
    @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 capacity;
    
        public BlockingQueue(int capacity) {
            this.capacity = capacity;
        }
    
        // 阻塞获取
        public T take() {
            lock.lock();
            try {
                while (queue.isEmpty()) {
                    try {
                        emptyWaitSet.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal();
                return t;
            } finally {
                lock.unlock();
            }
        }
    
        // 带超时的阻塞获取
        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 = emptyWaitSet.awaitNanos(nanos);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal();
                return t;
            } finally {
                lock.unlock();
            }
        }
    
        // 阻塞添加
        public void put(T task) {
            lock.lock();
            try {
                while (queue.size() == capacity) {
                    try {
                        log.debug("等待加入任务队列 {}", task);
                        fullWaitSet.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                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() == capacity) {
                    try {
                        log.debug("等待加入任务队列 {}", task);
                        if (nanos <= 0) {
                            return false;
                        }
                        nanos = fullWaitSet.awaitNanos(nanos);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                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() == capacity) {
                    // 将权利下放
                    rejectPolicy.reject(this, task);
                } else {
                    // 有空闲
                    log.debug("加入任务队列 {}", task);
                    queue.addLast(task);
                    emptyWaitSet.signal();
                }
            } finally {
                lock.unlock();
            }
        }
    }
  • 自定义线程池

    java 复制代码
    /**
     * 自定义线程池
     */
    @Slf4j(topic = "c.ThreadPool")
    class ThreadPool {
        // 任务队列
        private BlockingQueue<Runnable> taskQueue;
    
        // 线程集合
        private Set<Worker> workers = new HashSet<>();
    
        // 核心线程数
        private int coreSize;
    
        // 获取任务的超时时间
        private long timeout;
        private TimeUnit timeUnit;
    
        private RejectPolicy<Runnable> rejectPolicy;
    
        public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapacity, RejectPolicy<Runnable> rejectPolicy) {
            this.coreSize = coreSize;
            this.timeout = timeout;
            this.timeUnit = timeUnit;
            taskQueue = new BlockingQueue<>(queueCapacity);
            this.rejectPolicy = 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);
                    worker.start();
                    workers.add(worker);
                } else {
                    // 加入任务队列
                    //taskQueue.put(task);
                    // 1.死等
                    // 2.带超时等待
                    // 3.让调用者放弃任务的执行
                    // 4.让调用者抛出异常
                    // 5.让调用者自己执行任务
                    taskQueue.tryPut(rejectPolicy, task);
                }
            }
        }
    
        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.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);
                }
            }
        }
    
    }

ThreadPoolExecutor

线程池状态

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

状态名 高 3 位 接收新任务 处理阻塞队列任务 说明
RUNNING 111 Y Y
SHUTDOWN 000 N Y 不会接收新任务,但会处理阻塞队列剩余任务
STOP 001 N N 会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING 010 --- --- 任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED 011 --- --- 终结状态

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

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

java 复制代码
// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

构造方法

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略

工作方式:

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
  • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。
  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
  • 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
    • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
    • Netty 的实现,是创建一个新线程来执行任务
    • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
    • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。

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

Executors

固定大小线程池

newFixedThreadPool

java 复制代码
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

特点:

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务

评价: 适用于任务量已知,相对耗时的任务

带缓冲线程池

newCachedThreadPool

java 复制代码
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

java 复制代码
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}

使用场景: 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

提交任务

java 复制代码
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                              long timeout, TimeUnit unit)
        throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
                long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

关闭线程池

shutdown

java 复制代码
/*
线程池状态变为 SHUTDOWN
 - 不会接收新任务
 - 但已提交任务会执行完
 - 此方法不会阻塞调用线程的执行
*/
void shutdown();
java 复制代码
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 修改线程池状态
        advanceRunState(SHUTDOWN);
        // 仅会打断空闲线程
        interruptIdleWorkers();
        onShutdown(); // 扩展点 ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)
    tryTerminate();
}

shutdownNow

java 复制代码
/*
线程池状态变为 STOP
 - 不会接收新任务
 - 会将队列中的任务返回
 - 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();
java 复制代码
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 修改线程池状态
        advanceRunState(STOP);
        // 打断所有线程
        interruptWorkers();
        // 获取队列中剩余任务
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    // 尝试终结
    tryTerminate();
    return tasks;
}

其它方法

java 复制代码
// 不在 RUNNING 状态的线程池,此方法就返回 true
boolean isShutdown();
// 线程池状态是否是 TERMINATED
boolean isTerminated();
// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

创建多少线程池合适

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存

CPU 密集型运算

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


I/O 密集型运算

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

  • 经验公式如下:
    线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

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

  • 例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
    4 * 100% * 100% / 10% = 40

任务调度线程池

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

java 复制代码
@Slf4j(topic = "c.TestTimer")
public class TestTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 1");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 2");
            }
        };
        // 使用 timer 添加两个任务,希望它们都在 1s 后执行
        // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
        timer.schedule(task1, 1000);
        timer.schedule(task2, 1000);
    }
}

运行结果


延时执行

使用 ScheduledExecutorService 改写:

java 复制代码
@Slf4j(topic = "c.TestTimer")
public class TestTimer {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
		// 添加两个任务,希望它们都在 1s 后执行
        pool.schedule(()->{
            log.debug("task1");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },1,TimeUnit.SECONDS);
        pool.schedule(()->{
            log.debug("task1");

        },1,TimeUnit.SECONDS);
    }
}

运行结果

定时执行

scheduleAtFixedRate

每隔一段时间执行一次

  • 参数一:要执行的任务对象
  • 参数二:连续执行之间的时间间隔
  • 参数三:时间单位

注意: 如果任务的执行时间大于间隔时间必须要等上一个任务执行完成

java 复制代码
@Slf4j(topic = "c.TestTimer")
public class TestTimer {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
        log.debug("start......");
        pool.scheduleAtFixedRate(()->{
            log.debug("running......");
        },1,1,TimeUnit.SECONDS);
    }
}

scheduleWithFixedDelay

每隔一段时间执行一次

  • 参数一:要执行的任务对象
  • 参数二:连续执行之间的时间间隔
  • 参数三:时间单位

注意: 要等上一个任务执行完成才开始算时间间隔

正确处理执行任务异常

  • 方法1:主动捉异常

    java 复制代码
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
    log.debug("start......");
    pool.schedule(()->{
    	// 捕获异常
        try {
            log.debug("running......");
            int i = 1 / 0;
        }catch (Exception e){
            e.printStackTrace();
        }
    },1,TimeUnit.SECONDS);
  • 方法2:使用 Future

    java 复制代码
    ExecutorService pool = Executors.newFixedThreadPool(1);
    Future<Boolean> f = pool.submit(() -> {
     log.debug("task1");
     int i = 1 / 0;
     return true;
    });
    log.debug("result:{}", f.get());

Tomcat 线程池

Tomcat 在哪里用到了线程池

  • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore
  • Acceptor 只负责【接收新的 socket 连接】
  • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
  • Executor 线程池中的工作线程最终负责【处理请求】

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

  • 如果总线程数达到 maximumPoolSize
    • 这时不会立刻抛 RejectedExecutionException 异常
    • 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常

Tomcat 线程池配置
Connector 配置

配置项 默认值 说明
acceptorThreadCount 1 acceptor 线程数量
pollerThreadcount 1 poller 线程数量
minSpareThreads 10 核心线程数,即 corePollSize
maxThreads 200 最大线程数,即 maximumPoolSize
executor --- Executor 名称,用来引用下面的 Executor

Executor 线程配置

配置项 默认值 说明
threadPriority 5 线程优先级
daemon true 是否守护线程
minSpareThreads 25 核心线程数,即 corePoolSize
maxThreads 200 最大线程数,即 maximumPoolSize
maxIdleTime 60000 线程生存时间,单位是毫秒,默认值即 1 分钟
maxQueueSize Integer.MAX_VALUE 队列长度
prestartminSpareThreads false 核心线程是否在服务器启动时启动

Fork/Join

概念

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

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

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

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

使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务

java 复制代码
// 1 ~ n 之间整数的和
@Slf4j(topic = "c.MyTask")
class MyTask extends RecursiveTask<Integer> {

    private int n;

    public MyTask(int n) {
        this.n = n;
    }

    @Override
    public String toString() {
        return "{" + n + '}';
    }


    @Override
    protected Integer compute() {
        // 如果 n 已经为 1,可以求得结果了
        if (n == 1) {
            log.debug("join() {}", n);
            return n;
        }

        // 将任务进行拆分(fork)
        MyTask t = new MyTask(n - 1);
        t.fork();
        log.debug("fork() {} + {}", n, t);

        // 合并(join)结果
        int result = n + t.join();
        log.debug("join() {} + {} = {}", n, t, result);
        return result;

    }
}

然后提交给 ForkJoinPool 来执行

java 复制代码
public static void main(String[] args) {
	ForkJoinPool pool = new ForkJoinPool(4);
	System.out.println(pool.invoke(new MyTask(5)));
}

结果

用图来表示

改进

java 复制代码
@Slf4j(topic = "c.MyTask2")
class MyTask2 extends RecursiveTask<Integer> {

    int begin;
    int end;
    public MyTask2(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }
    @Override
    public String toString() {
        return "{" + begin + "," + end + '}';
    }
    @Override
    protected Integer compute() {
        // 5, 5
        if (begin == end) {
            log.debug("join() {}", begin);
            return begin;
        }
        // 4, 5
        if (end - begin == 1) {
            log.debug("join() {} + {} = {}", begin, end, end + begin);
            return end + begin;
        }

        // 1 5
        int mid = (end + begin) / 2; // 3
        MyTask2 t1 = new MyTask2(begin, mid); // 1,3
        t1.fork();
        MyTask2 t2 = new MyTask2(mid + 1, end); // 4,5
        t2.fork();
        log.debug("fork() {} + {} = ?", t1, t2);
        int result = t1.join() + t2.join();
        log.debug("join() {} + {} = {}", t1, t2, result);
        return result;
    }
}

然后提交给 ForkJoinPool 来执行

java 复制代码
public static void main(String[] args) {
    ForkJoinPool pool = new ForkJoinPool(4);
    System.out.println(pool.invoke(new MyTask2(1,5)));
}

结果

用图来表示

J.U.C

AQS(抽象队列同步器)原理

概述

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

特点:

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

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

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

获取锁的姿势

java 复制代码
// 如果获取锁失败
if (!tryAcquire(arg)) {
	 // 入队, 可以选择阻塞当前线程 park unpark
}

释放锁的姿势

java 复制代码
// 如果释放锁成功
if (tryRelease(arg)) {
	// 让阻塞线程恢复运行
}

实现不可重入锁

  • 自定义锁

    java 复制代码
    // 自定义锁(不可重入锁)
    class MyLock implements Lock {
    
        // 独占锁  同步器类
        class MySync extends AbstractQueuedSynchronizer {
            // 尝试获得锁
            @Override
            protected boolean tryAcquire(int arg) {
                if (compareAndSetState(0, 1)) {
                    // 加上了锁,并设置 owner 为当前线程
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            // 尝试释放锁
            @Override
            protected boolean tryRelease(int arg) {
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
    
            // 是否持有独占锁
            @Override
            protected boolean isHeldExclusively() {
                return getState() == 1;
            }
    
            public Condition newCondition() {
                return new ConditionObject();
            }
        }
    
        private MySync sync = new MySync();
    
        // 加锁(不成功会进入等待队列)
        @Override
        public void lock() {
            sync.acquire(1);
        }
    
        // 加锁,可打断
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
    
        // 尝试加锁(一次)
        @Override
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
    
        // 尝试加锁,带超时
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireSharedNanos(1,unit.toNanos(time));
        }
    
        // 解锁
        @Override
        public void unlock() {
            sync.release(1)
        }
    
        // 创建条件变量
        @Override
        public Condition newCondition() {
            return sync.newCondition();
        }
    }
  • 测试

    java 复制代码
    @Slf4j(topic = "c.TestAqs")
    public class TestAqs {
        public static void main(String[] args) {
            MyLock lock = new MyLock();
            new Thread(()->{
                lock.lock();
                try {
                    log.debug("locking......");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    log.debug("unlocking......");
                    lock.unlock();
                }
            },"t1").start();
    
            new Thread(()->{
                lock.lock();
                try {
                    log.debug("locking......");
                }finally {
                    log.debug("unlocking......");
                    lock.unlock();
                }
            },"t2").start();
        }
    }
  • 结果

ReentrantLock 原理

非公平锁实现原理

加锁解锁流程:先从构造器开始看,默认为非公平锁实现

java 复制代码
public ReentrantLock() {
	sync = new NonfairSync();
}

NonfairSync 继承自 AQS

  • 没有竞争时

  • 第一个竞争出现时

  • Thread-1 执行了

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

    • acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
    • 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
    • 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
    • shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时
      state 仍为 1,失败
    • 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回
      true
    • 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
  • 再次有多个线程经历上述过程竞争失败,变成这个样子

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

    • 设置 exclusiveOwnerThread 为 null
    • state = 0
  • 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程

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

  • 回到 Thread-1 的 acquireQueued 流程

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

    • exclusiveOwnerThread 为 Thread-1,state = 1
    • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
    • 原本的 head 因为从链表断开,而可被垃圾回收
  • 如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了

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

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

锁重入原理

java 复制代码
static final class NonfairSync extends Sync {
    // ...
    // Sync 继承过来的方法, 方便阅读, 放在此处
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
        else if (current == getExclusiveOwnerThread()) {
            // state++
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    // Sync 继承过来的方法, 方便阅读, 放在此处
    protected final boolean tryRelease(int releases) {
        // state--
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 支持锁重入, 只有 state 减为 0, 才释放成功
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
}

可打断原理

  • 不可打断模式: 在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

    java 复制代码
    // Sync 继承自 AQS
    static final class NonfairSync extends Sync {
        // ...
    
        private final boolean parkAndCheckInterrupt() {
            // 如果打断标记已经是 true, 则 park 会失效
            LockSupport.park(this);
            // interrupted 会清除打断标记
            return Thread.interrupted();
        }
    
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (; ; ) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null;
                        failed = false;
                        // 还是需要获得锁后, 才能返回打断状态
                        return interrupted;
                    }
                    if (
                            shouldParkAfterFailedAcquire(p, node) &&
                                    parkAndCheckInterrupt()
                    ) {
                        // 如果是因为 interrupt 被唤醒, 返回打断状态为 true
                        interrupted = true;
                    }
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    
        public final void acquire(int arg) {
            if (
                    !tryAcquire(arg) &&
                            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
            ) {
                // 如果打断状态为 true
                selfInterrupt();
            }
        }
    
        static void selfInterrupt() {
            // 重新产生一次中断
            Thread.currentThread().interrupt();
        }
    }
  • 可打断模式

    java 复制代码
    static final class NonfairSync extends Sync {
        public final void acquireInterruptibly(int arg) throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 如果没有获得到锁, 进入 ㈠
            if (!tryAcquire(arg))
                doAcquireInterruptibly(arg);
        }
    
        // ㈠ 可打断的获取锁流程
        private void doAcquireInterruptibly(int arg) throws InterruptedException {
            final Node node = addWaiter(Node.EXCLUSIVE);
            boolean failed = true;
            try {
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                            parkAndCheckInterrupt()) {
                        // 在 park 过程中如果被 interrupt 会进入此
                        // 这时候抛出异常, 而不会再次进入 for (;;)
                        throw new InterruptedException();
                    }
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    }

公平锁实现原理

java 复制代码
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    // AQS 继承过来的方法, 方便阅读, 放在此处
    public final void acquire(int arg) {
        if (
                !tryAcquire(arg) &&
                        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        ) {
            selfInterrupt();
        }
    }

    // 与非公平锁主要区别在于 tryAcquire 方法的实现
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
            if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        // h != t 时表示队列中有 Node
        return h != t &&
                (
                        // (s = h.next) == null 表示队列中还有没有老二
                        (s = h.next) == null ||
                                // 或者队列中老二线程不是此线程
                                s.thread != Thread.currentThread()
                );
    }
}

条件变量实现原理

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

await 流程

开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程

创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

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

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

park 阻塞 Thread-0

signal 流程

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

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

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

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

ReentrantLock 使用

读写锁

ReentrantReadWriteLock

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

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

    java 复制代码
    @Slf4j(topic = "c.DataContainer")
    class DataContainer{
        // 要操作的数据
        private Object data;
        // 读写锁对象
        private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        // 获取读锁
        private ReentrantReadWriteLock.ReadLock r = rw.readLock();
        // 获取写锁
        private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
    
        public Object read(){
            log.debug("获取读锁......");
            r.lock();
            try {
                log.debug("读取");
                TimeUnit.SECONDS.sleep(2);
                return data;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                log.debug("释放读锁......");
                r.unlock();
            }
        }
    
        public void write(){
            log.debug("获取写锁......");
            w.lock();
            try {
                log.debug("写入");
            }finally {
                log.debug("释放写锁......");
                w.unlock();
            }
        }
    }

注意事项

  • 读锁不支持条件变量

  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

    java 复制代码
    r.lock();
    try {
        // ...
        w.lock();
        try {
            // ...
        } finally{
            w.unlock();
        }
    } finally{
        r.unlock();
    }
  • 重入时降级支持:即持有写锁的情况下去获取读锁

读写锁原理

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

t1 w.lock,t2 r.lock

  • t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位

  • t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败

    tryAcquireShared 返回值表示

    • -1 表示失败
    • 0 表示成功,但后继节点不会继续唤醒
    • 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
  • 这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态

  • t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁

  • 如果没有成功,在 doAcquireShared 内 for ( ; ; ) 循环一次,把前驱节点的 waitStatus 改为 -1,再 for ( ; ; ) 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park


t3 r.lock,t4 w.lock

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

t1 w.unlock

  • 这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
  • 接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
  • 这回再来一次 for ( ; ; ) 执行 tryAcquireShared 成功则让读锁计数加一
  • 这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
  • 事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
  • 这回再来一次 for ( ; ; ) 执行 tryAcquireShared 成功则让读锁计数加一
  • 这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
  • 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

t2 r.unlock,t3 r.unlock

  • t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
  • t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
  • 之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for ( ; ; ) 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合 戳 使用

  • 加解读锁

    java 复制代码
    long stamp = lock.readLock();
    lock.unlockRead(stamp);
  • 加解写锁

    java 复制代码
    long stamp = lock.writeLock();
    lock.unlockWrite(stamp);
  • 乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

    java 复制代码
    long stamp = lock.tryOptimisticRead();
    // 验戳
    if(!lock.validate(stamp)){
    	// 锁升级
    }

stampedlock演示

注意:

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入

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

java 复制代码
@Slf4j(topic = "c.DataContainerStamped")
class DataContainerStamped{
    private int data;
    private final StampedLock lock = new StampedLock();

    public DataContainerStamped(int data){
        this.data = data;
    }

    public int read (int readTime) throws InterruptedException {
        long stamp = lock.tryOptimisticRead();
        log.debug("optimistic read locking...{}", stamp);
        TimeUnit.SECONDS.sleep(readTime);
        if (lock.validate(stamp)) {
            log.debug("read finish...{}, data:{}", stamp, data);
            return data;
        }
        // 锁升级 - 读锁
        log.debug("updating to read lock... {}", stamp);
        try {
            stamp = lock.readLock();
            log.debug("read lock {}", stamp);
            TimeUnit.SECONDS.sleep(readTime);
            log.debug("read finish...{}, data:{}", stamp, data);
            return data;
        } finally {
            log.debug("read unlock {}", stamp);
            lock.unlockRead(stamp);
        }

    }

    public void write(int newData){
        long stamp = lock.writeLock();
        log.debug("write lock {}",stamp);
        try{
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            log.debug("write unlock {}",stamp);
            lock.unlockWrite(stamp);
        }
    }
}

Semaphore(信号量)

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

java 复制代码
public static void main(String[] args) {
    // 1. 创建 semaphore 对象
    Semaphore semaphore = new Semaphore(3);
    // 2. 10个线程同时运行
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            // 3. 获取许可
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.debug("running...");
                TimeUnit.SECONDS.sleep(1);
                log.debug("end...");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                // 4. 释放许可
                semaphore.release();
            }
        }).start();
    }
}

原理

  • 加锁解锁流程

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

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

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

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

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

CountdownLatch(倒计时锁)

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

  • 不能够重用

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

java 复制代码
CountDownLatch latch = new CountDownLatch(3);
new Thread(()->{
    log.debug("begin");
    try {
        TimeUnit.SECONDS.sleep(1);
        latch.countDown();
        log.debug("end......");
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
},"t1").start();
new Thread(()->{
    log.debug("begin");
    try {
        TimeUnit.SECONDS.sleep(3);
        latch.countDown();
        log.debug("end......");
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
},"t2").start();
new Thread(()->{
    log.debug("begin");
    try {
        TimeUnit.SECONDS.sleep(2);
        latch.countDown();
        log.debug("end......");
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
},"t3").start();
log.debug("waiting");
latch.await();
log.debug("wait end......");

运行结果

CyclicBarrier(循环栅栏)

注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为 人满发车

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

java 复制代码
ExecutorService service = Executors.newFixedThreadPool(2);
CyclicBarrier barrier = new CyclicBarrier(2,()->{
    log.debug("task1,task2 finish......");
});
service.submit(()->{
    log.debug("task1 begin......");
    try {
        TimeUnit.SECONDS.sleep(1);
        barrier.await();    // 2-1=1
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
});
service.submit(()->{
    log.debug("task2 begin......");
    try {
        TimeUnit.SECONDS.sleep(2);
        barrier.await(); // 1-1=0
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
});

线程集合安全类

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

  • 遗留的线程安全集合如 HashtableVector
  • 使用 Collections 装饰的线程安全集合,如:
    • Collections.synchronizedCollection
    • 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,不再继续遍历

设计模式

两阶段终止模式(终止模式)

在一个线程T1中如何"优雅"(给T2一个料理后事的机会)终止T2?


错误思路

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

两阶段终止模式分析


利用isInterrupt

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行

java 复制代码
@Slf4j(topic = "c.Test12")
public class Test12 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        TimeUnit.SECONDS.sleep(3);

        tpt.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    private Thread monitor;

    // 启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    // 情况1
                    TimeUnit.SECONDS.sleep(1);
                    // 情况2
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 重新设置打断标记
                    current.interrupt();
                }
            }
        });
        monitor.start();
    }

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

运行结果

利用volatile

java 复制代码
@Slf4j(topic = "c.Test12")
public class Test12 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        TimeUnit.SECONDS.sleep(3);

        tpt.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    private Thread monitor;

    // 使用 volatile 关键字
    private volatile boolean stop;

    // 启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    // 情况1
                    TimeUnit.SECONDS.sleep(1);
                    // 情况2
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        monitor.start();
    }

    // 停止监控线程
    public void stop() {
        stop = true;
        // 为了立刻停止线程,最好也打断该线程
        monitor.interrupt();
    }
}

运行结果:

Balking(犹豫)模式(同步模式)

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

了,直接结束返回

java 复制代码
@Slf4j(topic = "MonitorService")
public class MonitorService {
    // 用来表示是否已经有线程已经在执行启动了
    private volatile boolean starting;

    public void start() {
        log.info("尝试启动监控线程");
        synchronized (this) {
            if(starting){
                return;
            }
            starting=true;
        }
    }
}

固定运行顺序(同步模式)

比如,必须先 2 后 1

wait notify 版

java 复制代码
@Slf4j(topic = "c.Test23")
public class Test23 {
    static final Object lock = new Object();
    // 表示 t2 是否运行过
    static boolean t2runned = false;

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

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.debug("2");
                t2runned = true;
                lock.notifyAll();
            }
        }, "t2");

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

park & unpark 版

java 复制代码
@Slf4j(topic = "c.Test24")
public class Test24 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1");
        }, "t1");
        t1.start();

        new Thread(()->{
            log.debug("2");
            LockSupport.unpark(t1);
        },"t2").start();
    }
}

交替运行顺序(同步模式)

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次,现在要求输出 abcabcabcabcabc 怎么实习

wait notify 版

java 复制代码
@Slf4j(topic = "c.Test25")
public class Test25 {
    public static void main(String[] args) {

        WaitNotify wn = new WaitNotify(1, 5);
        new Thread(()->{
            wn.print("a",1,2);
        },"t1").start();
        new Thread(()->{
            wn.print("b",2,3);
        },"t2").start();
        new Thread(()->{
            wn.print("c",3,1);
        },"t3").start();
    }

}

/*
输出内容    等待标记        下一个标记
    a   ------ 1        ------  2
    b   ------ 2        ------  3
    c   ------ 3        ------  1
 */
class WaitNotify {
    // 等待标记
    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) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}

await & signal 版

java 复制代码
@Slf4j(topic = "c.Test26")
public class Test26 {
    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);
        },"t1").start();

        new Thread(()->{
            awaitSignal.print("b",b,c);
        },"t2").start();

        new Thread(()->{
            awaitSignal.print("c",c,a);
        },"t3").start();

        TimeUnit.SECONDS.sleep(1);
        awaitSignal.lock();
        try {
            log.debug("开始");
            a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock {
    private int loomNumber;

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

    /**
     * @param str     打印内容
     * @param current 进入哪一间休息室
     * @param next    下一间休息室
     */
    public void print(String str, Condition current,Condition next) {
        for (int i = 0; i < loomNumber; i++) {
            lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                unlock();
            }
        }
    }
}  

park & unpark

java 复制代码
@Slf4j(topic = "c.Test27")
public class Test27 {
    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);
        }, "t1");
        t2 = new Thread(() -> {
            pu.print("b", t3);
        }, "t2");
        t3 = new Thread(() -> {
            pu.print("c", t1);
        }, "t3");
        t1.start();
        t2.start();
        t3.start();

        LockSupport.unpark(t1);
    }
}

class ParkUnpark {
    private int loopNumber;

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

    /**
     *
     * @param str       打印内容
     * @param next      下一个线程
     */

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

保护性暂停模式(同步模式)

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式
  • 与join差别在于join需要等待该线程结束,而保护性暂停模式只要得到需要的结果就可以继续往下执行

实现

java 复制代码
@Slf4j(topic = "c.Test17")
public class Test17 {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            // 等待结果
            log.debug("等待结果");
            List<String> list = (List<String>) guardedObject.get();
            log.debug("结果是:{}", list);
        }, "t1").start();

        new Thread(() -> {
            // 等待下载
            log.debug("执行下载");
            // 模拟下载
            try {
                TimeUnit.SECONDS.sleep(new Random().nextInt(10));
                List<String> list = new ArrayList<>(Arrays.asList("xiao", "wu"));
                guardedObject.complete(list);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

class GuardedObject {
    // 结果
    private Object response;

    // 获取结果
    public Object get() {
        synchronized (this) {
            // 没有结果
            while (response == null) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    /**
     * 增加超时效果
     *
     * @param timeout 表示要等待多久
     * @return
     */
    public Object get(long timeout) {
        synchronized (this) {
            // 开始时间
            long begin = System.currentTimeMillis();
            // 经历的时间
            long passedTime = 0;
            // 没有结果
            while (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 response) {
        synchronized (this) {
            // 给结果成员变量赋值
            this.response = response;
            this.notifyAll();
        }

    }
}

运行结果

多任务版 GuardedObject

图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员

如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理


  • 代码如下

    java 复制代码
    @Slf4j(topic = "c.Test17")
    public class Test17 {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 3; i++) {
                new People().start();
            }
            TimeUnit.SECONDS.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 {
        // 信件Id
        private int id;
        // 信件内容
        private String mail;
    
        public Postman(int id, String mail) {
            this.id = id;
            this.mail = mail;
        }
    
        @Override
        public void run() {
            GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
            log.debug("送信 id:{},内容:{}", guardedObject.getId(),mail);
            guardedObject.complete(mail);
        }
    }
    
    /**
     * 信箱
     */
    class Mailboxes {
        private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
    
        private static int id = 1;
    
        // 产生唯一 ID
        public static synchronized int generateId() {
            return id++;
        }
    
        public static GuardedObject getGuardedObject(int id) {
            return boxes.remove(id);
        }
    
        public static GuardedObject createGuardedObject() {
            GuardedObject go = new GuardedObject(generateId());
            boxes.put(go.getId(), go);
            return go;
        }
    
        public static Set<Integer> getIds() {
            return boxes.keySet();
        }
    }
    
    class GuardedObject {
        // 区分不同的GuardedObject对象
        @Getter
        private int id;
    
        // 结果
        private Object response;
    
        public GuardedObject(int id) {
            this.id = id;
        }
    
        // 获取结果
        public Object get() {
            synchronized (this) {
                // 没有结果
                while (response == null) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return response;
            }
        }
    
        /**
         * 获取结果 ------ 增加超时效果
         *
         * @param timeout 表示要等待多久
         * @return
         */
        public Object get(long timeout) {
            synchronized (this) {
                // 开始时间
                long begin = System.currentTimeMillis();
                // 经历的时间
                long passedTime = 0;
                // 没有结果
                while (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 response) {
            synchronized (this) {
                // 给结果成员变量赋值
                this.response = response;
                this.notifyAll();
            }
    
        }
    }
  • 运行结果

生产者 / 消费者模式(异步模式)

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式

实现

  • 代码

    java 复制代码
    @Slf4j(topic = "c.Test18")
    public class Test18 {
        public static void main(String[] args) {
            MessageQueue queue = new MessageQueue(2);
            for (int i = 0; i < 3; i++) {
                int id = i;
                new Thread(() -> {
                    queue.put(new Message(id, "值" + id));
                }, "生产者" + i).start();
            }
            new Thread(() -> {
                while(true){
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        queue.take();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }, "消费者").start();
        }
    }
    
    // 消息队列类    ------ Java 线程直接通信
    @Slf4j(topic = "c.MessageQueue")
    class MessageQueue {
        // 消息的队列集合
        private final LinkedList<Message> list = new LinkedList<>();
        // 队列容量
        private final int capacity;
    
        public MessageQueue(int capacity) {
            this.capacity = capacity;
        }
    
        // 获取消息
        public Message take() {
            // 检查队列是否为空
            synchronized (list) {
                while (list.isEmpty()) {
                    try {
                        log.debug("队列为空,消费者线程等待");
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 从队列头部获取消息并返回
                Message message = list.removeFirst();
                log.debug("已消费消息:{}",message);
                list.notifyAll();
                return message;
            }
        }
    
        // 存入消息
        public void put(Message message) {
            synchronized (list) {
                // 检查对象是否已满
                while (list.size() == capacity) {
                    try {
                        log.debug("队列已满,生产者线程等待");
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 将消息加入队列尾部
                list.addLast(message);
                log.debug("已生产消息:{}",message);
                list.notifyAll();
            }
        }
    }
    
    
    final class Message {
        private final int id;
        private final Object value;
    
        public Message(int id, Object value) {
            this.id = id;
            this.value = value;
        }
    
        public int getId() {
            return id;
        }
    
        public Object getValue() {
            return value;
        }
    
        @Override
        public String toString() {
            return "Message{" +
                    "id=" + id +
                    ", value=" + value +
                    '}';
        }
    }
  • 运行结果

工作线程(异步模式)

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

例如: 海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)

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

例如: 如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工


饥饿

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

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,在此 期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿
java 复制代码
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
    static Random RANDOM = new Random();

    static String cooking() {
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = executorService.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        executorService.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = executorService.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
    }
}

解决饥饿问题------ 不同的任务由不同的线程池执行

java 复制代码
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
    static Random RANDOM = new Random();

    static String cooking() {
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }

    public static void main(String[] args) {
        ExecutorService waiterPool = Executors.newFixedThreadPool(1);
        ExecutorService cookPool = Executors.newFixedThreadPool(1);
        waiterPool.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = cookPool.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        waiterPool.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = cookPool.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
    }
}
相关推荐
斌斌_____13 分钟前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@22 分钟前
Spring如何处理循环依赖
java·后端·spring
一个不秃头的 程序员44 分钟前
代码加入SFTP JAVA ---(小白篇3)
java·python·github
丁总学Java1 小时前
--spring.profiles.active=prod
java·spring
上等猿1 小时前
集合stream
java
java1234_小锋1 小时前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i1 小时前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动
林的快手1 小时前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
向阳12182 小时前
mybatis 缓存
java·缓存·mybatis
上等猿2 小时前
函数式编程&Lambda表达式
java