【JUC】并发编程(全量整理)

文章内容

1. 概念

1.1 多线程概念

CPU分时轮转调度机制:时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

进程:内存中正在运行的的程序;指一个内存中运行的应用程序;每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。进程中是可以有多个线程,这个应用程序也可以称之为多线程程序。

二者对比

  • (根本区别)进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    同一台计算机的进程通信称为 IPC(Inter-process communication)
    不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

管程:管程,对应的英文是Monitor。所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。

并行:同一时刻,不同主体可以同时处理事情的能力(强调不同实体)。多核cpu可以实现并行。
在这里插入图片描述

并发:与单位时间相关,在单位时间内可以处理事情的能力(强调同一实体)
在这里插入图片描述


结论

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

  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
    ● 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】)
    ● 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义

  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

锁的分类

  1. 从锁的实现方式来说:
    1. 乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据(将内存值与待更新的值进行比较,相同则更新为期望值,否则更新失败),如果失败则要重复读-比较-写的操作。另外,可以借助诸如版本号的手段来解决ABA问题。java 中的乐观锁基本都是通过 CAS (Compare and Swap)操作实现的。
    2. 悲观锁:悲观锁采用悲观思想的态度,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试cas去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
  2. 从锁的使用方式来说:
    1. 独占锁:独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
    2. 共享锁:共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

1.2 查看线程运行情况

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程
    taskkill /F /PID pid 杀死进程
    在这里插入图片描述

linux

  • ps -ef | grep java 查看关键字为Java的运行进程
  • kill - 9 pid 强制杀死进程
  • kill -15 pid 等待任务执行完毕,再杀死进程
  • top 动态的显示进程信息
  • top -H -p pid 查看某进程下的所有线程信息

Java

  • jps显示正在运行的java进程
  • jstack pid 查看更详细的进程信息(jdk命令)
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置
需要以如下方式运行你的 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类

2. Java线程

2.1 线程创建与运行

线程创建的几种方式

1、继承Thread类

2、实现Runnable接口

3、实现Callable接口。

  • call()方法可以有返回值;

  • call()方法可以向上抛出异常,而run()方法仅能内部处理;

    使用步骤
    (1)创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
    (2)使用FutureTask类来包装Callable对象(构造传入Callable对象)
    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程;
    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

4、通过线程池方式(重要)

  • Executors.newFixedThreadPool:(core=max=指定的值)创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  • Executors.newCachedThreadPool:(core=0,max=Integer.value)创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
  • Executors.newSingleThreadExecutor:(core=max=1)创建仅单个线程数的线程池,它可以保证任务先进先出的执行顺序;
  • Executors.newScheduledThreadPool:(定时任务的线程池)创建一个可以执行延迟任务的线程池;
  • Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
  • ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。(非常重要!!!)

5、线程复用

每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

package com.ma.juc;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Description
 * 线程创建的几种方式
 *     1、继承Thread类
 *     2、实现Runnable接口
 *     3、实现Callable接口
 *         3.1、call()方法可以有返回值;
 *         3.2、call()方法可以声明抛出异常;
 *         方式:
 *         (1)创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
 *         (2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
 *         (3)使用FutureTask对象作为Thread对象的target创建并启动新线程;
 *         (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
 *     4、通过线程池方式(重要)
 *         Executors.newFixedThreadPool:(core=max=指定的值)创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
 *         Executors.newCachedThreadPool:(core=0,max=Integer.value)创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
 *         Executors.newSingleThreadExecutor:(core=max=1)创建仅单个线程数的线程池,它可以保证任务先进先出的执行顺序;
 *         Executors.newScheduledThreadPool:(定时任务的线程池)创建一个可以执行延迟任务的线程池;
 *         Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
 *         Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
 *         ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。<br/> (非常重要!!!)
 * 转:[线程池的7种创建方式,强烈推荐你用它...](https://blog.csdn.net/qq36846776/article/details/111312342)
 *     [为什么尽量不要使用Executors创建线程池](https://www.cnblogs.com/kyoner/p/12318057.html)
 *
 *
 * 线程分类:
 *      线程可分为用户线程(user thread) 和 守护线程(daemon thread)。
 *      守护线程指在后台运行的线程,也称为后台线程,用于提供后台服务。
 *      Java创建的线程默认是用户线程。
 *      两者的差别是,当进程中还有用户线程在运行时,进程不终止;
 *      当进程中只有守护线程在运行时,进程终止。
 *      Thread类与守护线程有关的方法声明如下:
 *      public final void setDaemon(boolean on) //若on为true,则设置为守护线程,必须在启动线程前调用
 *      public final boolean isDaemon() //判断是否为守护线程,若是,则返回true;否则返回false
 * ————————————————
 * 版权声明:本文为CSDN博主「打你个落花流水」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
 * 原文链接:https://blog.csdn.net/a_haideyanlei/article/details/84923604
 * @Classname ThreadTest
 * @Created by Fearless
 * @Date 2021/8/2 19:53
 */
public class ThreadTest implements Runnable {



    /**
     * 参数说明:
     *      int corePoolSize:核心线程数 线程池中存放的已就绪的线程数量,若设置allowCoreThreadTimeOut则线程超时就会回收
     *      int maximumPoolSize:最大线程数量 控制资源
     *      long keepAliveTime:存活时间 线程中断到重新接受新任务之间的最大时间。
     *                          当前线程数量大于核心线程数量时,会释放空闲的线程
     *                          (需要释放的线程数量为maximumPoolSize - corePoolSize)
     *      TimeUnit unit:时间单位
     *      BlockingQueue<Runnable> workQueue:阻塞队列 任务堆积时,会将任务存放到队列中去,等待空闲队列的执行
     *      ThreadFactory threadFactory:线程工厂 负责线程的创建
     *      RejectedExecutionHandler handler:拒绝策略 如果队列任务满了,按照指定的拒绝策略拒绝执行任务
     *                                        丢弃最老的;丢弃新来的,同时抛异常;直接利用当前线程直接调用run方法同步执行任务;丢弃新来的(不抛异常)
     * 工作流程:
     *      1)线程池创建,准备好core数量的核心线程,准备接受任务
     *      1.1、core满了,就将在进来的任务放入阻塞队列中,(注意是先进队列队列满了再起非核心线程提供服务)。空闲的core就会自己去阻塞队列中获取任务执行
     *      1.2、阻塞队列满了,就直接开新线程执行,最大开大max指定的数量
     *      1.3、max满了就用RejectedExecutionHandler拒绝任务
     *      1.4、如果线程池中线程数量大于core,将在指定的keepAliveTime以后,释放max-core数量的多余线程
     *
     */
    private static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,200,10, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        /*Runnable thread1 = new Thread2();
        Thread thread2 = new Thread(thread1);
        thread2.setName("Thread2");
        thread2.start();
        Thread.sleep(60000);*/
        Thread3 thread3 = new Thread3();
        FutureTask<Integer> task = new FutureTask<Integer>(thread3);
        for (int j = 0; j < 50 ; ++j){
            System.out.println(Thread.currentThread().getName() + " 大循环的循环变量j的值为:" + j);
            if (j == 20){
                new Thread(task,"有返回值的线程" + j).start();
            }
        }
        try {
            // 阻塞等待获取子线程的返回值
            System.out.println("子线程的返回值:" + task.get());
            Thread.sleep(6000);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
     }
}

class Thread1 extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在运行..");
    }

}
class Thread2 implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在运行..");
    }
}
class Thread3 implements Callable{

    @Override
    public Object call() throws Exception {
        int i = 0;
        for (;i < 50 ; ++i){
            System.out.println(Thread.currentThread().getName() + " 的线程执行体内的循环变量i的值为:" + i);
        }
        return i;
    }
}

2.2 线程运行原理

栈与栈帧

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 频繁发生会影响性能

思考:线程上下文切换与线程用户态向内核态转换之间的关系?
【JUC】什么是用户态及内核态?

2.3 常用api

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

start 与 run:调用start会通过操作系统创建出线程,之后由此线程来执行run方法(可以将run方法看做一个任务);单独调用run仅是方法调用,仍然是在当前线程中运行。

2)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 闲时,优先级几乎没作用
3)join() 方法(使线程插队)

用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。如在主线程中调用ti.join(),则是主线程等待t1线程结束,join 采用同步。

Thread t1 = new Thread();
//等待 t1 线程执行结束
t1.join();
// 最多等待 1000ms,如果 1000ms 内线程执行完毕,则会直接执行下面的语句,不会等够 1000ms
t1.join(1000);
4)interrupt() 方法

interrupt 中断线程有两种情况,如下:

  • 如果一个线程在在运行中被中断,中断标记会被置为 true
  • 如果是中断因sleep wait join 方法而进入其它状态的(比如阻塞)线程,将无法中断抛出InterruptedException异常,中断标记仍为 false
5)终止模式之两阶段终止模式

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。
在这里插入图片描述
简单实现代码如下:

package com.ma.juc;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @author mayujie@jyd.com.cn
 * @version v1.0
 * @description TwoPhaseTermination 两阶段终止模式
 * @date 2022/6/14 15:19
 */
@Slf4j
public class TwoPhaseTermination {
    private Thread monitor;

    // 开启监控线程
    public void start(){
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                try {
                    if (current.isInterrupted()) {
                        log.info("料理后事...");
                        break;
                    }
                    TimeUnit.SECONDS.sleep(1);
                    log.info("执行监控记录...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    current.interrupt();
                }
            }
        });
        monitor.start();
    }
    // 停止监控线程
    public void stop(){
        monitor.interrupt();
    }

    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination phaseTermination = new TwoPhaseTermination();
        phaseTermination.start();
        TimeUnit.SECONDS.sleep(3);
        phaseTermination.stop();
    }
}
6)balking (犹豫)模式

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

  • 用一个标记来判断该任务是否已经被执行过了
  • 需要避免线程安全问题
  • 加锁的代码块要尽量的小,以保证性能
public class BalkingDemo {

    public static void main(String[] args) throws InterruptedException {
        Monitor monitor = new Monitor();
        monitor.start();
        monitor.start();
        TimeUnit.SECONDS.sleep(3);
        monitor.stop();
    }
}

class Monitor {
    Thread monitor;
    // 设置标记,用于判断是否被终止了
    private volatile  boolean stop = false;
    // 设置标记,用于判断是否已经启动过了
    private boolean starting = false;

    /**
     * 启动监视器线程
     */
    public void start() {
        synchronized (this) {
            if (starting) {
                return;
            }
            starting = true;
        }
        String s = UUID.randomUUID().toString();
        monitor = new Thread(() -> {
            // 开始不停的监控
            while (true) {
                if (stop) {
                    System.out.println(Thread.currentThread().getName() + "处理后续任务...");
                }
                System.out.println(Thread.currentThread().getName() + "监视器运行中...");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + "被打断了...");
                }
            }
        }, s);
        monitor.start();;
    }

    /**
     * 用于停止监视器线程
     */
    public void stop() {
        monitor.interrupt();// 打断线程
        stop = true;
    }
}
7)sleep,yiled,wait,join 对比

参考文章:sleep、yield、wait、join的区别(阿里)

8)守护线程

默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用 t1.setDeamon(true); 方法变成守护线程。

2.4 线程状态

1)从操作系统层划分,线程有 5 种状态

在这里插入图片描述

  1. 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
  2. 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
  3. 运行状态,指线程获取了CPU时间片,正在运行
    当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
  4. 阻塞状态
    如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切 换,进入【阻塞状态】
    等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
  5. 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

2)java线程的 6 种状态及状态切换

这是从 Java API 层面来描述的,我们主要研究的就是这种。可以参考文章👉Java线程的6种状态及切换(透彻讲解)

在这里插入图片描述

  • NEW 跟五种状态里的初始状态是一个意思
  • RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分。

以上可从java.lang.Thread.State查看

在这里插入图片描述

测试代码如下:

package com.ma.juc;

import org.junit.Test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author mayujie@jyd.com.cn
 * @version v1.0
 * @description ThreadStateTest
 * @date 2021/12/29 11:08
 */
public class ThreadStateTest {

    public static void main(String[] args) {
        // 没调用start方法查看下thread状态 NEW
        threadStateNew();
        // 调用start方法查看thread状态 RUNNABLE
        workingThread();
        // thread调用join方法查看状态 TERMINATED
        threadStateTerminate();
        // main线程持有锁(可重入锁)的同时查看thread状态 WAITING
        threadBlockedByLock();
        // 多个线程同时竞争同一把锁时,未获得锁的线程处于 BLOCKED
        threadBlockedBySynchronized();

        threadSleep();

        threadWait();

        threadTimeWait();
    }

    private static void threadStateNew(){
        System.out.println("--------------------------------------");
        System.out.println("Never Start Thread State:");
        Thread thread = new Thread(() -> {
            // System.out.println("Thread Running...");
        }, "Thread Never Start");
        // print NEW
        System.out.println(thread.getState());
        System.out.println("--------------------------------------");

    }

    private static void workingThread(){
        System.out.println("--------------------------------------");
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                doSomeElse();
            }
        });
        thread.start();
        doSomeElse();
        // printf RUNNABLE
        System.out.println("Working Thread State:" + thread.getState());
        System.out.println("--------------------------------------");
    }

    private static void threadStateTerminate(){
        System.out.println("--------------------------------------");
        System.out.println("Finish Job Thread State:");
        Thread thread = new Thread(() -> {
        }, "Thread Finish Job");
        thread.start();
        try {
            // Main Thread will wait until this thread finished job
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // print TERMINATE
        System.out.println(thread.getState());
        System.out.println("--------------------------------------");

    }

    /**
     * 为啥这块是waiting而下面是blocked???
     */
    private static void threadBlockedByLock(){
        System.out.println("--------------------------------------");
        System.out.println("Thread State Blocked By Lock:");
        ReentrantLock lock = new ReentrantLock();
        Thread thread = new Thread(lock::lock, "Blocked Thread");
        lock.lock();
        try {
            thread.start();
            doSomeElse();
            //print WAITING
            System.out.println(thread.getState());
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        System.out.println("--------------------------------------");

    }

    /**
     * BLOCKED状态是两个线程同时获取一把锁时,没拿到锁的线程切换到BLOCKED状态,
     * 有时打印出RUNNABLE可能是线程没到开始获取锁,打印线程的状态动作有些提前
     */
    private static void threadBlockedBySynchronized(){
        System.out.println("--------------------------------------");
        System.out.println("Thread Blocked By Synchronized:");
        Thread thread = new Thread(() -> {
            synchronized (ThreadStateTest.class){
                System.out.println("thread成功获取synchronized锁");
            }
        }, "Blocked by Synchronized Thread");
        thread.start();
        synchronized (ThreadStateTest.class){
            System.out.println("main成功获取synchronized锁");
            doSomeElse();
            // print BLOCKED 怎么还能打印RUNNABLE???
            // 阻塞main线程3s,等待thread去获取锁
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread State is:" + thread.getState());
            System.out.println("锁释放...");
        }
        System.out.println("--------------------------------------");
    }

    private static void threadSleep(){
        System.out.println("--------------------------------------");
        System.out.println("Sleeping Thread:");
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread Sleep");
        thread.start();
        doSomeElse();
        // print TIMED_WAITING
        System.out.println(thread.getState());
        System.out.println("--------------------------------------");

    }

    private static void threadWait(){
        System.out.println("--------------------------------------");
        System.out.println("Thread waiting");
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock){
                System.out.println("threadA获得到锁!!!");
                try {
                    lock.wait();
                    for (int i = 0; i < 100; i++) {
                        doSomeElse();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread Waiting");
        Thread threadB = new Thread(() -> {
            synchronized (lock){
                System.out.println("threadB获得到锁!!!");
                // print WAITING
                System.out.println("Before Notify, Thread A State:" + threadA.getState());
                lock.notify();
                // print BLOCKED
                System.out.println("After Notify, Thread A State:" + threadA.getState());
            }
        });

        threadA.start();
        doSomeElse();
        threadB.start();

        try {
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // print RUNNABLE
        System.out.println("After Thread B finish job, Thread A State:" + threadA.getState());
        System.out.println("--------------------------------------");
    }

    private static void threadTimeWait(){
        System.out.println("--------------------------------------");
        System.out.println("Thread Waiting:");
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock){
                try {
                    lock.wait(1000);
                    for (int i = 0; i < 100; i++) {
                        doSomeElse();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread Waiting");
        Thread threadB = new Thread(() -> {
            synchronized (lock) {
                // print TIMED_WAITING
                System.out.println("Before Notify, Thread A State:" + threadA.getState());
                lock.notify();
                // print BLOCKED
                System.out.println("After Notify, Thread A State:" + threadA.getState());
            }
        });
        System.out.println(Thread.currentThread().getState());
        threadA.start();
        doSomeElse();
        threadB.start();
        try {
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // print RUNNABLE
        System.out.println("After Thread B finish job, Thread A State:" + threadA.getState());
        System.out.println("--------------------------------------");

    }


    /**
     * take some times, let the thread get cpu time
     */
    private static void doSomeElse(){
        double meanless = 0d;
        for(int i=0; i<10000; i++){
            meanless += Math.random();
        }
    }

    // 自己创建的thread线程是由main线程调用start方法通过操作系统初始化出来的,而run方法只是thread的执行任务而已
    // 因此start方法必定是在run方法执行之前
    @Test
    public void test(){
        new Thread(() -> {
            System.out.println("run执行了" + System.currentTimeMillis());
        }).start();
        System.out.println("start执行了" + System.currentTimeMillis());
    }


}

2.5 线程生命周期

  • 非守护线程:按上图操作系统线程运行状态来阐述各个阶段的流程。
  • 守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

3. 共享模式之管程

3.1 线程共享带来的问题

线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了,最终造成数据错乱等现象。比较常见的例子有多线程情况下,i++、++i等自增自减操作时,会造成线程不安全的发生。这块需要清楚两个概念:

1)临界区 Critical Section

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

例如,下面代码中的临界区

static int counter = 0;
 
static void increment() 
// 临界区 
{   
    counter++; 
}
 
static void decrement() 
// 临界区 
{ 
    counter--; 
}

2)竞态条件 Race Condition

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

3.2 synchronized 解决方案

1)解决手段

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

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

现在讨论使用synchronized关键字来进行解决,即俗称的对象锁(这指的是广义上的对象),它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

3.3 变量的线程安全分析(重要)

线程安全:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。(简单来说就是多线程情况下,任意的读写操作都不会造成最终的数据错乱,则该对象就是线程安全的

线程安全问题大多是由全局变量静态变量引起的,局部变量逃逸也可能导致线程安全问题。

1)成员变量和静态变量的线程安全分析
  • 如果变量没有在线程间共享,那么线程对该变量操作是安全的
  • 如果变量在线程间共享
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码就是临界区,需要考虑线程安全问题
2)部变量线程安全分析
  • 局部变量【局部变量被初始化为基本数据类型】是安全的
  • 局部变量是引用类型或者是对象引用则未必是安全的
    • 如果局部变量引用的对象没有引用线程共享的对象,那么是线程安全的
    • 如果局部变量引用的对象引用了一个线程共享的对象,那么要考虑线程安全问题

如果局部变量引用的对象逃离方法的范围,那么要考虑线程安全的,分析如下代码:

@Slf4j(topic = "c.Code_18_Test")
public class Code_18_Test {

    public static void main(String[] args) {
    	// 局部引用对象unsafeTest 是否线程安全取决于内部实现
        UnsafeTest unsafeTest = new UnsafeTest();	
        for(int i = 0; i < 10; i++) {
            new Thread(() -> {
                unsafeTest.method1();
            }, "t" + i).start();
        }
    }

}

class UnsafeTest {

    List<Integer> list = new ArrayList<>();	//该List被多个线程共享读写,因此线程不安全

    public void method1() {
        for (int i = 0; i < 200; i++) {
            method2();
            method3();
        }
    }

    public void method2() {
        list.add(1);
    }

    public void method3() {
        list.remove(0);
    }

}
3)不安全原因分析

在这里插入图片描述
如图所示,因为 list 是实例变量,则多个线程都会使用到这个共享的实例变量,就会出现线程安全问题,为什么会有安全问题呢,首先要理解 list 添加元素的几步操作,第一步会获取添加元素的下标 index,第二步对指定的 index 位置添加元素,第三步将 index 往后移。
当 t0 线程从 list 拿到 index = 0 后,t0 线程的时间片用完,出现上下文切换,t1 获取时间片开始执行,从 list 也拿到 index = 0,然后将元素添加到 index 位置,然后将 index 值加 1,然后 t0 线程获取时间片,对 index = 0 位置添加元素,此时 index = 0 已经存在元素,就会出现报错。

4)解决方法

可以将 list 修改成局部变量,然后将 list 作为引用传入方法中,因为局部变量是每个线程私有的,不会出现共享问题,那么就不会有上述问题了。修改的代码如下:

class SafeTest {

    public void method1() {
 
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            method2(list);
            method3(list);
        }
    }

    public void method2(List<Integer> list) {
        list.add(1);
    }

    public void method3(List<Integer> list) {
        list.remove(0);
    }

}
5)思考 private 或 final的重要性

在上诉代码中,其实存在线程安全的问题,因为 method2,method3 方法都是用 public 声明的,如果一个类继承 SafeTest 类,对 method2,method3 方法进行了重写,比如重写 method3 方法,代码如下:

class UnsafeSubTest extends UnsafeTest {

    @Override
    public void method3(List<Integer> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

可以看到重写的方法中又使用到了线程,当主线程和重写的 method3 方法的线程同时存在,此时 list 就是这两个线程的共享资源了,就会出现线程安全问题,我们可以用 private 访问修饰符解决此问题,代码实现如下:

class ThreadSafe {
    public final 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);
    }
}
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】。

6)常见线程安全类
  • String
  • Integer
  • StringBuffer
  • Random
  • Vector (List的线程安全实现类)
  • Hashtable (Hash的线程安全实现类)
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。如:

Hashtable table = new Hashtable();
new Thread(()->{
 	table.put("key1", "value1");
}).start();
new Thread(()->{
 	table.put("key2", "value2");
}).start();

线程安全类方法的组合

但注意它们多个方法的组合不是原子的,看如下代码

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

在这里插入图片描述
如上图所示,当使用方法组合时,出现了线程安全问题,当线程 1 执行完 get(“key”) ,这是一个原子操作没出问题,但是在 get(“key”) == null 比较时,如果线程的时间片用完了,线程 2 获取时间片执行了 get(“key”) == null 操作,然后进行 put(“key”, “v2”) 操作,结束后,线程 1 被分配 cpu 时间片继续执行,执行 put 操作就会出现线程安全问题。

不可变类的线程安全

String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的,有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了!

public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen); // 新建一个对象,然后返回,没有修改等操作,是线程安全的。
    }

4. Monitor

4.1 Monitor监视器锁

每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:

在这里插入图片描述

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的monitorenter和monitorexit指令来实现。

同步代码块:

monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

a. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
b. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
c. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去
获取这个 monitor 的所有权。monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异常退出释放锁。

示例代码:

public class TestSynchronized {
    static final Object obj = new Object();
    static int i=0;
    public static void main(String[] args) {
        synchronized (obj){
            i++;
        }
    }
}

反编译后的字节码

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2         // 获取obj对象
         3: dup
         4: astore_1
                  
         5: monitorenter		//将obj对象的markword置为monitor指针
         6: getstatic     #3                  
         9: iconst_1
        10: iadd
        11: putstatic     #3                  
        14: aload_1
        15: monitorexit			//同步代码块正常执行时,将obj对象的markword重置,唤醒EntryList
        16: goto          24
                  
        19: astore_2
        20: aload_1
        21: monitorexit			//同步代码块出现异常时,将obj对象的markword重置,唤醒EntryList
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any  //监测6-16行jvm指令,如果出现异常就会到第19行
            19    22    19   any

通过代码描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

同步方法:

方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:

示例代码:

public class TestSynchronized {
    static int i=0;
    public synchronized  void add(){
        i++;
    }
}

反编译后

 public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field i:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field i:I
         8: return

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

说了这么多,那什么是Monitor呢??

可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

在这里插入图片描述
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

当多个线程同时请求某个对象锁时,对象锁会设置⼏种状态⽤来区分请求的线程:

  • Contention List:所有请求锁的线程将被⾸先放置到该竞争队列
  • Entry List:Contention List中那些有资格(锁的竞争者)成为候选⼈的线程被移到Entry List
  • Wait Set:那些调⽤wait⽅法被阻塞的线程被放置到Wait Set
  • OnDeck:任何时刻最多只能有⼀个线程正在竞争锁,该线程称为OnDeck
  • Owner:获得锁的线程称为Owner
  • !Owner:释放锁的线程

当⼀个线程尝试获得锁时,如果该锁已经被占⽤,则会将该线程封装成⼀个 ObjectWaiter 对象插⼊到Contention List的队列的队⾸,然后调⽤ park 函数挂起当前线程。

当线程释放锁时,会从Contention List或EntryList中挑选⼀个线程唤醒,被选中的线程叫做 Heir presumptive 即假定继承⼈,假定继承⼈被唤醒后会尝试获得锁,但 synchronized 是⾮公平的,所以假定继承⼈不⼀定能获得锁。这是因为对于重量级锁,线程先⾃旋尝试获得锁,这样做的⽬的是为了减少执⾏操作系统同步操作带来的开销。如果⾃旋不成功再进⼊等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有⼀个不公平的地⽅是⾃旋线程可能会抢占了Ready线程的锁(Ready线程是干嘛的???)。线程获得锁后调⽤ Object.wait ⽅法,则会将线程加⼊到WaitSet中,当被 Object.notify 唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调⽤⼀个锁对象的 wait 或 notify ⽅法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。监视器Monitor通过两种方式实现同步:互斥协作Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行, 使用notify/notifyAll/wait 方法来协同不同线程之间的工作。这些方法在 Object 类上被定义,会被所有的 Java 对象自动继承。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。

  • 互斥。监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
  • 协作。监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。

在这里插入图片描述

执行流程:

  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
  2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
  3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
  4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList中。
  5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
  6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
  7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
  8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

参考文章:

4.1 对象布局

在这里插入图片描述
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

  • 实例数据:存放类的属性数据信息,包括父类的属性信息;

  • 对齐填充:由于虚拟机要求,对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?
是存在锁对象的对象头中的。
HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,
arrayOopDesc对象用来描述数组类型。
instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,
另外,arrayOopDesc的定义对应 arrayOop.hpp 。

在这里插入图片描述

以 64 位虚拟机为例,普通对象的对象头结构如下,其中的 Klass Word 为指针,指向对应的 Class 对象;

在这里插入图片描述

其中 Mark Word 结构为
在这里插入图片描述
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:

在这里插入图片描述
在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5. synchronized 原理进阶(重要)

5.1 基本概念

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制。但是在有些情况下,JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除锁消除的依据是逃逸分析的数据支持。

如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时 ,如StringBuffer、Vector、HashTable等,这个时候会存在隐式的加锁操作。比如StringBuffer的append()方法,Vector的add()方法。

public class VectorTest {
    public void test(){
        Vector<Integer> vector = new Vector<Integer>(); 
        for(int i=0;i<10;i++){
            vector.add(i);
        }
        System.out.println(vector);
    }
}

在运行这段代码时, JVM可以明显检测到变量vector没有逃逸出方法VectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

锁粗化

在使用同步锁的时候,需要让同步块的作用范围尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作量尽可能缩小。如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

在大多数的情况下,上述观点是正确的。但是连续加锁解锁操作可能会导致不必要的性能损耗,所以引入锁粗化的概念。锁粗化概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例: vector每次add的时候都需要加锁操作, JVM检测到对同一个对象( vector )连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

锁膨胀(synrhonized锁的升级)

5.2 synchronized 用于同步代码块与同步方法原理

无锁–》偏向锁–》轻量级锁–》重量级锁

synchronized锁升级详细流程图(JDK1.6优化):
在这里插入图片描述
参考如下文章:

注意:下述锁均为synchronized锁不同时刻的状态,不是单独类型的锁。另外锁升级后状态不可逆!!!

下面将阐述各种状态synchronized锁的工作原理:

1)自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。 所以引入自旋锁。

所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行段无意义的循环即可(自旋)。

自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来住能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。若同步代码块执行比较耗时,即线程持有锁的时间较长,自旋锁效率下降,不建议使用。

适应性自旋锁(自旋锁的进一步优化)

假如我将自旋次数调整为10 ,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁) ,你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

JDK 1.6引入了更加聪明的自旋锁,即适应性自旋锁。所谓适应性就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的。那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

JDK1.6 :-XX: +UseSpinning 开启;-XX: PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;

2)偏向锁

在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行 CAS 操作,这是有点耗时的。那么 java6 开始引入了偏向锁,只有第一次使用 CAS 时将对象的 Mark Word 头设置为偏向线程 ID,之后这个入锁线程再进行重入锁时,发现线程 ID 是自己的,那么就不用再进行CAS了。(解决锁重入,实现单线程加锁的锁消除

偏向状态,对象头格式如下:

在这里插入图片描述

  • 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的 Thread,epoch,age 都是 0 ,在加锁的时候进行设置这些的值.
  • 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:
    -XX:BiasedLockingStartupDelay=0 来禁用延迟
    注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
/**
 * @Classname TestBiased
 * @Description TODO
 * @Created by Fearless
 * @Date 2022/6/16 20:47
 */
@Slf4j
public class TestBiased {

    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        // System.out.println(Integer.toHexString(dog.hashCode()));
        log.debug(ClassLayout.parseInstance(dog).toPrintable());
        synchronized (dog) {
            log.debug(ClassLayout.parseInstance(dog).toPrintable());
        }
        // 1000110101101000000000101
        // 00000000011ad005
        log.debug(ClassLayout.parseInstance(dog).toPrintable());
    }
}


class Dog {

}

运行结果:

在这里插入图片描述

撤销偏向

以下几种情况会使对象的偏向锁失效

  • 调用对象的 hashCode 方法
  • 多个线程使用该对象
  • 调用了 wait/notify 方法(调用wait方法会导致锁膨胀而使用重量级锁)
3)轻量级锁

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized ,假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
     synchronized( obj ) {
         // 同步块 A
         method2();
     }
}
public static void method2() {
     synchronized( obj ) {
         // 同步块 B
     }
}
  1. 每次指向到 synchronized 代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的 Mark Word 和对象引用 reference
    在这里插入图片描述

  2. 让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中。
    在这里插入图片描述

  3. 如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态 00 表示轻量级锁,如下所示
    在这里插入图片描述

  4. 如果cas失败,有两种情况

    1. 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
    2. 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数。
      在这里插入图片描述
  5. 当退出 synchronized 代码块(解锁时)

    1. 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
    2. 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
      1.成功,则解锁成功
      2.失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁(此时mark word值已经被改变),进入重量级锁解锁流程
      在这里插入图片描述
4)锁膨胀

如果在尝试加轻量级锁的过程中,cas 操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    在这里插入图片描述
  2. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,
    1. 即为对象申请Monitor锁,让Object指向重量级锁地址
    2. 然后自己进入Monitor 的EntryList 变成BLOCKED状态
      在这里插入图片描述
  3. 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程(此时只有线程Thread-1)
5)重量级锁

重量级锁就是Monitor锁,参考上文Monitor的介绍。

6. Wait/Notify

1)原理

在这里插入图片描述

  • 锁对象调用wait方法(obj.wait),就会使当前线程进入 WaitSet 中,变为 WAITING 状态。
  • 处于BLOCKED和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别:
    • BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于 BLOCKED 状态
    • WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了 wait 方法而进入了 WaitSet 中,处于 WAITING 状态
  • BLOCKED 状态的线程会在锁被释放的时候被唤醒,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法(obj.notify/obj.notifyAll),才会被唤醒。

注:只有当对象加锁以后,才能调用 wait 和 notify 方法

2)优雅地使用 wait/notify

什么时候适合使用wait

  • 当线程不满足某些条件,需要暂停运行时,可以使用 wait 。这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用 sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行。使用wait/notify需要注意什么
  • 当有多个线程在运行时,对象调用了 wait 方法,此时这些线程都会进入 WaitSet 中等待。如果这时使用了 notify 方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用 notifyAll 方法
synchronized (lock) {
	while(//不满足条件,一直等待,避免虚假唤醒) {
		lock.wait();
	}
	//满足条件后再运行
}

synchronized (lock) {
	//唤醒所有等待线程
	lock.notifyAll();
}
3)同步模式之保护性暂停

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

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

多任务版 GuardedObject 图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,但是生产者消费者模式并不是。rpc 框架的调用中就使用到了这种模式。

在这里插入图片描述

示例代码:

/**
 * 同步模式-保护性暂停 (Guarded-Suspension-pattern)
 */
@Slf4j(topic = "c.Code_23_Test")
public class Code_23_Test {

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {
            new People().start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        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.info("收信的为 id: {}", guardedObject.getId());
        Object o = guardedObject.get(5000);
        log.info("收到信的 id: {}, 内容: {}", guardedObject.getId(), o);
    }
}

@Slf4j(topic = "c.Postman")
class Postman extends Thread {

    private int id;
    private String mail;

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

    @Override
    public void run() {
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        log.info("送信的 id: {}, 内容: {}", id, mail);
        guardedObject.complete(mail);
    }
}

class Mailboxes {

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

    public static synchronized int generateId() {
        return id++;
    }

    // 用户会进行投信
    public static GuardedObject createGuardedObject() {
        GuardedObject guardedObject = new GuardedObject(generateId());
        boxes.put(guardedObject.getId(), guardedObject);
        return guardedObject;
    }

    // 派件员会派发信
    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }

    public static Set<Integer> getIds() {
        return boxes.keySet();
    }
}

class GuardedObject {

    private int id;

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

    public int getId() {
        return this.id;
    }

    private Object response;

    // 优化等待时间
    public Object get(long timeout) {
        synchronized (this) {
            long begin = System.currentTimeMillis();
            long passTime = 0;
            while (response == null) {
                long waitTime = timeout - passTime; // 剩余等待时间
                if(waitTime <= 0) {
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                passTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (this) {
            this.response = response;
            this.notify();
        }
    }

}
4)异步模式之生产者/消费者

要点

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

“异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。

在这里插入图片描述

5)小结
  1. 当调用 wait 时,首先需要确保调用了 wait 方法的线程已经持有了对象的锁(调用 wait 方法的代码片段需要放在 sychronized 块或者时 sychronized 方法中,这样才可以确保线程在调用wait方法前已经获取到了对象的锁)
  2. 当调用 wait 时,该线程就会释放掉这个对象的锁,然后进入等待状态 (wait set)
  3. 当线程调用了 wait 后进入到等待状态时,它就可以等待其他线程调用相同对象的 notify 或者 notifyAll 方法使得自己被唤醒
  4. 一旦这个线程被其它线程唤醒之后,该线程就会与其它线程以同开始竞争这个对象的锁(公平竞争);只有当该线程获取到对象的锁后,线程才会继续往下执行
  5. 当调用对象的 notify 方法时,他会随机唤醒对象等待集合 (wait set) 中的任意一个线程,当某个线程被唤醒后,它就会与其它线程一同竞争对象的锁
  6. 当调用对象的 notifyAll 方法时,它会唤醒该对象等待集合 (wait set) 中的所有线程,这些线程被唤醒后,又会开始竞争对象的锁
  7. 在某一时刻,只有唯一的一个线程能拥有对象的锁

7. park & unpark

1)基本使用

park & unpark 是 LockSupport 线程通信工具类的静态方法。

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark;
2)park unpark 原理

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

  • 打个比喻线程就像一个旅人,Parker 就像他随身携带的背包,条件变量 _ cond 就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

先调用park再调用upark的过程

  1. 先调用 park
    1. 当前线程调用 Unsafe.park() 方法
    2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁(mutex对象有个等待队列 _cond)
    3. 线程进入 _cond 条件变量阻塞
    4. 设置 _counter = 0

在这里插入图片描述

  1. 调用 upark
    1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    2. 唤醒 _cond 条件变量中的 Thread_0
    3. Thread_0 恢复运行
    4. 设置 _counter 为 0

在这里插入图片描述

先调用upark再调用park的过程

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

在这里插入图片描述

8. 活跃性

1)定义

线程因为某些原因,导致代码一直无法执行完毕,这种的现象叫做活跃性。

2)死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
如:t1 线程获得 A 对象锁,接下来想获取 B 对象的锁 t2 线程获得 B 对象锁,接下来想获取 A 对象的锁。

public static void main(String[] args) {
		final Object A = new Object();
		final Object B = new Object();
		new Thread(()->{
			synchronized (A) {
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (B) {

				}
			}
		}).start();

		new Thread(()->{
			synchronized (B) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (A) {

				}
			}
		}).start();
	}

发生死锁的必要条件

  • 互斥条件
    在一段时间内,一种资源只能被一个进程所使用
  • 请求和保持条件
    进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源
  • 不可抢占条件
    进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放
  • 循环等待条件
    发生死锁时,必然存在一个进程——资源的循环链。

定位死锁的方法

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

哲学家就餐问题

有五位哲学家,围坐在圆桌旁。 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。 如果筷子被身边的人拿着,自己就得等待 。
当每个哲学家即线程持有一根筷子时,他们都在等待另一个线程释放锁,因此造成了死锁。这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况。

/**
 * @author mayujie@jyd.com.cn
 * @version v1.0
 * @description Chopstick
 * @date 2022/6/22 16:33
 */
public class Chopstick extends ReentrantLock {

    String name;

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

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

    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();

    }
}

@Slf4j
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

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

    public void eat() {
        log.debug("eating...");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            /*// 获取左筷子
            synchronized (left) {
                // 获取右筷子
                synchronized (right) {
                    eat();
                }
            }*/
            // 使用ReentrantLock解决哲学家就餐问题
            // 公平锁会降低并发度
            if (left.tryLock()) {
                try {
                    if (right.tryLock()) {

                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }

                    }
                } finally {
                    left.unlock();
                }
            }

        }
    }
}

避免死锁的方法

  1. 避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。主线程要对 A、B 两个对象的 Lock 进行锁定,副线程也要对 A、B 两个对象的 Lock 进行锁定,这就埋下了导致死锁的隐患。
  2. 具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。比如上面的死锁程序,主线程先对 A 对象的 Lock 加锁,再对 B 对象的 Lock 加锁;而副线程则先对 B 对象的 Lock 加锁,再对 A 对象的 Lock 加锁。这种加锁顺序很容易形成嵌套锁定,进而导致死锁。如果让主线程、副线程按照相同的顺序加锁,就可以避免这个问题。
  3. 使用定时锁。程序在调用 acquire() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
  4. 死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。(比如银行家算法)
3)活锁

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

避免活锁的方法
在线程执行时,中途给予不同的间隔时间即可。

死锁与活锁的区别

  • 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。
  • 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。
4)饥饿

某些线程因为优先级太低,导致一直无法获得资源的现象。在使用顺序加锁时,可能会出现饥饿现象

9. ReentrantLock

和 synchronized 相比具有的的特点

  • 可重入:同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此无需竞争再次使用这把锁
  • 可中断:如果某个线程处于阻塞状态,可以调用其 interrupt 方法让其停止阻塞,然后停止运行
  • 可以设置超时时间:规定时间内未获取到锁就直接停止运行
  • 可以设置为公平锁 (先到先得):构造方法传入true/false
  • 支持多个条件变量( 具有多个 WaitSet)

常用API:

  1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
  2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
  3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
  4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
  5. getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
  6. getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
  7. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
  8. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
  9. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
  10. hasQueuedThreads():是否有线程等待此锁
  11. isFair():该锁是否公平锁
  12. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
  13. isLock():此锁是否有任意线程占用
  14. lockInterruptibly():如果当前线程未被中断,获取锁
  15. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
  16. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。

Condition 类和 Object 类锁方法区别区别

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

tryLock 和 lock 和 lockInterruptibly 的区别

  1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit
    unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
  2. lock 能获得锁就返回 true,不能的话一直等待获得锁
  3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,
    lock 不会抛出异常,而 lockInterruptibly 会抛出异常

10. Semaphore、CyclicBarrier、CountDownLatch

10.1 Semaphore(信号量-控制同时访问的线程个数)

Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。

/**
 * @author mayujie@jyd.com.cn
 * @version v1.0
 * @description Semaphore
 * @date 2022/7/28 17:42
 */
public class SemaphoreTest {

    public static void main(String[] args) {
        Semaphore semp = new Semaphore(5);
        try {
            semp.acquire(); // 申请许可
            System.out.println("执行业务代码...");
            System.out.println(semp.availablePermits());	// print 4
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semp.release();// 释放许可
        }
        System.out.println(semp.availablePermits());	// 释放之后可用的信号量复原
    }

}

Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与release()方法来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与 ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。

此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与 ReentrantLock 几乎一致。Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定。

Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。

10.2 CyclicBarrier(回环栅栏-等待至 barrier 状态再全部同时执行)

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用。我们暂且把这个状态就叫做barrier,当调用 await()方法之后,线程就处于 barrier 了。CyclicBarrier 中最重要的方法就是 await 方法,它有 2 个重载版本:

  1. public int await():用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任
    务;
  2. public int await(long timeout, TimeUnit unit):让这些线程等待至一定的时间,如果还有
    线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续任务。

10.3 CountDownLatch(计数器)

CountDownLatch 类位于 java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch来实现这种功能了。

区别:

  • CountDownLatch 和 CyclicBarrier 都能够实现线程之间的等待,只不过它们侧重点不同;CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行;而 CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;另外,CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的。
  • Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。

11. 阻塞队列

方法类型名称用法介绍
插入boolean add(E paramE)将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。如果该元素是 NULL,则会抛出 NullPointerException 异常。
插入boolean offer(E paramE)将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false。
插入put(E paramE)将指定元素插入此队列中,将阻塞等待可用的空间(如果有必要)
插入offer(E o, long timeout, TimeUnit unit)可以设定等待的时间,如果在指定的时间内,还不能往队列中加入 BlockingQueue,则返回失败。
取出poll(time)取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null
取出poll(long timeout, TimeUnit unit)从 BlockingQueue 取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败
取出take()取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 BlockingQueue 有新的数据被加入。
取出drainTo()一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

Java中的阻塞队列实现有:

  1. ArrayBlockingQueue :由数组结构组成的有界阻塞队列。用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐
    量。
  2. LinkedBlockingQueue :由链表结构组成的有界阻塞队列。基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
  3. PriorityBlockingQueue :支持优先级排序的无界阻塞队列。是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
  4. DelayQueue:使用优先级队列实现的无界阻塞队列。是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
  5. SynchronousQueue:不存储元素的阻塞队列。是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用, SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和ArrayBlockingQueue。
  6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。是一个由链表结构组成的无界阻塞 TransferQueue 队列。相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。
    1. transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的poll()方法时), transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
    2. tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素, 则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必 须等到消费者消费了才返回。
  7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列。是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同于 takeFirst,不知道是不是 Jdk 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。

12. Java 内存模型(JMM)

Java内存模型的英文名称为Java Memory Model(JMM),其并不像JVM内存结构一样真实存在,而是一个抽象的概念。在JSR133(java规范提案,JSR是Java界的一个重要标准)里提出用来定义一致的、跨平台的内存模型。Java多线程对共享内存(主内存)进行操作的时候,会存在一些如可见性、原子性和有序性的问题,JMM就是围绕着多线程这些特征而建立的模型。而JMM定义了一些语法集,而这些语法集映射到Java语言的volatile、synchronized等关键字,来保证jvm在内存上数据读写的规则。其中需要保证三个特性:

  1. 原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。类比事务原子性;
  2. 可见性:工作内存与主存间存在同步延迟,可见性要求线程对共享变量修改后,其他线程能立刻感知到该变量的修改,即写操作对后续的读操作是可见的。除volatile关键字能实现可见性外,还有synchronized,Lock;
  3. 有序性:单线程下表现为串行执行,也就是在满足happens-before原则下从前往后,依次执行。但在多线程情况下,由于指令重排导致线程乱序执行。

另外,多线程对主存上变量(跨线程的共享变量)进行读写访问时,通常时copy一份变量副本到工作内存然后进行其他操作。但在多线程情况下会造成破坏上面的特性,因此需要通过使用volatile或加锁的方式来避免线程的不安全现象。

在这里插入图片描述

感兴趣的小伙伴可以参考文章👉

【对线面试官】Java内存模型为什么存在?
【对线面试官】深入浅出Java内存模型

13. Volatile

1)Volatile实现原理
public class VolatileVisibilityDemo {

    public static void main(String args []) {

        // 资源类
        MyData myData = new MyData();

        // AAA线程 实现了Runnable接口的,lambda表达式
        new Thread(() -> {

            System.out.println(Thread.currentThread().getName() + "\t come in");

            // 线程睡眠3秒,假设在进行运算
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改number的值
            myData.addTo60();

            // 输出修改后的值
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);

        }, "AAA").start();

        // main线程就一直在这里等待循环,直到number的值不等于零
        while(myData.number == 0) {}

        // 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
        // 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
        // 由于没有volatile修饰MyData类的成员变量number,main线程将会卡在while(myData.number == 0) {},不能正常结束
        // 若想正确结束,用volatile修饰MyData类的成员变量number
        System.out.println(Thread.currentThread().getName() + "\t mission is over");

    }


    /**
     * 假设是主物理内存
     */
    static class MyData {

        /**
         * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
         */
        volatile int number = 0;
        // int number = 0;

        // synchronized仅能保证原子性和有序性,但是可见性无法保证
        public synchronized void addTo60() {
            this.number = 60;
        }
    }
}

试问上述代码变量number 如果不加volatile修饰关键字,效果会怎么样?

在这里插入图片描述

虽然main线程创建的AAA线程修改了number的值,但由于主存中存放的值为0,AAA线程修改后的值并没有立刻可见,也就是不能及时的刷新到主存中(思考:不加volatile什么时候将会更新主存的值呢?),因此可以看出mission is over一直没打印出来,main处于持续的死循环中。显然,这不满足可见性的要求。

当我们加上volatile便能正常运行!!!

volatile原理:volatile修饰的变量是通过lock前缀指令,通过在读写操作前后添加内存屏障来解决可见性、有序性(这块的有序性,我理解应该是建立在满足可见性,且没有重排序的基础上来说的,具体还得分析。不过java多线程在满足这两个条件后,就能基本保证有序性的要求了)问题。内存屏障的作用有两点:禁止指令重排序来保证有序性;修改后的共享变量会立即同步到主内存中,同时让其他线程的本地内存(缓冲区/高速缓存)中该变量失效,其他线程访问共享变量时需要再从主内存加载,从而避免工作内存(缓冲区/高速缓存)与主存间的同步延迟问题。

那么volatile能否保证原子性呢?试着执行下面代码

public class VolatileAtomicityDemo {

    public static void main(String[] args) {
        MyData2 myData = new MyData2();

        // 创建10个线程,线程里面进行1000次循环
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
        // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
        while(Thread.activeCount() > 2) {
            // yield表示不执行
            Thread.yield();
        }

        // 查看最终的值
        // 假设volatile保证原子性,那么输出的值应该为:  20 * 1000 = 20000
        System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);

    }

    static class MyData2 {
        /**
         * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
         */
        volatile int number = 0;


        public void addPlusPlus() {
            number ++;
        }
    }

}

在这里插入图片描述

从上面代码可以看到number的值并没有像预计的那样累加到20000,因此可以得出volatile并不能保证原子性。主要原因在于 i++ 这个操作本身不是原子性的(等效于i = i + 1 可以分为读取 i,执行 i+1 操作,把 i+1 赋值给 i 这三个操作), 如果两个线程A和B,他们都读取了主存中的 i 值,A进行了 i+1 的操作,之后被阻塞,B又进行了 i+1 的操作,之后将新值赋值给 i,i 值被刷新回主存,此时由于A已经执行完了 i+1 操作,所以即使主存中的i值改变了,缓存一致性原则将A中的 i 变为新值,但是这个 i 值的改变也不会影响它将之前执行完的 i+1 得到的值赋给 i这一步,同样导致了最后的结果出错重要的点在于是否执行了 i+1 操作,如果只是读取了 i 值,在进行下一步操作之前,主存中的 i 已经变化了,那么我觉得还是会在缓存一致性原则下,刷新已经读取过的值。(如果可见性在+1之前发生就不会造成写覆盖,最终的结果出错,根本原因还是i++不是原子操作)

2)double-checked locking问题

下面代码使用单例模式用的懒汉式写法,简单应用下volatile关键字:

public class Singleton implements Serializable {
    //懒汉式:某些编译器可能发生指令重排,先instance指向内存空间,后初始化对象,导致后来的线程会获得未初始化的对象
    /*private static volatile Singleton instance;    //避免指令重排
    private Singleton(){
        //利用异常保证反射安全
        if (Singleton.instance != null){
            throw new IllegalStateException("该对象已创建!");
        }
    }
    public static Singleton getInstance(){
        //同步代码块,doubleChecking保证线程安全 双重校验可以减少同步次数
        // 使用双检锁的原因:首先要明白单线程下的单例模式必须要有一个if判断才能保证单例,多线程下synchronized放在if判断外及方法上,
        // 能保证线程安全但是锁的粒度太大,因此加锁前进行额外一次的判断操作,确保对象被创建出来后后续线程继续去竞争锁,进而提高并发度。
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    // 双检锁并不一定保证线程安全,因为还存在指令重排。对象初始化过程如下:
                    // 1、分配内存空间
                    // 2、初始化对象
                    // 3、将对象引用赋值给instance
                    // 会导致2、3执行顺序交换,破坏单例
                    instance = new Singleton();
                    System.out.println("已创建!");
                }
            }
        }
        return instance;
    }
    // 保证反射安全
    private Object readResolve(){
        return SingletonHolder.instance;
    }    
}

单例模式必须通过double-checking(加锁)及volatile 的方式才能确保线程安全。

14. happens-before、as-if-serial

as-if-serial:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、处理器和系统内存都必须遵守as-if-serial语义。所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。

happens-before:JMM通过happens-before关系对外提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见,即B线程能够立刻感知到A线程的修改操作)

具体规则:

  1. 程序次序规则:在一个线程中,按照代码的顺序,前面的操作happens-before于后面的任意操作。
  2. volatile变量规则:对一个volatile变量的写操作,happens-before于后续对这个变量的读操作。
  3. 传递规则: 如果A happens-before B,并且B happens-before C,则A happens-before C。
  4. 锁定规则: 对一个锁的解锁操作 happens-before于后续对这个锁的加锁操作。
  5. 线程启动规则:如果线程A调用线程B的start()方法来启动线程B,则start()操作happens-before于线程B中的任意操作。
  6. 线程终结规则:线程A等待线程B完成(在线程A中调用线程B的join()方法实现),当线程B完成后(线程A调用线程B的join()方法返回),则线程A能够访问到线程B对共享变量的操作。
  7. 线程中断规则:对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
  8. 对象终结原则:一个对象的初始化完成happens-before于它的finalize()方法的开始。

更多>>👉Java并发编程之happens-before和as-if-serial语义

15. 原子类型

15.1 原子整数

java.util.concurrent.atomic并发包提供了一些并发工具类,这里把它分成五类:
使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

上面三个类提供的方法几乎相同,所以我们将以 AtomicInteger 为例子来介绍。通过观察源码可以发现,AtomicInteger 内部都是通过cas的原理来实现的。

在这里插入图片描述

常用api包括:

    public static void main(String[] args) {
        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));
    }

15.2 原子引用

为什么需要原子引用类型?保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人)。
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起。
1)AtomicReference

先看如下代码的问题:

class DecimalAccountUnsafe implements DecimalAccount {
    BigDecimal balance;
    public DecimalAccountUnsafe(BigDecimal balance) {
        this.balance = balance;
    }
    @Override
    public BigDecimal getBalance() {
        return balance;
    }
    // 取款任务
    @Override
    public void withdraw(BigDecimal amount) {
        BigDecimal balance = this.getBalance();
        this.balance = balance.subtract(amount);
    }
}

当执行 withdraw 方法时,可能会有线程安全,我们可以加锁解决或者是使用无锁的方式 CAS 来解决,解决方式是用 AtomicReference 原子引用解决。
代码如下

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 preVal = balance.get();
            BigDecimal nextVal = preVal.subtract(amount);
            if(balance.compareAndSet(preVal, nextVal)) {
                break;
            }
        }
    }
}
2)ABA 问题

看如下代码:

	public static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        String preVal = ref.get();
        other();
        TimeUnit.SECONDS.sleep(1);
        log.debug("change A->C {}", ref.compareAndSet(preVal, "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();
    }

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况,如果主线程希望:只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。使用AtomicStampedReference来解决。

3)AtomicStampedReference

使用 AtomicStampedReference 加 stamp (版本号或者时间戳)的方式解决 ABA 问题。代码如下:

// 两个参数,第一个:变量的值 第二个:版本号初始值
    public static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        String preVal = ref.getReference();
        int stamp = ref.getStamp();
        log.info("main 拿到的版本号 {}",stamp);
        other();
        TimeUnit.SECONDS.sleep(1);
        log.info("修改后的版本号 {}",ref.getStamp());
        log.info("change A->C:{}", ref.compareAndSet(preVal, "C", stamp, stamp + 1));
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            int stamp = ref.getStamp();
            log.info("{}",stamp);
            log.info("change A->B:{}", ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            int stamp = ref.getStamp();
            log.info("{}",stamp);
            log.debug("change B->A:{}", ref.compareAndSet(ref.getReference(), "A",stamp,stamp + 1));
        }).start();
    }
4)AtomicMarkableReference

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A ->C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference 。

15.3 原子数组

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray :引用类型数组原子类

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍,代码如下:

public class Code_10_AtomicArrayTest {

    public static void main(String[] args) throws InterruptedException {
        /**
         * 结果如下:
         * [9934, 9938, 9940, 9931, 9935, 9933, 9944, 9942, 9939, 9940]
         * [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
         */
        demo(
                () -> new int[10],
                (array) -> array.length,
                (array, index) -> array[index]++,
                (array) -> System.out.println(Arrays.toString(array))
        );
        TimeUnit.SECONDS.sleep(1);
        demo(
                () -> new AtomicIntegerArray(10),
                (array) -> array.length(),
                (array, index) -> array.getAndIncrement(index),
                (array) -> System.out.println(array)
        );
    }

    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer) {
        ArrayList<Thread> ts = new ArrayList<>(); // 创建集合
        T array = arraySupplier.get(); // 获取数组
        int length = lengthFun.apply(array); // 获取数组的长度
        for(int i = 0; i < length; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach((thread) -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        printConsumer.accept(array);
    }

}

使用原子数组可以保证元素的线程安全。

15.4 字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

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

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

代码如下:

public class Code_11_AtomicReferenceFieldUpdaterTest {

    public static AtomicReferenceFieldUpdater ref =
            AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");

    public static void main(String[] args) throws InterruptedException {
        Student student = new Student();

        new Thread(() -> {
            System.out.println(ref.compareAndSet(student, null, "list"));
        }).start();
        System.out.println(ref.compareAndSet(student, null, "张三"));
        System.out.println(student);
    }

}

class Student {

    public volatile String name;

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

字段更新器就是为了保证类中某个属性线程安全问题。

15.5 原子累加器

1)AtomicLong Vs LongAdder
	public static void main(String[] args) {
        for(int i = 0; i < 5; i++) {
            demo(() -> new AtomicLong(0), (ref) -> ref.getAndIncrement());
        }
        for(int i = 0; i < 5; i++) {
            demo(() -> new LongAdder(), (ref) -> ref.increment());
        }
    }

    private static <T> void demo(Supplier<T> supplier, Consumer<T> consumer) {
        ArrayList<Thread> list = new ArrayList<>();

        T adder = supplier.get();
        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 4; i++) {
            list.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    consumer.accept(adder);
                }
            }));
        }
        long start = System.nanoTime();
        list.forEach(t -> t.start());
        list.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start)/1000_000);
    }

执行代码后,发现使用 LongAdder 比 AtomicLong 快2,3倍,使用 LongAdder 性能提升的原因很简单,就是在有竞争时,设置多个累加单元(但不会超过cpu的核心数),Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

16. LongAdder 原理

LongAdder 类有几个关键域
public class LongAdder extends Striped64 implements Serializable {}
下面的变量属于 Striped64 被 LongAdder 继承。

// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy; 
1)使用 cas 实现一个自旋锁
public class Code_13_LockCas {

    public AtomicInteger state = new AtomicInteger(0); // 如果 state 值为 0 表示没上锁, 1 表示上锁

    public void lock() {
        while (true) {
            if(state.compareAndSet(0, 1)) {
                break;
            }
        }
    }

    public void unlock() {
        log.debug("unlock...");
        state.set(0);
    }

    public static void main(String[] args) {
        Code_13_LockCas lock = new Code_13_LockCas();
        new Thread(() -> {
            log.info("begin...");
            lock.lock();
            try {
                log.info("上锁成功");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        new Thread(() -> {
            log.info("begin...");
            lock.lock();
            try {
                log.info("上锁成功");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }

}

2)原理之伪共享

其中 Cell 即为累加单元

// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
    final boolean cas(long prev, long next) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
    // 省略不重要代码
}

下面讨论 @sun.misc.Contended 注解的重要意义
得从缓存说起,缓存与内存的速度比较

在这里插入图片描述

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。缓存离 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 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

在这里插入图片描述

3)add 方法分析

LongAdder 进行累加操作是调用 increment 方法,它又调用 add 方法。

public void increment() {
        add(1L);
    }

第一步:add 方法分析,流程图如下

在这里插入图片描述

源码如下:

public void add(long x) {
        // as 为累加单元数组, b 为基础值, x 为累加值
        Cell[] as; long b, v; int m; Cell a;
        // 进入 if 的两个条件
        // 1. as 有值, 表示已经发生过竞争, 进入 if
        // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
        // 3. 如果 as 没有创建, 然后 cas 累加成功就返回,累加到 base 中 不存在线程竞争的时候用到。
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // uncontended 表示 cell 是否有竞争,这里赋值为 true 表示有竞争
            boolean uncontended = true;
            if (
                // as 还没有创建
                    as == null || (m = as.length - 1) < 0 ||
                            // 当前线程对应的 cell 还没有被创建,a为当线程的cell
                            (a = as[getProbe() & m]) == null ||
       // 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
                            !(uncontended = a.cas(v = a.value, v + x))
            ) {
                // 当 cells 为空时,累加操作失败会调用方法,
                // 当 cells 不为空,当前线程的 cell 创建了但是累加失败了会调用方法,
                // 当 cells 不为空,当前线程 cell 没创建会调用这个方法
                // 进入 cell 数组创建、cell 创建的流程
                longAccumulate(x, null, uncontended);
            }
        }
    }

第二步:longAccumulate 方法分析,流程图如下:

在这里插入图片描述
源码如下:

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
        if ((h = getProbe()) == 0) {
            // 初始化 probe
            ThreadLocalRandom.current();
            // h 对应新的 probe 值, 用来对应 cell
            h = getProbe();
            wasUncontended = true;
        }
        // collide 为 true 表示需要扩容
        boolean collide = false;
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            // 已经有了 cells
            if ((as = cells) != null && (n = as.length) > 0) {
                // 但是还没有当前线程对应的 cell
                if ((a = as[(n - 1) & h]) == null) {
                    // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
                    // 成功则 break, 否则继续 continue 循环
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    // 判断槽位确实是空的
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                }
                // 有竞争, 改变线程对应的 cell 来重试 cas
                else if (!wasUncontended)
                    wasUncontended = true;
                    // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
                else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                    break;
                    // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
                else if (n >= NCPU || cells != as)
                    collide = false;
                    // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
                else if (!collide)
                    collide = true;
                    // 加锁
                else if (cellsBusy == 0 && casCellsBusy()) {
                    // 加锁成功, 扩容
                    continue;
                }
                // 改变线程对应的 cell
                h = advanceProbe(h);
            }
            // 还没有 cells, cells==as是指没有其它线程修改cells,as和cells引用相同的对象,使用casCellsBusy()尝试给 cellsBusy 加锁
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
                // 成功则 break;
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // 上两种情况失败, 尝试给 base 使用casBase累加
            else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
        }
    }
4)sum 方法分析

获取最终结果通过 sum 方法,将各个累加单元的值加起来就得到了总的结果。

public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

17. Unsafe

1)Unsafe 对象的获取

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。LockSupport 的 park 方法,cas 相关的方法底层都是通过Unsafe类来实现的。

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
		// Unsafe 使用了单例模式,unsafe 对象是类中的一个私有的变量 
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        
    }
2)Unsafe 模拟实现 cas 操作
public class Code_14_UnsafeTest {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

        // 创建 unsafe 对象
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);

        // 拿到偏移量
        long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

        // 进行 cas 操作
        Teacher teacher = new Teacher();
        unsafe.compareAndSwapLong(teacher, idOffset, 0, 100);
        unsafe.compareAndSwapObject(teacher, nameOffset, null, "lisi");

        System.out.println(teacher);
    }

}

@Data
class Teacher {

    private volatile int id;
    private volatile String name;

}
3)Unsafe 模拟实现原子整数
public class Code_15_UnsafeAccessor {

    public static void main(String[] args) {
        Account.demo(new MyAtomicInteger(10000));
    }
}

class MyAtomicInteger implements Account {

    private volatile Integer value;
    private static final Unsafe UNSAFE = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = UNSAFE.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    public MyAtomicInteger(Integer value) {
        this.value = value;
    }

    public Integer get() {
        return value;
    }

    public void decrement(Integer amount) {
        while (true) {
            Integer preVal = this.value;
            Integer nextVal = preVal - amount;
            if(UNSAFE.compareAndSwapObject(this, valueOffset, preVal, nextVal)) {
                break;
            }
        }
    }

    @Override
    public Integer getBalance() {
        return get();
    }

    @Override
    public void withdraw(Integer amount) {
        decrement(amount);
    }
}

18. 共享模型之不可变

18.1 日期转换的问题

    public static void main(String[] args) {
        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 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类 DateTimeFormatter

	DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
                log.debug("{}", date);
            }).start();
        }

18.2 不可变设计

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

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

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

public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        // 上面是一些校验,下面才是真正的创建新的String对象
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

发现其内部是调用 String 的构造方法创建了一个新字符串

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;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        // 上面是一些安全性的校验,下面是给String对象的value赋值,新创建了一个数组来保存String对象的值
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】。通过拷贝副本的方式保证每线程仅对副本操作,就能确保线程安全。

18.3 模式之享元

简介定义英文名称:Flyweight pattern。 当需要重用数量有限的同一类对象时,归类为:Structual patterns(属于结构型模式的一种设计模式)。

包装类

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

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

Byte, Short, Long 缓存的范围都是 -128~127
Character 缓存的范围是 0~127
Integer 的默认范围是 -128~127,最小值不能变,但最大值可以通过调整虚拟机参数 "-Djava.lang.Integer.IntegerCache.high "来改变
Boolean 缓存了 TRUE 和 FALSE
String 池
参考如下文章:JDK1.8关于运行时常量池, 字符串常量池的要点
BigDecimal、BigInteger

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值