并发编程原理解析

并发编程概述

1 并发和并行,线程和进程

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并发和并行不是完全等同的概念,它们可以同时存在,也可以单独存在。并发强调的是多个任务在时间上交替执行,而并行强调的是多个任务在物理上同时执行。

线程的定义
  现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

2 并发编程的优势和挑战

2.1 多线程的优势

2.1.1 更多的处理器核心

        线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的执行效率。相反,如果该程序使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。

2.1.2 更快的响应时间

        有时我们会编写一些较为复杂的代码(这里的复杂不是说复杂的算法,而是复杂的业务逻辑),例如,一笔订单的创建,它包括插入订单数据、生成订单快照、发送邮件通知卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快地完成呢?
        在上面的场景中,可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验。

2.2 多线程的挑战

2.2.1 上下文切换

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个 任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这 个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。由于上下文切换带来的性能开销,当线程数量超过一定的阈值,会导致CPU时间片频繁切换,进而影响整个系统的性能。减少上下文切换的方法有:

无锁并发编程: 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
CAS算法: Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
使用最少线程: 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这 样会造成大量线程都处于等待状态。
协程: 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

2.2.2 死锁

是指在多线程编程中,两个或多个线程在执行过程中因竞争资源而造成的一种相互等待的现象,导致这些线程都无法继续执行。死锁是多线程编程中常见的问题之一,会导致系统性能下降,甚至崩溃。‌

1.2.3 资源限制 

        资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。
        在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
        对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
        如何在资源限制的情况下,让程序执行得更快呢?方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。

3 线程的创建和启动方式

继承Thread类

public class ThreadDemo {
    public static void main(String[] args) {
        //1. 创建出来
        Worker worker = new Worker();
        //2. 设置名字
        worker.setName("老王");
        //3. 启动线程
        worker.start();
        System.out.println("主线程执行完成");
    }
    static class Worker extends Thread{
        @Override
        public void run() {
            for (int i = 1; i <= 3000; i++) {
                System.out.printf("正在打第%s个螺丝%n", i);
            }
        }
    }
}

实现Runnable接口

public class RunnableDemo {

    public static void main(String[] args) {
        //1. 创建任务
        DaluosiTask daluosiTask = new DaluosiTask();
        //2. 我把一个打螺丝的任务交给了worker1
        Thread worker1 = new Thread(daluosiTask);
        worker1.setName("worker1");
        worker1.start();
        System.out.println("打了多少螺丝了");
    }

    static class DaluosiTask implements Runnable{
        @Override
        public void run() {
            for (int i = 1; i <= 3000; i++) {
                System.out.printf("%s正在打第%s个螺丝%n", Thread.currentThread().getName(), i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

实现Callable接口,使用FutureTask运行

public class FutureTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        DaluosiTask daluosiTask = new DaluosiTask();
        FutureTask<String> daluosiFutureTask1 = new FutureTask<>(daluosiTask);

        Thread worker1 = new Thread(daluosiFutureTask1);
        worker1.setName("worker1");
        worker1.start();
        System.out.printf("worker1打螺丝是否成功: %s%n", daluosiFutureTask1.get());
    }

    static class DaluosiTask implements Callable<String> {
        @Override
        public String call() {
            for (int i = 1; i <= 300; i++) {
                System.out.printf("%s正在打第%s个螺丝%n", Thread.currentThread().getName(), i);
            }
            return "300个螺丝打完了";
        }
    }
}

使用线程池

public class ExecutorDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        DaluosiTask daluosiTask = new DaluosiTask();
        Future<String> daluosiTaskResult = threadPoolExecutor.submit(daluosiTask);
        System.out.println(daluosiTaskResult.get());
    }

    static class DaluosiTask implements Callable<String> {
        @Override
        public String call() {
            for (int i = 1; i <= 3000; i++) {
                System.out.printf("%s正在打第%s个螺丝%n", Thread.currentThread().getName(), i);
            }
            return "3000个螺丝打完了";
        }
    }
}

实现线程的方式总结: 低端一点的回答,4种。中端:1种, Start()。高端一点的回答:JVM没有创建线程的能力

严谨来说Java是不能创建线程的。JVM本质上和 QQ,微信这种应用软件是差不多的,它仅仅是一个应用程序。线程本身不 是JVM创建的,是JVM调用了操作系统,然后操作系统让CPU来创建的。

4 线程的状态及状态转换

一般来说,在Java中,线程的状态一共是6种状态,分别是:

NEW: 初始状态,线程被构建,但是还没有调用start方法。
RUNNABLED: 运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED: 阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况:
  等待阻塞: 运行的线程执行wait方法,jvm会把当前线程放入到等待队列;
  同步阻塞: 运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中;
  其他阻塞: 运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,JVM会把当前线程设置为阻塞状态,当sleep结束、join线程终止、io处理完毕则线程恢复。
WAITING: 等待状态。
TIME_WAITING: 超时等待状态,超时以后自动返回。
TERMINATED: 终止状态,表示当前线程执行完毕。

5 线程的终止方式

Thread提供了线程的一些操作方法,比如stop、suspend等,这些方法可以终止一个线程或者挂起一个线程,但是这些方法都不建议大家使用.
那在哪些情况下,线程的中断需要外部干预呢?

  • 线程中存在无限循环执行,比如while(true)循环。
  • 线程中存在一些阻塞的操作,比如sleep、wait、join等。

5.1 interrupt方法

当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。线程通过检查自身是否被中断来进行相应,可以通过isInterrupted()来判断是否被中断。
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

6 线程间的通信方式

volatile: 关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
synchronized: 关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
wait/notify: 等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
join: 如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。
Condition: Condition实际上就是J.U.C版本的wait/notify。可以让线程基于某个条件去等待和唤醒。

并发编程安全性的原因及解决方案

线程的三大特性: 原子性、可见性、有序性

并发编程的安全性问题有哪些:

运行的结果是错误的。

线程发布和初始化导致的安全问题。

活跃性的问题, 死锁,活锁,锁饥饿。

1 并发多指令操作导致原子性问题

1.1 原子性问题

1.1.1 原子性问题的现象
public class ConcurrentBugDemo {
    public static int count = 0;
    /**
     * 螺丝+1
     */
    public static void incr() {
        count++;
    }

    /**
     * 创建了三个工人,每个工人打一万个螺丝,刚好三万个
     */
    public static void main(String[] args) throws InterruptedException {

        Thread worker1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                incr();
            }
        });

        Thread worker2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                incr();
            }
        });

        Thread worker3 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                incr();
            }
        });

        worker1.start();
        worker2.start();
        worker3.start();

        worker1.join();
        worker2.join();
        worker3.join();
        System.out.println(count);
    }
}

实际运行结果是小于等于3000.

1.1.2 原子性问题的本质

count++是属于Java高级语言中的编程指令,而这些指令最终可能会有多条CPU指令来组成,而count++最终会生成3条指令。

 

3 通过CAS/同步锁解决原子问题

原子性的解决方案有两种,一种是悲观锁例如synchronized,一种是乐观锁CAS(也叫自旋锁)。由于除了synchronized的偏向锁,其他几乎所有的锁抢锁过程都涉及到CAS。
  CAS(Compare-And-Swap),它是一条CPU并发原语,用于判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。CAS并发原语体现在Java中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一套完全依赖于硬件的功能,通过它实现了原子操作。
  JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

4 编译器指令重排导致有序性问题

CPU它为了提升计算的效率,可能会对编译后的代码做一些 重新排序,来提升代码的性能。这里给大家举一个例子就很 好理解了,比如说送外卖一般都是同时送很多个人的单的, 外卖小哥一般都会制定好计划,第一个单送哪里,第二个单 送哪里,第三个单送哪里能够让行程最短,不要走回头路, 他并不会说按照接单的顺序去一个个送,不然会绕很多弯 路。CPU也是一样的道理,虽然是个机器,但是它也会想办 法说规划出更合理的执行方式。

public class OrderedDemo {
    private static int x=0, y=0, a=0, b=0;

    public static void main(String[] args) throws InterruptedException {
        //计数器
        int i = 0;

        //死循环
        for(;;) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            //1. t1先执行,a=1,x=0, b=1, y=1
            //2. t2先执行,b=1,y=0, a=1, x=1
            /**
             * 3. 交叉执行
             * a = 1;
             * b = 1;
             * y = a;
             * x = b;
             *  y=1, x=1
             *
             */

            /**
             * 4. 指令重排
             * x = b;  x=0;
             * y = a;  y=0;
             * a = 1;
             * b = 1;
             */
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String result = String.format("第%s次:(%s,%s)", i, x, y);

            System.out.println(result);
            if(x == 0 && y == 0) {
                System.out.println(result);
                break;
            }
        }
    }
}

这里我们定义了四个整数,然后写了一个死循环一直执行。 死循环里面创建了两个线程,一个线程里将a赋值为1,将x赋 值b,另一个线程里将b赋值为1,将y赋值为a,最后,我们要 输出x和y的值来。

这里,x和y的值有几种结果输出。

1. 第一种就是t1先执行,那么x赋值的b是0,y呢,因为a=1 已经赋值了,所以y就是1.

2. 第二种就是t2先执行,那么y赋值的a是0,x呢,因为b=1 已经赋值了,所以x就是1.

3. 第三种情况就是t1在执行完a=1的时候,t2开始执行了,那 么此时x和y都是1.

4. 还有最后一种就是发生了指令重排,x=b在a=1之前执行 了,y=a在b=1之前执行了,那么,还会出现的一种情况就 是a和b都还没有开始初始化,x和y就已经完成赋值了,此 时x和y就都是0。就和之前说的那个例子一样,外卖都还 没取餐呢,就已经到送餐的楼下了。

5 从硬件层面分析JVM的可见性、有序性问题

        在整个计算机的发展历程中,除了CPU、内存以及I/O设备不断迭代升级来提升计算机处理性能之外,还有一个非常核心的矛盾点,就是这三者在处理速度的差异。CPU的计算速度是非常快的,其次是内存、最后是IO设备(比如磁盘),也就是CPU的计算速度远远高于内存以及磁盘设备的I/O速度。为了平衡这三者之间的速度差异,最大化的利用CPU。所以在硬件层面、操作系统层面、编译器层面做出了很多的优化:

  1. CPU增加了高速缓存。
  2. 操作系统增加了进程、线程。通过CPU的时间片切换最大化的提升CPU的使用率。
  3. 编译器的指令优化,更合理的去利用好CPU的高速缓存。

5.1 CPU 高速缓存

CPU在做计算时,和内存的IO操作是无法避免的,而这个IO过程相对于CPU的计算速度来说是非常耗 时,基于这样一个问题,所以在CPU层面设计了高速缓存,这个缓存行可以缓存存储在内存中的数据, CPU每次会先从缓存行中读取需要运算的数据,如果缓存行中不存在该数据,才会从内存中加载,通过 这样一个机制可以减少CPU和内存的交互开销从而提升CPU的利用率。

对于主流的x86平台,cpu的缓存行(cache)分为L1、L2、L3总共3级。

5.2 缓存一致性问题的解决方案

CPU高速缓存的出现,虽然提升了CPU的利用率,但是同时也带来了另外一个问题--缓存一致性问题, 这个一致性问题体现在。

在多线程环境中,当多个线程并行执行加载同一块内存数据时,由于每个CPU都有自己独立的L1、L2缓存,所以每个CPU的这部分缓存空间都会缓存到相同的数据,并且每个CPU执行相关指令时,彼此之间不可见,就会导致缓存的一致性问题,具体流程如下图所示:

为了解决缓存不一致的问题,在 CPU 层面做了很多事情, 主要提供了两种解决办法。

总线锁

        什么是总线:负责将各个CPU连接到主内存里,CPU要从内存 里面读数据,写数据都是要通过这个总线。

        什么是总线锁:当我一个CPU在处理数据的时候,其他CPU就 要阻塞住。 总线锁住。

        在多 cpu 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK# 信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,总线锁定的开销比较大,这种机制显然是不合适的。

缓存锁

相比总线锁,缓存锁其实就是降低了总线锁的粒度。核心机制是基于缓存一致性协议来实现的。为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有 MSI、MESI、MOSI 等。最常见的就是 MESI协议。MESI表示缓存行的四种状态,分别是:

  • M(Modify) 表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致。
  • E(Exclusive) 表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改。
  • S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致。
  • I(Invalid) 表示缓存已经失效。

MESI协议执行流程示例:

1. 第一步,E的全拼是Exclusive,表示是独享的,看到第一 个图,这里CPU0将主内存里面的data先加载到CPU的高速 缓存里,此时这个data其实是CPU0独享的,所以我们可 以把它的状态标记为一个E。就像我们刚刚说到的那个计算 加法的例子,你的大脑读到了一个数字,比如说100这个值,如果你发现这个值只有你一个人读取到了,那这个值 其实就是你独享的,这个100只有你知道。

2. 第二步,这里CPU2也去读取了一下主内存里的data,将这个data也存了一份在自己的缓存里。这个时候CPU0会根据这个总线了解到CPU2的行为,然后CPU0就知道了这 个data数据已经不是它的独享了,其他CPU也有了这个 data数据,它就会把这个data的状态修改为S状态,S的全拼是Shared,意思就是共享的意思。

3. 第三步,当CPU0将缓存里的data修改了的时候,此时, 这个CPU0缓存里的data又会修改为E状态,独享状态,然后CPU2缓存里的data此时就会通过总线机制获取到CPU0 的操作,这时CPU2发现它的数据不是最新的,就会给它的 data标记一个I状态,也就是Invaildated,失效状态。

4. 第四步,最后如果我们对一个独享状态的data进行修改的 话,那么这个data的状态就会更新为M状态,也就是 Modified,修改状态。

MESI演示:

网站演示:https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm

网站分析:

1. 第一个部分,这个MEMORY就是主内存。

2. 然后就是data bus,address bus,shared,这些我们不 用关心他们是什么,只要知道这几个就是总线,内存和 CPU交互的时候就需要通过这个总线来进行交互。

3. 然后就是下面的CPU0,CPU1, CPU2,这里其实就是模拟 了一个三核的CPU,然后每个CPU都有一个对应的高速缓 存。

4. 这里的CPU里的read a0, write a0是可以直接点击的,比 如说点一下read a0,其实就是模拟了一个线程去读取一个 a0的变量,点击write a0呢,就是对这个a0进行一个+1的 操作。

操作步骤:

1. [CPU0: read a0] 首先第一步,我们先让CPU0来读取一下 a0这个数据,此时我们可以看到,它先从本地缓存里获 取,此时没有这个数据,那么它就通过地址总线去其他 CPU和主内存里面获取看看有没有这个a0,此时本地内存 里是存在的,那么本地内存就会通过数据总线把a0=0的数 据发过来,此时CPU0的缓存收到数据后就会写入到缓存 里,并且将状态标记为E状态,也就是Exclusive独享状 态。因为其他的CPU里面没有这个a0嘛,这个a0此时就是 CPU0的独享数据。

2. [CPU0: read a0] 这里我们再执行一下read a0,看看这个 时候它会去哪里读。我们点击一下,此时可以看到,这个 read a0的操作直接在缓存里面就把这个数据拿到后,就直 接返回了! 这其实就体现出缓存的作用了,此时它不用再 去和总线交互,不用再去和其他的CPU进行交互,不再需 要和主内存进行交互,此时它的性能其实就会提升不少。

3. [CPU0: write a0] 这一步操作我们要开始写数据了,我们 点击write a0,给a0做一个加1的操作。此时,我们可以看 到有两个变化,第一个变化就是a0的值从0变成了1,然后 a0的状态从E,Exclusive独享状态变成了M,也就是 Modified,之前我们就讲到了,如果我们对一个独享状态 的数据进行修改的话,那么这个data的状态就会更新为修 改状态。 还有一个变化呢,就是主内存,主内存里的a0 变成了一个深灰色,这个意思就是a0在主内存已经是失效 状态了。

4. [CPU0: write a0]再来写一次a0,其实也还是在CPU0的高 速缓存里进行了修改,并不会影响到其他的CPU或者内 存。这个执行效率是非常非常高的。

5. [CPU1: read a0] 我们让其他的CPU来读取一下a0的值,这 里我们让CPU1来做读取。我们点击CPU1的read a0,此时 我们可以看到它也是先从自己的缓存里获取,获取不到的 时候,就通过地址总线去其他CPU和主内存里获取,这里 它感知到了CPU0的缓存里有a0这个值,它就直接把CPU0 里的a0修改为S共享状态,然后cpu0会通过地址总线将a0 等于2的值写回到主内存和CPU1里面,此时CPU1里的a0 也是共享状态。

6. [CPU1: write a0] 在CPU1里面,我们再点击一下write a0,我们要在CPU1里面给a0做一个加一操作。这时我们 可以看到,首先CPU1会把它的缓存里的a0从2修改为3, 修改完之后,就会通过数据总线将主内存里的a0更新到 3,在这更新的时候,同时也会将这个缓存里的a0从S状态 修改为E的独享状态。 然后这个地址总线还会将CPU0缓存 里的a0修改为I状态,也就是invalidated失效状态。

7. [CPU1: write a0] 当我们再在这个CPU1里面进行自增操 作,它其实就只会修改CPU1自己缓存里的数据,将值从3 修改为4,并将状态修改为modified修改状态。它不会去 干扰其他的CPU和主内存了。性能是非常高的。

8. [CPU0: write a0] 这时,我们看到CPU0里面的a0是失效状 态的,那么我们如果要对这个失效的a0进行加一操作会怎样呢。我们来试一下看啊。 这里我们可以看到,虽然命 中了这个缓存,但是因为这个缓存是失效的,所以这里还 是要通过地址总线去其他CPU和主内存里面看看有没有这 个a0。此时,它发现CPU1的缓存里有这个值,那么CPU1 他就会通过数据总线,将主内存里的a0进行更新,以及 CPU0的缓存进行更新,此时是将CPU0里的a0修改为4, 并且状态都是共享状态。然后呢,CPU0会马上将a0做一 个加一操作,就从4变为了5,它又会马上通过总线将主内 存里的a0更新为最新,然后将CPU1缓存里的a0从S状态修 改为了I失效状态。

这个执行流程还是有点问题的。就比如说我们的CPU在read a0的时候,它是要去其他的CPU获取,去主内存里面获取, 这个时候,这个CPU其实是串行执行的。假如咱们同时定义 了a0和a1这两个变量,那么加载的时候,就必须是先加载 a0,再加载a1,这样的同步方式其实就会导致性能低不少。

所以,这里又要引入两个新的概念,一个是写缓存,一个是 无效队列

假如CPU0要执行一段代码,就是这里的int a = 0; int b = 1; 当CPU0在写a = 0这条数据的时候,是需要通过BUS总线发送 失效消息让其他CPU里的a值失效,并且是需要收到失效消息 的ack确认之后,CPU0才能执行完a = 0这个操作,这个操作 其实是阻塞的,它必须先执行第一行a=0,才能再执行b=1这 一行,这种阻塞执行的方式,就让这个效率特别低下了。

引入了写缓冲和无效队列后:

其实很好理解,它就是一个同步到异步的优化,异步操作性能肯定是比同步高。

这里我们通过写缓冲区和无效队列,在执行int a = 0;还没执 行完的时候,int b = 1就执行了,这个问题就是指令重排。

volatile底层是直接加屏障来解决有序性问题的。 内存屏障的 作用就是在操作之间建立一道屏障,屏障前的所有指令是可 以重新排序的,屏障之后的指令也是可以重新排序的,但是 啊,他们重排序的时候是不能越过内存屏障的。就像进厂打 螺丝,你坐在流水线上哪个位置都行,因为打螺丝的位置是 没有屏障的。但是,如果你要坐到领导的办公室里去打,那 肯定是不行的,因为打工仔和领导之间是有屏障的,你不能 随便排座!

之前我们讲到的那个重排序的例子,就是int a = 0; int b = 1; 的那个,其实JVM就是帮我们在a=0和b=1中间加了一个内存 屏障,加了这个屏障之后,b=1就必须等a=0执行完成之后它 才能执行,这样其实就保证了这个代码执行的有序性。

这个内存屏障又是怎么加的呢,其实就是,当我们这个代码 里的变量被volatile修饰之后啊,JVM在把字节码生成机器码 的时候,发现操作的是volatile变量的话,它就会根据JVM的 要求,会在相对应的位置去添加一个内存屏障的指令。就像 工厂一样,在工厂建立起来的时候啊,领导就会把车间,流 水线,办公室,厕所这些区域的位置规划好,每个区域之间 是有屏障的,你不能随意去跨越。

这个内存屏障呢,是有四种。这里有个图大家可以参考一 下。

1. 第一种是LoadLoad屏障,这是在前后都是读操作的时候加 的一个屏障,保证loadload之前的读取操作在loadload之 后的操作之前执行。

2. 第二种是StoreStore屏障,这是在前后都是写操作的时候 加的一个屏障,意思是在storestore之后的的写操作执行 前,保证storestore之前的写操作已经刷新到主内存。

3. 第三种是StoreLoad屏障,这个就是在前面是一个写操 作,后面是个读操作,意思是在storeload之前的写操作已 经刷新到主内存之后,storeload之后的读操作才能正常执行。

4. 第四种就是LoadStore屏障了,这个就是在前面是一个读 操作,后面是一个写操作,意思是在loadstore之后的写操 作执行前,必须保证loadStore之前的读操作已经结束。

MESI失效的场景:

  1. CPU不支持缓存一致性协议。
  2. MESI是对单独一个缓存行进行加锁,此时如果这个数据的大小超出了一个缓存行的大小,那么也会无效。

        需要注意的是,缓存锁与缓存一致性协议并不是相同的,在《Intel® 64 and IA-32 Architectures Software Developer’s Manual》中原文如下(详见第六章):通过lock前缀指令,会锁定变量缓存⾏区域并写回主内存,这个操作称为“缓存锁定”,缓存⼀致性机制会阻⽌同时修改被两个以上处理器缓存的内存区域数据。⼀个处理器的缓存回写到内存会导致其他处理器的缓存⽆效。 说明缓存锁的作用是让缓存中的数据立刻写入内存,进而通过MESI协议让其他CPU的缓存立即失效。并且还有一个非常重要的区分就是,MESI是CPU自动实现的,对任何变量都会生效。而缓存锁或者总线锁只对加了汇编指令Lock前缀的变量生效(volatile的关键作用之一就是在这个变量前面加入了Lock前缀)。

5.3 伪共享

        缓存是由缓存行组成的,通常是64字节(常用处理器的缓存行是64字节的,比较旧的处理器缓存行是32字节的),并且它有效地引用主内存中的一块地址。一个java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。

        在程序运行的过程中,缓存每次更新都从主内存中加载连续的64个字节。因此,如果访问一个long类型的数组时,当数组中的一个值被加载到缓存中时,另外7个元素也会被加载到缓存中。但是,如果使用的数据结构中的项在内存中不是彼此相邻的,比如链表,那么将得不到免费缓存加载带来的好处。

        不过,这种免费加载也有一个问题。如果一个cpu核心的线程在对a进行修改,另一个cpu核心的线程却在对b进行读取。当前者修改a时,会把a和b同时加载到前者核心的缓存行中,更新完a后其它所有包含a的缓存行都将失效,因为其它缓存中的a不是最新值了。而当后者读取b时,发现这个缓存行已经失效了,需要从主内存中重新加载。

        这样就出现了一个问题,b和a完全不相干,每次却要因为a的更新需要从主内存重新读取,它被缓存未命中给拖慢了。这就是传说中的伪共享。

解决伪共享的方式有两种,一种是在两个long类型之间增加缓存行填充,确保这两个变量不在同一个缓存行。这种方式由于p1-p7变量没有真正使用,有可能被JVM优化掉。

另一种就是通过注解@sun.misc.Contended实现(本质是一样的)。通过这种方式需要在JVM启动参数里配置-XX:-RestrictContended才能生效。

   static class Pointer {
        @sun.misc.Contended
        volatile long x;
        volatile long y;
    }

6 缓存一致性协议优化方案及带来新的可见性问题

6.1 Store buffer

缓存的消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。
  比如你需要修改本地缓存中的一条信息,那么你必须将 I(无效)状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。 等待确认的过程会阻塞处理器,这会降低处理器的性能。应为这个等待远远比一个指令的执行时间长的多。
  为了避免这种CPU运算能力的浪费,Store Bufferes被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
  这么做有个问题:当写操作被存入Store buffer并且未刷入缓存时,CPU执行了下一条读指令,就会读到缓存中的脏数据。为了解决这个问题,处理器会尝试从Store buffer中读取值,这个的解决方案称为Store Forwarding,它使得加载的时候,如果Store buffer中存在,则进行返回。
  但是由于Store buffer本身是异步的,就没有办法保证其他CPU准时接收并处理该CPU的失效请求,从而导致自己的脏数据没有被置位I(Invalid)(就算接收并处理了请求将缓存状态更新为I,此时也可能存在新数据未同步到内存,导致从内存中继续读取老数据)。

6.2 Invalid queue

        执行失效也不是一个简单的操作,它需要处理器去处理。此外,存储缓存(Store Buffers)并不是无穷大的, 所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列。它们的约定如下:

  1. 对于所有的收到的Invalidate请求,Invalidate Acknowlege消息 必须立刻发送。
  2. Invalidate并不真正执行,而是 被放在一个特殊的队列中,在方便的时候才会去执行。
  3. 处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate。

6.3 可见性问题的解决方案

由于Store buffer和Invalid queue的存在,使得缓存的同步出现延迟。从用户的角度上讲,看起来就像是指令重排序导致的可见性问题。

 value = 3;
 
 void exeToCPUA(){
 	value = 10;
 	isFinsh = true;
 }
 
 void exeToCPUB(){
 	if(isFinsh){
 		//value一定等于10?!
 		assert value == 10;
	 }
 }

试想一下开始执行时,CPU A保存着finished在E(独享)状态,而value并没有保存在它的缓存中。(例如,Invalid)。在这种情况下,value会比finished更迟地抛弃存储缓存。完全有可能CPU B读取finished的值为true,而value的值不等于10。即isFinsh的赋值在value赋值之前。
  由于处理器并不知道什么时候优化是允许的,而什么时候并不允许。干脆将这个任务丢给了写代码的人。这就是内存屏障(Memory Barriers)。

并发编程基础工具

1 synchronized关键字

1.1 ynchronized基本使用及锁的范围

synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

1.2 Java对象头内存布局解析

这就要引出Markword对象头这个概念了,它是对象头的意思,简单理解,就是一个对象,在JVM内存 中的布局或者存储的形式。

在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据 (Instance Data)、对齐填充(Padding)

  1. mark-word:对象标记字段占4个字节,用于存储一些列的标记位,比如:哈希值、轻量级锁的标 记位,偏向锁标记位、分代年龄等。
  2. Klass Pointer:Class对象的类型指针,Jdk1.8默认开启指针压缩后为4字节,关闭指针压缩( - XX:-UseCompressedOops )后,长度为8字节。其指向的位置是对象对应的Class对象(其对应的 元数据对象)的内存地址。
  3. 对象实际数据:包括对象的所有成员变量,大小由各个成员变量决定,比如:byte占1个字节8比 特位、int占4个字节32比特位。
  4. 对齐:最后这段空间补全并非必须,仅仅为了起到占位符的作用。由于HotSpot虚拟机的内存管理 系统要求对象起始地址必须是8字节的整数倍,所以对象头正好是8字节的倍数。因此当对象实例 数据部分没有对齐的话,就需要通过对齐填充来补全。

通过ClassLayout打印对象头

添加Jol依赖

<dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
</dependency>

 编写测试代码,在不加锁的情况下,对象头的信息打印

public class ClassLayoutDemo {
    Object o=new Object();
    public static void main(String[] args) {
        //构建了一个对象实例
        ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo();
        //假设A线程进入到同步代码. 偏向A线程,
        //存在多个线程来抢占锁, 线程B来抢占锁. 轻量级锁()
        System.out.println(ClassLayout.parseInstance(ClassLayoutDemo.class).toPrintable());
    }
}

1.3 synchronized实现原理

Synchronized到底帮我们做了什么,为什么能够解决原子性呢? 在没有加锁之前,多个线程去调用incr()方法时,没有任何限制,都是可以同时拿到这个i的值进行 ++ 操 作,但是当加了Synchronized锁之后,线程A和B就由并行执行变成了串行执行。

1.4 synchronized锁的优化—锁粗化与锁消除

锁消除是 synchronized 锁的一种较保守的优化策略,通过编译器和JVM判断锁是否可以消除。这里的锁消除只会处理一些直接可以判断,完全不涉及线程安全问题的锁,比如在单线程环境下使用 StringBuffer 类中的方法。

锁粗化

这里有一个锁的粒度的概念,可以这么认为:在锁对象代码块中的代码越少则认为锁的粒度越细,反之则是越粗。

实际开发中,使用细粒度的锁,往往是为了锁可以被其他线程及时获取。但有时,可能很长一段时间都没用其他线程来竞争这个锁。

因此,如果一段逻辑中出现多次加锁解锁,根据编译器和JVM的判断会自动对锁进行粗化。

锁粗化是指将多个细粒度的锁合并为一个粗粒度的锁,可以在特定场景下提高程序的执行效率,减小系统开销。

1.5 synchronized锁的升级—偏向锁、轻量级锁、重量级锁

锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞 争的激烈而逐渐升级。

这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无锁的实现。

  • 默认情况下是偏向锁是开启状态,偏向的线程ID是0,偏向一个Anonymous BiasedLock
  • 如果有线程去抢占锁,那么这个时候线程会先去抢占偏向锁,也就是把markword的线程ID改为当 前抢占锁的线程ID的过程
  • 如果有线程竞争,这个时候会撤销偏向锁,升级到轻量级锁,线程在自己的线程栈帧中会创建一个 LockRecord,用CAS操作把markword设置为指向自己这个线程的LR的指针,设置成功后表示抢 占到锁。
  • 如果竞争加剧,比如有线程超过10次自旋(-XX:PreBlockSpin参数配置),或者自旋线程数超过 CPU核心数的一般,在1.6之后,加入了自适应自旋Adapative Self Spinning. JVM会根据上次竞争 的情况来自动控制自旋的时间。 升级到重量级锁,向操作系统申请资源, Linux Mutex,然后线程被挂起进入到等待队列。
  1. 无锁,顾名思义,就是没有锁,没有任何线程来执行,根 本就没有锁的概念。可以理解成厕所大门没锁,谁都能进 去上厕所。
  2. 偏向锁, 然后当第一个人进去了厕所,也就是第一个线程进 入到了代码区域,他就会加一个偏向锁,这个偏向锁可以 简单理解成这人给厕所大门安装了一把指纹锁,当这个人 下次还想再进测试的时候,就直接判断一下指纹是不是正 确的就了。在代码里其实就是在锁对象的文件头里标记了 一下当前线程的线程ID,这个线程ID因为是唯一的,所以 它其实就相当于一个人的指纹。当这个线程下次再去执行 同步代码块的时候,其实只要简单对比一下自己的线程ID 和对象头里的线程ID是否一致就好了,相等判断的性能还 是非常快的,这样的性能损耗会特别低!偏向锁这个东西 当时是hotspot研发团队做了一个研究,就是通常情况下 的锁一般不会发生竞争,大部分的场景都是同一个线程多 次去获取锁,所以,这种情况下咱们使用偏向锁的性能就 会特别高了!
  3. 轻量级锁,然而在开发中,高并发场景下多线程开发是肯 定存在的,厕所肯定不可能是只有一个人上。假如有很多 人要上厕所,你一个人在厕所大门装一把指纹锁,你猜别 人会不会打你。所以,偏向锁就肯定不行了,先得把这个 指纹锁,也就是偏向锁给拆了,再给安装一个轻量级锁。 这个轻量级锁的原理也很简单,就是你在上厕所的时候, 外面的人会不停的来敲门问你上完了吗,当你说上完了之 后,外面的人会立马冲进来。线程,就是通过自旋的方式 去获取锁,自旋其实就是一个while循环,这样去获取锁的 时候线程是不会阻塞的,所以这样性能也比较高。
  4. 重量级锁, 如果线程锁的竞争非常激烈,那么其实自旋 while循环,一直while循环下去是非常消耗CPU资源的, 不能让资源一直消耗下去,那么就要给这个锁再升级一次 了,升级成重量级锁。这里,重量级锁的控制权就交给操 作系统了,由操作系统,CPU来给线程做调度,这里的线程挂起和唤醒都是系统来做。就相当于厕所里坑位竞争很 激烈,秩序已经无法自行维护了,就必须请一个厕所管理 员来调度这些人上厕所了。

总的来说就是根据竞争的情况来选择对应的锁,避免资源的 浪费

1.5.1 轻量级锁的获取及原理
public class Demo {
    Object o=new Object();
    public static void main(String[] args) {
        Demo demo=new Demo(); //o这个对象,在内存中是如何存储和布局的。
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        synchronized (demo){
            System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        }
    }
}

// 在未加锁之前,对象头中的第一个字节最后三位为 [001], 其中最后两位 [01]表示无锁,第一位[0]也 表示无锁

org.example.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4   java.lang.Object Demo.o                                    (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

// 下面部分是加锁之后的对象布局变化 // 其中在前4个字节中,第一个字节最后三位都是[000], 后两位00表示轻量级锁,第一位为[0],表示当 前不是偏向锁状态。

org.example.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           18 f1 27 03 (00011000 11110001 00100111 00000011) (52949272)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4   java.lang.Object Demo.o                                    (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可是为什么这里明明没有竞争,它的锁的标记是轻量级锁呢? 

1.5.2 偏向锁的获取及原理

默认情况下,偏向锁的开启是有个延迟,默认是4秒。为什么这么设计呢?

因为JVM虚拟机自己有一些默认启动的线程,这些线程里面有很多的Synchronized代码,这些 Synchronized代码启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁的升级和 撤销,效率较低。

通过下面这个JVM参数可以讲延迟设置为0.

XX:BiasedLockingStartupDelay=0

public class Demo {
    Object o=new Object();
    public static void main(String[] args) {
        Demo demo=new Demo(); //o这个对象,在内存中是如何存储和布局的。
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        synchronized (demo){
            System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        }
    }
}

 得到如下的对象布局,可以看到对象头中的的高位第一个字节最后三位数为[101],表示当前为偏向锁 状态。

这里的第一个对象和第二个对象的锁状态都是101,是因为偏向锁打开状态下,默认会有配置匿 名的对象获得偏向锁。

org.example.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4   java.lang.Object Demo.o                                    (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

org.example.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 30 a6 02 (00000101 00110000 10100110 00000010) (44445701)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4   java.lang.Object Demo.o                                    (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

1.5.3 重量级锁的获取 

在竞争比较激烈的情况下,线程一直无法获得锁的时候,就会升级到重量级锁。

仔细观察下面的案例,通过两个线程来模拟竞争的场景

public class Demo {
    public static void main(String[] args) {
        Demo testDemo = new Demo();
        Thread t1 = new Thread(() -> {
            synchronized (testDemo){
                System.out.println("t1 lock ing");
                System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
            }
        });
        t1.start();
        synchronized (testDemo){
            System.out.println("main lock ing");
            System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
        }
    }
}

从结果可以看出,在竞争的情况下锁的标记为 [010] ,其中所标记 [10]表示重量级锁 

 main lock ing
org.example.Demo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           da ca 68 03 (11011010 11001010 01101000 00000011) (57199322)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 lock ing
org.example.Demo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           da ca 68 03 (11011010 11001010 01101000 00000011) (57199322)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2 volatile关键字

volatile的内存语义:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

volatile使用场景举例——DCL

双重检查锁定(Double Check Lock,DCL)是一种常见的单例模式实现手法,其目的是在确保线程安全的前提下,尽可能地提高代码执行的效率。

public class Singleton {
    private volatile static Singleton instance;
 
    private Singleton() {
        // 初始化工作
    }
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile的实际作用

  1. 禁止了编译器优化重排序和指令级并行重排序。
  2. 向CPU发出lock指令,通过总线锁(缓存锁+缓存一致性协议)的方式保证可见性。

gcc编译器在遇到内嵌汇编语句asm volatile(“” ::: “memory”),将以此作为一条内存屏障,重排序内存操作,即此语句之前的各种编译优化将不会持续到此语句之后。

Linux 内核提供函数 barrier()用于让编译器保证其之前的内存访问先于其之后的完成。

3 ThreadLocal

3.1 ThreadLocal是什么及其作用

在 Java 多线程编程中,我们经常会遇到共享变量的并发访问问题。为了解决这个问题,Java 提供了 ThreadLocal 类,它允许我们在每个线程中存储和访问线程局部变量,而不会影响其他线程的数据。

3.2 ThreadLocal使用场景举例

  • 存储用户信息上下文

使用 ThreadLocal,在控制层拦截请求把用户信息存入 ThreadLocal,这样我们在任何一个地方,都可以取出 ThreadLocal 中存的用户数据。

  • 数据库连接池

数据库连接池的连接交给 ThreadLoca 进行管理,保证当前线程的操作都是同一个 Connnection。

3.3 ThreadLocal原理解析

ThreadLocal get方法源码分析

public T get() {
  //1. 首先,它先通过currentThread方法拿到当前线程
  Thread t = Thread.currentThread();
  // 2. 然后通过一个getMap方法将线程t传进去,拿到了一个ThreadLocalMap,
     ThreadLocalMap map = getMap(t);
    //3. ,然后这里因为是第一次获取这个map,thread里的这个map默认又是null,所以这一步流程是执行不了。
     if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
            @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
        //4. 那么代码流程就直接到了最后一步,setInitialValue。
      return setInitialValue();
}

3.4 ThreadLocal内存泄漏原因及解决方案

内存泄露是什么

内存泄露其实就是说,你创建了一个对象,用完了之后,就可以被垃圾回收器回收了,但是它却因为一些原因不能被回 收,JVM回收不到这个垃圾对象。如果这样的对象越来越多的 话,就会导致机器的内存越来越少,最后就会出现OOM的错误。

就好比卫生间的坑位是有限的,就像机器的内存一样。当有 人占了一个坑位之后,其实就是相当于内存被占用了。如果 这个人上完厕所之后正常从大门出来的话,那就是内存被释 放了,但是如果这个人不走寻常路,他不开门,他直接从坑 位边上爬出去;此时,这个坑位是没有被使用了,应该被释 放出来,但是呢,这个坑位里面锁住了,就导致了其他人根 本是没办法去使用这个坑位的。

Key内存泄露

咱们可以看到这个ThreadLocal内存结构图,这个图里我们 可以看到,当我们把炒饭这个ThreadLocal置为null的时候, 其实就是将这里的第一步连接给断掉。虽然第一步是断掉 了,但是,第五步的连接还是没断掉的。张三线程里map的 entry的key是引用了这个对象的,李四的线程里的map的 entry的key也是引用了这个对象的。所以呢,因为存在引 用,垃圾回收器就没办法去回收这个对象。

但是呢,ThreadLocal这个类它还是比较强的。它是有办法自 己去把这个对象回收掉的。 看到Entry这个数据结构,可以看到,它是继承了 WeakReference的类,字面意思就是弱引用的意思。

static class Entry extends
WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

强引用,简单来说,就是我们每次通过new创建的对象都是强 引用,只要某个对象和强引用对象有关联,JVM一定不会回收 这个对象,就算内存不足了都不会回收,它直接就抛一个 OOM的异常出来了。 

弱引用意思就是说如果有对象只被弱引用的对象关联,那么 只要JVM一运行垃圾回收,不管JVM的内存是不是足够,这个 对象都会被回收。 因为Entry是WeakReference,这个ThreadLocal呢就是和这 个WeakReference关联的! 所以,只要垃圾回收一运行,这 个ThreadLocal就能顺利被回收了。 这里使用WeakReference只是将key的内存泄漏问题解决了。 那么value其实也有内存泄漏的问题。 正常情况下线程停止的时候,value还是能被正常的垃圾回收 的,但是有时候线程的生命周期是特别长的,如果这个线程 一直不会停,然后value这个值就一直不会被正常回收。

垃圾回收算法里面有一种叫可达性分析的算法。栈里面的变 量没被淘汰的话,那么它下面通过这个链路能够找到的对象 都是不能被回收的。 这里我们可以看到张三这个线程关联了 Thread对象,Thread对象里面又有ThreadLocalMap, ThreadLocalMap里面又关联了Entry,Entry里面又关联了蛋 炒饭这个String对象。所以,value在线程没有停止的时候是 不会被回收的。

内存泄露的案例

public class ThreadLocalOutOfMemoryDemo {

    /**
     * 一个10MB的对象
     */
    static class DataEntity {
        private final byte[] byteArr = new byte[1024 * 1024 * 10];
    }

    /**
     * 一个线程数量为6的线程池
     */
    final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(
            6,
            6,
            3,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    /**
     * 一个存储DataEntity 10MB对象的ThreadLocal
     */
    static ThreadLocal<DataEntity> dataEntityTl = new ThreadLocal<DataEntity>();

    public static void main(String[] args) throws InterruptedException {
        // for循环6次,提交6次任务到线程池里面
        for (int i = 0; i < 6; ++i) {
            int finalI = i;
            POOL_EXECUTOR.execute(() -> {
                DataEntity dataEntity = new DataEntity(); //10MB
                ThreadLocalOutOfMemoryDemo.dataEntityTl.set(dataEntity);
                System.out.println(String.format("线程 [%s] 正在执行[%s号]任务", Thread.currentThread().getName(), finalI));
                dataEntityTl.remove();
            });

            Thread.sleep(10);
        }
        //dataEntityTl = null;
        System.out.println("所有任务执行结束");
    }
}

Value内存泄露 

解决办法很简单,就是调用一下dataEntityTl.remove();。 此时我们再来运行一下代码,然后再点击一下GC回收,此时 内存占用就只有一兆多了。这六个线程里面的60兆数据就已 经被回收了。

并发编程工具包J.U.C

1 locks包

Lock API介绍及使用示例

JDK1.5之后,并发包中新增 了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
  Lock是一个接口,它定义了锁获取和释放的基本操作,API如下:

ReentrantLock基于AQS的抢锁和释放锁的过程(以非公平锁为例)

抢锁过程

1、先基于CAS尝试两次抢锁

	// 抢锁入口
	final void lock() {
		// CAS更新状态
	    if (compareAndSetState(0, 1))
	    	// 更新成功则把当前持锁线程更新为自己
	        setExclusiveOwnerThread(Thread.currentThread());
	    else
	        acquire(1);
	}

	public final void acquire(int arg) {
		//再次尝试抢锁
	    if (!tryAcquire(arg) && 
	    	// 再次抢锁失败则包装成Node节点并加入AQS队列
	    	acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	        selfInterrupt();
	}

	protected final boolean tryAcquire(int acquires) {
	    return nonfairTryAcquire(acquires);
	}

	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()) {
	        int nextc = c + acquires;
	        if (nextc < 0) // overflow
	            throw new Error("Maximum lock count exceeded");
	        // 若为重入锁则重入次数增加
	        setState(nextc);
	        return true;
	    }
	    return false;
	}

2、加入队列并进行自旋等待

	// 回到第一次抢锁失败的代码
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
            selfInterrupt();
    }

	// 添加一个排它锁Node
	private Node addWaiter(Node mode) {
	    Node node = new Node(Thread.currentThread(), mode);
	    // Try the fast path of enq; backup to full enq on failure
	    Node pred = tail;
	    // 队列不为空,直接入队
	    if (pred != null) {
	        node.prev = pred;
	        // 节点插入到队列尾部
	        if (compareAndSetTail(pred, node)) {
	            pred.next = node;
	            return node;
	        }
	    }
	    // 队列为空,需要初始化头节点再入队
	    enq(node);
	    return node;
	}

	// 初始化头结点再入队
    private Node enq(final Node node) {
    	// 死循环
        for (;;) {
            Node t = tail;
            // 初始化头节点
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
	        	// 节点插入到队列尾部
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            	// 若该节点在AQS队列头部则再次尝试抢锁
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 判断是否需要进行中断,需要则调用parkAndCheckInterrupt进行中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        	// 抢锁成功,将节点置位失效
            if (failed)
                cancelAcquire(node);
        }
    }

	// 判断是否进行中断,如果因为原因返回false,则会通过上一个方法继续进入下一次循环
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 上一个节点有效,则直接中断(说明不在队列头部,且前面还有需要抢锁的线程)
        if (ws == Node.SIGNAL)
            return true;
        // 上一个节点无效,则删除上一个节点,循环遍历
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	// 看起来跟共享锁有关,0或者-3都需要更新为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    //ThreadB、 ThreadC、ThreadD、ThreadE -> 都会阻塞在下面这个代码的位置.
    private final boolean parkAndCheckInterrupt() {
        //被唤醒. (interrupt()->)
        LockSupport.park(this);
        //中断状态(是否因为中断被唤醒的.)
        return Thread.interrupted();
    }

释放过程

    public final boolean release(int arg) {
    	// 尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
            	// 唤醒队列的第一个线程(对应第二个节点,头结点是个空节点)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

	// 正常重入锁是逐层释放,releases=1,有特殊情况直接全部释放,例如await
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        // 线程是否空闲,如果是重入锁为释放完应该是false
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

	// 唤醒队列的第一个线程
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        // 如果s节点失效则取下一个
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

Condition 

Condition的实际应用

-> 实现阻塞队列(业务组件)

-> 在线程池中会用到阻塞队列

-> 生产者消费者

-> 流量缓冲

public class ConditionDemoAwait implements Runnable {
    private Lock lock;
    private Condition condition;

    public ConditionDemoAwait(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        System.out.println("begin - ConditionDemoWait");
        lock.lock();
        try {
            condition.await();
            //让当前线程阻塞,Object.wait();
            System.out.println("end - ConditionDemoWait");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ConditionDemoSignal implements Runnable {
    private Lock lock;
    private Condition condition;

    public ConditionDemoSignal(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        System.out.println("begin - ConditionDemeNotify");
        lock.lock();
        try {
            condition.signal();
            System.out.println("end - ConditionDemeNotify");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Condition await和signal源码解析

执行await方法

    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 添加到condition队列
        Node node = addConditionWaiter();
        //完全释放锁(考虑重入问题)
        long savedState = fullyRelease(node);
        int interruptMode = 0;
        // 判断当前线程是否进入AQS,signal的时候会把该线程移入AQS
        while (!isOnSyncQueue(node)) {
        	//阻塞当前线程(当其他线程调用signal()方法时,该线程会从这个位置去执行)
            LockSupport.park(this);
            //要判断当前被阻塞的线程是否是因为interrupt()唤醒,是则直接break
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        //重新竞争锁,savedState表示的是被释放的锁的重入次数.
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        // clean up if cancelled
        if (node.nextWaiter != null) 
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

	// 添加到condition队列
    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();
            t = lastWaiter;
        }
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        if (t == null)
            firstWaiter = node;
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
    }

	// 判断是否被中断唤醒
    private int checkInterruptWhileWaiting(Node node) {
        return Thread.interrupted() ?
        	// 若被interrupt唤醒则更新节点,更新成功抛异常,更新失败则等待其他线程更新成功后再次中断
            (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
            0;
    }

执行signal方法

    public final void signal() {
    	// 如果当前线程没有获取到锁抛异常
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }

    private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        // 把condition队列里的节点移动到sync队列
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

    final boolean transferForSignal(Node node) {
		// 如果更新失败说明节点已失效
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
		// 节点插入同步队列
        Node p = enq(node);
        int ws = p.waitStatus;
        // 如果上一个节点已失效,则唤醒当前节点对应的线程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

线程的通信

共享内存

wait/notify -> 基于某个条件来等待或者唤醒

public class Consumer implements Runnable{
    private Queue<String> bags;
    private int maxSize;
    public Consumer(Queue<String> bags, int maxSize) {
        this.bags = bags;
        this.maxSize = maxSize;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bags){
                if(bags.isEmpty()){
                    System.out.println("bags为空");
                    try {
                        bags.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String bag=bags.remove();
                System.out.println("消费者消费:"+bag);
                bags.notify(); //这里只是唤醒Producer线程,但是Producer线程并不能马上执行。
                // public class Consumer implements Runnable{
                    private Queue<String> bags;
                    private int maxSize;
                    public Consumer(Queue<String> bags, int maxSize) {
                        this.bags = bags;
                        this.maxSize = maxSize;
                    }
                    @Override
                    public void run() {
                        while(true){
                            synchronized (bags){
                                if(bags.isEmpty()){
                                    System.out.println("bags为空");
                                    try {
                                        bags.wait();
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                }
                                try {
                                    Thread.sleep(1000);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                String bag=bags.remove();
                                System.out.println("消费者消费:"+bag);
                                bags.notify(); //这里只是唤醒Producer线程,但是Producer线程并不能马上执行。
                            } //同步代码块执行结束, monitorexit指令执行完成
                        }
                    }
                }

public class Producer implements Runnable {
    private Queue<String> bags;
    private int maxSize;
    public Producer(Queue<String> bags, int maxSize) {
        this.bags = bags;
        this.maxSize = maxSize;
    }
    @Override
    public void run() {
        int i=0;
        while(true){
            i++;
            synchronized (bags){ //抢占锁
                if(bags.size()==maxSize){
                    System.out.println("bags 满了");
                    try {
//park(); ->JVM ->Native
                        bags.wait(); //满了,阻塞当前线程并且释放Producer抢到的锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("生产者生产:bag"+i);
                bags.add("bag"+i); //生产bag
                bags.notify(); //表示当前已经生产了数据,提示消费者可以消费了
            } //同步代码快执行结束
        }
    }
}

2 atomic包

当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变 量i=1,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可能i不等于3,而是等于2。因 为A和B线程在更新变量i的时候拿到的i都是1,这就是线程不安全的更新操作,通常我们会使 用synchronized来解决这个问题,synchronized会保证多线程不会同时更新变量i。
  而Java从JDK 1.5开始提供了java.util.concurrent.atomic包(以下简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式,他们都是基于CAS实现的原子操作。

  • AtomicBoolean:原子更新布尔类型。
  • AtomicInteger:原子更新整型。
  • AtomicLong:原子更新长整型。
  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。
  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicReference:原子更新引用类型。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
  • AtomicMarkableReference:原子更新带有标记位的引用类型。
  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。

3 ConcurrentMap接口

put主方法分析

	final V putVal(K key, V value, boolean onlyIfAbsent) {
        // CHM不允许为空
        if (key == null || value == null) throw new NullPointerException();
        // 计算hash值
        int hash = spread(key.hashCode());
        // 该数组节点上元素个数
        int binCount = 0;
        // 失败重试,直到put成功
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 延迟初始化,第一次put进行table初始化
            if (tab == null || (n = tab.length) == 0)
            	// 初始化数组,详见步骤3
                tab = initTable();
            // 如果数组下标位置为空,直接尝试CAS写入
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 尝试将table的i下标位置由null更新为当前节点
                if (casTabAt(tab, i, null,
                        new Node<K,V>(hash, key, value, null)))
                    // CAS成功,结束循环
                    break;
            }
            // ForwardingNode的hash值,代表当前table正在扩容
            else if ((fh = f.hash) == MOVED)
                // 进行协助扩容
                tab = helpTransfer(tab, f);
            // 数组下标已有节点,且没有在扩容阶段,此时应该遍历链表/红黑树进行节点插入
            // 插入后还需判断是否进行链表转红黑树或者扩容
            else {
                V oldVal = null;
                // 锁住该下标,比JDK1.7的分段锁粒度更小,效率更高
                synchronized (f) {
                    // 前面赋值了这里要重新判断,类似DoubleCheckLock
                    if (tabAt(tab, i) == f) {
                        // 链表的处理逻辑
                        // 小于0的情况:出于扩容中(-1),红黑树根节点(-2),其他特殊情况
                        if (fh >= 0) {
                            // 有一个非空节点,开始计算节点数
                            binCount = 1;
                            // 遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 如果key相同则覆盖value
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                // 如果到了链表最后一个节点,则插入
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        // 如果是红黑树
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            // 遍历红黑树,如果key相同则覆盖value
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 只要成功插入,这个分支都会进入
                if (binCount != 0) {
                    // 总结点数大于8就需要进行处理,根据条件确定是要扩容还是转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                    	// 链表长度大于8的处理逻辑,详见步骤4
                        treeifyBin(tab, i);
                    // oldVal不为空,说明key已存在进行了覆盖,无需增加size,直接return
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 增加节点计数,计算size,详见步骤7
        addCount(1L, binCount);
        return null;
    }

初始化数组

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        // 数组未初始化完成,所有线程都必须进入该循环,等待其中一个线程初始化完成,此处也采用CAS
        while ((tab = table) == null || tab.length == 0) {
            // 第一次不走这个分支,有线程抢占标量成功后会将sizeCtl更新为-1
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            // CAS尝试抢占标量SIZECTL,将其更新为-1,抢占成功初始化表
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    // 再次判断table,还是DoubleCheckLock
                    if ((tab = table) == null || tab.length == 0) {
                        // sc > 0 代表初始化容量,没有设置则使用默认容量16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        // 新建Node数组,这就是Hash桶
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // sc设置为下次扩容阈值,为n*0.75
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

链表长度大于8的处理逻辑

    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            // table长度小于64则进行扩容
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                // 翻倍扩容:n<<1等价于2n,详见步骤5
                tryPresize(n << 1);
            // 否则转化为红黑树
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                    new TreeNode<K,V>(e.hash, e.key, e.val,
                                            null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

扩容/协助扩容

    private final void tryPresize(int size) {
        // 要扩容的值size如果大于等于最大容量的一半,则直接扩容到最大容量
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
                tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            // 初始化操作
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            // 扩容目标小于初始化后容量,或者当前已经超过最大容量则结束
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                // 生成扩容戳,高16位表示当前扩容标记,低16位表示当前扩容线程数
                int rs = resizeStamp(n);
                // 协助扩容走这段逻辑
                if (sc < 0) {
                    Node<K,V>[] nt;
                    // 扩容结束
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        // 进行数据迁移,详见步骤6
                        transfer(tab, nt);
                }
                // 第一次循环走这段逻辑,因为sizectl为1表示初始化,所以第一次直接+2
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    // 进行数据迁移,详见步骤6
                    transfer(tab, null);
            }
        }
    }

数据迁移

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // stride代表每个线程处理的数据的区间大小,如果是单线程直接等于n
        // 如果是多线程则为n/8/CPU核数(应该是根据计算资源默认一个CPU用8个扩容线程并发可以达到较高的效率)
        // 最小是16。
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // 初始化扩容后数组
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                // n<<1 扩容目标是当前数组的两倍
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            // 扩容后数组
            nextTable = nextTab;
            // 表示待分配处理的数组长度,初始化时为old数组总长度,每分配一段区间出去,就减去该长度
            transferIndex = n;
        }
        // 扩容后数组长度
        int nextn = nextTab.length;
        // 用来表示已经迁移完的状态,也就是说,如果某个old数组的节点完成了迁移,则需要更改成fwd。
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        // 推进标记,是否推进下一组区间
        boolean advance = true;
        // 完成标记
        boolean finishing = false; // to ensure sweep before committing nextTab
        // i 表示分配给当前线程任务,执行到的桶位
        // bound 表示分配给当前线程任务的下界限制
        for (int i = 0, bound = 0;;) {
            // f 桶位的头结点
            // fh 头结点的hash
            Node<K,V> f; int fh;

            // 总的来说,这个循环就是为了分配任务,如果确认没有任务可分配则通过标量设置i=-1,以便后续流程退出
            while (advance) {
                // nextIndex 分配任务的开始下标
                // nextBound 分配任务的结束下标
                int nextIndex, nextBound;

                // --i,每次一个位置处理完成后i往前移动一位,i>=bound表示当前区间还有下标未处理完
                // 假设old数组长度64,每次处理16,那么线程第一个处理的区间是48-63,bound=48,i=63
                // 处理完63这个节点后,处理62,等到处理完区间最后一个节点48时,i变成47,此时条件不满足
                // 执行下面的CAS TRANSFERINDEX,将bound变成32
                // finishing表示任务完全处理完
                if (--i >= bound || finishing)
                    advance = false;

                // transferIndex被更新为0,表示当前没有区间可以分配给该线程
                // 意味着扩容即将结束,设置i=-1,以便后续流程直接退出
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // 能走到这里说明当前线程空闲且还存在待分配区间需要处理
                // 通过CAS抢占区间,抢到了就执行该区间数据的迁移
                else if (U.compareAndSwapInt
                        (this, TRANSFERINDEX, nextIndex,
                                nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            // 如果未分配到任务(即扩容即将完成或已经完成,无需当前线程继续处理),
            // 进行退出前处理并结束扩容
            if (i < 0 || i >= n || i + n >= nextn) {
                // 保存sizeCtl 的变量
                int sc;
                // 最后一个执行完成的线程会进入该方法,进行变量赋值操作再退出
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                // 将当前处理的线程数-1
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 条件成立:说明当前线程不是最后一个退出transfer任务的线程,这时候直接返回即可
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n;
                }
            }

            // 走到这里说明当前线程的任务尚未处理完,正在进行中

            // 如果当前桶位未存放数据,将此处设置为fwd节点。
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);

            // hash=MOVED说明当前节点已经是fwd节点,进入下一次循环处理下个节点
            else if ((fh = f.hash) == MOVED)
                advance = true;

            // 该节点数据需要处理
            else {
                synchronized (f) {
                    // 随处可见的DCL,防止在加锁头对象之前,当前桶位的头对象被其它写线程修改过,导致目前加锁对象错误...
                    if (tabAt(tab, i) == f) {
                        // ln 表示低位链表引用
                        // hn 表示高位链表引用
                        // 此处是由于扩容后节点可能会分配到其他节点
                        // 用低位链连接扩容后处在原节点的数据
                        // 用高位链连接扩容后被移动到另一个节点的数据
                        // 例如原来数组长度为16,此时4和20都在table[3]的位置
                        // 扩容到32后,4的位置不变,20会移动到table[19]的位置
                        Node<K,V> ln, hn;
                        // 节点的hash值大于0,说明是链表,另一个分支是红黑树的处理逻辑
                        if (fh >= 0) {
                            // 用fh & n 来表示迁移后位置是否会发生变化,结果为0说明不会发生变化,结果为1说明会发生变化
                            int runBit = fh & n;
                            // 用来获取最后一段runBit相等的第一个节点,有点抽象,举个例子,还是假设数组由16扩容到32
                            // 如果链表是4,20,36,68,100,这时候36,68,100的runBit都为0(即迁移后位置不会发生变化)
                            // 此时lastRun就是36
                            // 如果链表是4,36,20,52,这时候20,52的runBit都为1(即迁移后位置不会发生变化)
                            // 此时lastRun就是20
                            // 有lastRun的目的就是后续遍历进行高低链处理的时候只需要处理到runBit就行
                            // runBit后面的数组是一条天然链表,无需重复进行断链重组
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                // 如果下个节点的runBit跟上一个不一样,则把下个节点当做lastRun,继续遍历
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            // 根据runBit判断这个lastRun是高位链还是低位链
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            // 根据runBit判断这个lastRun是高位链还是低位链
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            // 重新遍历一次,完善高低链
                            // 此时只需要遍历到lastRun即可
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            // CAS设置高低链
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            // 处理完成,将当前节点更新为fwd
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        //条件成立:表示当前桶位是 红黑树 代理结点TreeBin
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                        (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

计算元素总数

    private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        // 如果counterCells不是空,或者CAS增加baseCount失败,说明需要通过counterCells计数
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            // 如果CounterCell是空(尚未出现并发)
            if (as == null || (m = as.length - 1) < 0 ||
                    // 如果随机取余一个数组位置为空或者
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    // 修改这个槽位的变量失败(前两个条件都不满足,即CounterCell不为空且随机位置也不为空才会走到这里)
                    !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                // 死循环处理,完成CounterCell的初始化以及元素的累加,详见步骤8
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        // 检查是否需要扩容,在putVal方法调用时,默认就是要检查的。
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            // 总数大于需要扩容的阈值sizeCtl且table不为空,且长度未达到上限,进行扩容
            // 这段代码跟之前的扩容代码一样
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

竞争激烈情况下的保成功计数

    private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            // 如果counterCells已经初始化
            if ((as = counterCells) != null && (n = as.length) > 0) {

                // 通过该值与当前线程probe求与,获得cells的下标元素,和hash表获取索引是一样的
                // 如果下标为空,说明可以尝试直接存入counterCell
                if ((a = as[(n - 1) & h]) == null) {

                    // 说明当前CounterCell没有被其他线程占用
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        CounterCell r = new CounterCell(x); // Optimistic create

                        // 通过cas设置cellsBusy标识,防止其他线程来对counterCells并发处理
                        if (cellsBusy == 0 &&
                                U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {
                                CounterCell[] rs; int m, j;
                                // 将初始化的r对象的元素个数放在对应下标的位置
                                if ((rs = counterCells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            // 说明指定cells下标位置的数据不为空,则进行下一次循环
                            continue;
                        }
                    }
                    collide = false;
                }
                // 说明在addCount方法中cas失败了,并且获取probe的值不为空
                else if (!wasUncontended)
                    // 设置为未冲突标识,进入下一次自旋
                    wasUncontended = true;
                // 由于指定下标位置的cell值不为空,则直接通过cas进行原子累加,如果成功,则直接退出
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                // 如果已经有其他线程建立了新的counterCells或者CounterCells大于CPU核心数(很巧妙,线程的并发数不会超过cpu核心数)
                else if (counterCells != as || n >= NCPU)
                    // 设置当前线程的循环失败不进行扩容
                    collide = false;
                // 恢复collide状态,标识下次循环会进行扩容
                else if (!collide)
                    collide = true;
                // 进入这个步骤,说明CounterCell数组容量不够,线程竞争较大,所以先设置一个标识表示为正在扩容
                else if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {
                            //扩容一倍2变成4,这个扩容比较简单
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    // 继续下一次自旋
                    continue;                   // Retry with expanded table
                }
                h = ThreadLocalRandom.advanceProbe(h);
            }
            // cellsBusy=0表示没有在做初始化,通过cas更新cellsbusy的值标注当前线程正在做初始化操作
            else if (cellsBusy == 0 && counterCells == as &&
                    U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        // 初始化CounterCell
                        CounterCell[] rs = new CounterCell[2];
                        // 将x也就是元素的个数 放在指定的数组下标位置
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    // 回复标识
                    cellsBusy = 0;
                }
                // 初始化完成,结束流程
                if (init)
                    break;
            }
            // 竞争激烈,其它线程占据cell数组,尝试直接累加在base变量中
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;
        }
    }

4 BlockingQueue接口

4.1 阻塞队列概述

        阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。支持阻塞的插入方法是指当队列满时,队列会阻塞插入元素的线程,直到队列不满。支持阻塞的移除方法是指在队列为空时,获取元素的线程会等待队列变为非空。
  阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器

4.2 阻塞队列中的方法

添加元素

  • add -> 如果队列满了,抛出异常
  • offer -> true/false , 添加成功返回true,否则返回false
  • put -> 如果队列满了,则一直阻塞
  • offer(timeout) , 带了一个超时时间。如果添加一个元素,队列满了,此时会阻塞timeout时长,超过阻塞时长,返回false。

删除元素

  • element-> 队列为空,抛异常
  • peek -> true/false , 移除成功返回true,否则返回false
  • take -> 一直阻塞
  • poll(timeout) -> 如果超时了,还没有元素,则返回null

4.3 J.U.C 中的阻塞队列

  • ArrayBlockingQueue 基于数组结构
  • LinkedBlockingQueue 基于链表结构
  • PriorityBlcokingQueue 基于优先级队列
  • DelayQueue 允许延时执行的队列
  • SynchronousQueue 没有任何存储结构的的队列

5 Executor接口

5.1 为什么要使用线程池

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

5.2 线程池参数含义

  • corePoolSize:当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • workQueue:用于保存等待执行的任务的阻塞队列。
  • maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并 且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设 置更有意义的名字。
  • RejectedExecutionHandler:当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。在JDK 1.5中Java线程池框架提供了以下4种策略。AbortPolicy:直接抛出异常。CallerRunsPolicy:只用调用者所在线程来运行任务。DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。DiscardPolicy:不处理,丢弃掉。 当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
  • keepAliveTime:线程池的工作线程空闲后,保持存活的时间。所以, 如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
  • TimeUnit:线程活动保持时间的单位,可选的单位有天(DAYS)、小时(HOURS)、分钟 (MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

5.3 newFixedThreadPool,newCachedThreadPool和newSingleThreadPool使用示例

    private static void one() {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 20; i++) {
            int finalI = i;
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println(String.format("线程【%s】正在执行[%s]任务", Thread.currentThread().getName(), finalI));
                }
            };
            executor.execute(task);
        }
    }
private static void two() {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 20; i++) {
            int finalI = i;
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println(String.format("线程【%s】正在执行[%s]任务", Thread.currentThread().getName(), finalI));
                }
            };
            executor.execute(task);
        }
    }

private static void three() {
        //特点,线程数量几乎可以无限增加   闲置
        ExecutorService executor = Executors.newCachedThreadPool();

        for (int i = 0; i < 2000; i++) {
            int finalI = i;
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println(String.format("线程【%s】正在执行[%s]任务", Thread.currentThread().getName(), finalI));
                }
            };
            executor.execute(task);
        }
    }
private static void four() {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

        executor.schedule(() -> {
            System.out.println("5秒后执行");
        }, 5, TimeUnit.SECONDS);

        executor.scheduleAtFixedRate(() -> {
            System.out.println("5秒后执行,再之后每秒执行一次");
        }, 5, 1, TimeUnit.SECONDS);

    }

实际上这四种方式,底层都是在创建一个名叫 ThreadPoolExecutor的对象。这四种方式的创建,实际上都 是在修改它的一些参数,比如说核心线程数,最大线程数, 存活时间,底层队列的类型等

5.4 线程池状态 

1. 首先看下RUNNING状态,这个就是运行中的状态,这个 时候,咱们的客户端可以正常提交任务,线程池也能够正常的执行任务。可以简单的想象成你的工厂开业了,可以 打螺丝了。

2. 接着如果咱们调用一下shutdown()方法,就会进入 shutdown的状态,这个状态它就不会接收新任务了,但 是它还会处理当前任务队列里的任务。就相当于你工厂关 门了,不会对外再接订单了,但是,你之前签的那一堆订 单你得执行完,不然的话就违约了。

3. 或者你可以调用shutdownNow()方法,shutdown不会立 即结束,shutdownNow()就是立即结束。直接进入STOP 状态,这个状态下,你不能接收新任务,还不可以处理当 前任务队列里的任务,当前正在打的螺丝,也要立马停 止。

4. 接着这shutdown和stop状态都会进到tidying状态里。这 个状态就表明所有的任务都停止了,线程也都停了,那么 线程池就会帮我们调用一个terminated方法,这个方法就 是会进入到最后的终止状态。

5.5 线程池源码解析

创建线程池

    public void execute(Runnable command) {
    	// 命令不可为空
        if (command == null)
            throw new NullPointerException();
		//获取ctl对应的int值
        int c = ctl.get();
        // c是一个32位int变量,前3位表示线程池状态,后29位表示线程数量
        // workerCountOf是将c与上1<<29-1,获取线程数量
        // 如果小于核心线程数,则新建线程并将任务丢给新线程处理(延迟初始化)
        if (workerCountOf(c) < corePoolSize) {
        	// 尝试创建核心线程并执行
            if (addWorker(command, true))
                return;
            // ctl发生变化,重新获取
            c = ctl.get();
        }
        // isRunning是判断c的线程池状态(前3位)是否为running(大于0)状态
        //  private static final int RUNNING    = -1 << COUNT_BITS;
        //	private static final int SHUTDOWN   =  0 << COUNT_BITS;
        //	private static final int STOP       =  1 << COUNT_BITS;
        //	private static final int TIDYING    =  2 << COUNT_BITS;
        //	private static final int TERMINATED =  3 << COUNT_BITS;
        // 线程池状态正常则提交任务,由于此处需要对提交结果进行处理,使用offer,失败表示队列已满
        if (isRunning(c) && workQueue.offer(command)) {
        	// 再次获取ctl的int值
        	// 任务入队的过程中,线程池状态可能已经被修改
            int recheck = ctl.get();
            // 如果线程池状态不是RUNNING,并且成功删除刚刚入队的任务
            if (!isRunning(recheck) && remove(command))
            	// 直接执行拒绝策略
                reject(command);
                
            // 进入本分支有几种情况:
            // 1.线程池处于RUNNING状态,但工作线程数为0
            // 2.线程池处于非RUNNING状态,但是任务从阻塞队列删除失败,此时工作线程数为0
            else if (workerCountOf(recheck) == 0)
            	// 创建新的非核心线程
                addWorker(null, false);
        }
        // 尝试创建非核心线程并执行
        else if (!addWorker(command, false))
        	// 执行拒绝策略
            reject(command);
    }

创建并启动线程

    private boolean addWorker(Runnable firstTask, boolean core) {
    	// 外层循环标记
        retry:
        for (;;) {
            int c = ctl.get();
            // 获取线程池的运行状态
            int rs = runStateOf(c);
            
            // 线程池状态不为运行中(参数见execute方法内注释) 
            // 且不存在线程池状态为SHUTDOWN,任务为空但队列不为空
            // 逻辑有点拗口,翻译过来就是当线程池状态不为RUNNING的时候,只有一种情况可以继续执行
            // 就是线程池状态为SHUTDOWN,传进来的任务为空(不再传任务)但队列中还有任务
            // 再翻译过来就是说,线程池被SHUTDOWN的时候workQueue内的任务需要执行完成
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
			
            for (;;) {
            	// 获取工作线程数
                int wc = workerCountOf(c);
                // 如果工作线程数大于CAPACITY
                // 或者创建的是核心线程且工作线程数大于等于corePoolSize
                // 或者创建的是非核心线程且工作线程数大于等于maximumPoolSize
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    // 如果是核心线程超过,此处返回false,由execute继续调用创建非核心线程
                    return false;
                // CAS增加线程数
                if (compareAndIncrementWorkerCount(c))
                	// 增加成功,退出retry循环,去创建线程、处理任务
                    break retry;
                c = ctl.get(); 
                if (runStateOf(c) != rs)
                	// 状态发生变化,进入retry循环重新执行
                    continue retry;
            }
        }

		// 标记工作线程是否被启动
        boolean workerStarted = false;
        // 标记工作线程是否被添加成功
        boolean workerAdded = false;
        // 工作线程
        Worker w = null;
        try {
        	// 创建一个工作线程
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
					// 如果线程池状态正常或者为SHUTDOWN但是还有任务待处理
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) 
                            throw new IllegalThreadStateException();
                        // 工作线程的HashSet中添加此工作线程
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                	// 启动线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
        	// 如果workerStarted为false即工作线程启动失败
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

获取并执行任务

    public void run() {
        runWorker(this);
    }

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        // 这里不是解锁操作,这里是为了设置state = 0 以及 ExclusiveOwnerThread = null.因为起始状态state = -1, 
        // 不允许任何线程抢占锁,这里就是初始化操作。
        w.unlock(); 
        boolean completedAbruptly = true;
        try {
        	// task如果执行后还是为null,说明无法再获取任务,说明任务队列为空
            while (task != null || (task = getTask()) != null) {
            	// 加锁,防止线程未处理完任务被线程池shutdown
                w.lock();
				// 这里有两个作用:
				// 1、线程池处于STOP/TIDYING/TERMINATION状态时需要设置线程的中断标志位
				// 2、强制刷新标志位,通过Thread.interrupted()方法,因为有可能上一次执行task时,
				// 当先线程的中断标志位被设置为了true,且没有处理,这里就需要处理一下。
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    // 响应中断
                    wt.interrupt();
                try {
                	// 钩子方法,留给子类实现
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

从阻塞队列获取任务

    private Runnable getTask() {
    	// 表示当前线程获取任务是否超时,默认是false,true表示已超时。
        boolean timedOut = false; 
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 这里判断如果说线程池状态是非Running状态 && (队列中没有任务了 || 线程池当前最低状态也是STOP)
            // 就会使用CAS的方式将ctl值-1,即减少一个工作线程数。最后直接返回NULL。
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
            int wc = workerCountOf(c);

            // timed表示当前线程在从队列中获取任务的时候是否有超时时间。
            // 设置此参数的主要依据就是,判断allowCoreThreadTimeOut是否允许核心线程超时被回收
            // 当前线程数的数量已经大于了核心线程数,说明当前线程当获取任务超时时一定可以被回收。 
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

			// 判断当前线程是否达到了回收的标准
			// 当工作线程数大于最大线程数或者获取任务超时
			// 并且当前线程池线程数量大于1或者队列中没有任务了,当前线程就达到回收标准了
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                // CAS有可能会失败,为什么会失败?
                // 1.其他线程先你一步退出
                // 2.线程池状态发生了变化。
                if (compareAndDecrementWorkerCount(c))
                    return null;
                // 如果CAS失败,再次自旋,timed就有可能是false了,因为当前线程CAS失败,
                // 很有可能是因为其他线程成功退出导致的,再次自旋时检查发现,当前线程就可能不属于回收范围了。
                continue;
            }

            try {
            	// 根据timed的值,判断去队列中获取任务是使用带超时时间的还是不带超时时间的。
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                // 说明当前线程超时了。继续进行自旋。
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

6 CompletionService接口

CompletionService是Java并发包提供的一个用于处理多个并发任务结果的工具类。它可以将任务的执行结果封装为Future对象,并提供一个take()方法来从已完成的任务中获取结果,以便及时处理任务的结果。

CompletionService是一个接口,它继承自Executor接口,并为提交的任务提供了一种获取完成结果的机制。在使用CompletionService时,常用的方法包括:

  1. submit(Callable task):将任务提交给CompletionService进行处理,并返回表示任务结果的Future对象。
  2. submit(Runnable task, T result):将任务提交给CompletionService进行处理,并返回表示任务结果的Future对象,此处的result参数用于任务执行完成后返回的结果。
  3. take():获取已完成的任务的结果,并返回表示任务结果的Future对象。如果没有已完成的任务,take()方法会阻塞等待任务完成。
  4. poll():尝试立即获取已完成的任务的结果,并返回表示任务结果的Future对象。如果没有已完成的任务,poll()方法会立即返回null
  5. poll(long timeout, TimeUnit unit):在指定的时间内,尝试获取已完成的任务的结果,并返回表示任务结果的Future对象。如果在指定时间内没有已完成的任务,poll()方法会返回null

这些方法提供了不同的方式来获取已完成任务的结果,take()方法会阻塞等待任务完成,而poll()方法则是立即返回结果或者null

除了上述方法,CompletionService还提供了一些其他便捷方法,如isShutdown()shutdown()awaitTermination(),用于管理CompletionService所使用的Executor。这些方法可以用于控制和管理任务的提交和处理过程。

CompletionServiceFuture都是用于处理并发任务结果的工具,但它们有一些不同之处,CompletionService相对于Future有以下几个优点:

  1. 异步获取结果: CompletionService提供了一个take()方法,可以异步获取已完成任务的结果,而不需要阻塞等待所有任务都完成。这样可以避免主线程长时间阻塞,从而提高并发性能和响应性。
  2. 有序处理结果: CompletionService可以按照任务完成的先后顺序获取结果,这对于需要按照任务完成先后顺序处理结果的场景是很有用的。而Future只能通过get()方法来获取结果,顺序不可控。
  3. 逐个处理结果: CompletionService提供了一个类似于迭代器的方式来获取已完成任务的结果,可以逐个处理结果,而不需要等待所有任务都完成。这对于处理大量任务的情况下,能够及时处理已完成的任务结果,提高效率。
  4. 错误隔离: CompletionService可以将每个任务的异常隔离开来处理,即使一个任务出现异常,也不会影响其他任务的执行和结果处理。这对于并发任务异常处理非常有用。

综上所述,CompletionService相对于Future提供了更多的灵活性和便利性,特别是在需要并发执行任务并及时处理结果的场景中,更加适合使用CompletionService

7 Future接口

构建一个CompletableFuture

supplyAsync 异步执行一个任务,提供返回值

supplyAsync(runnable,Executor executor) 提供返回值

runAsync(runnable,Executor executor) -> 异步执行一个任务,但是可以自定义线程池,没有返 回值

runAsync(runnable) -> 异步执行一个任务, 默认用ForkJoinPool.commonPool();,没有返回值

8 CountDownLatch

CountDownLatch的实现原理

它可以让一个线程阻塞

也可以让多个线程阻塞

共享锁的实现。

可以允许多个线程同时抢占到锁,然后等到计数器归零的时候,同时唤醒. 

假如有这样一个需求:我们需要解析一个Excel里多个sheet的数据,此时可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。这个需求中,主线程等待所有线程完成sheet的解析操作,可以通过JDK 1.5之后的并发包中提供的CountDownLatch来实现。

import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
    static CountDownLatch c = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " countDown");
                c.countDown();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " countDown");
                c.countDown();
            }
        }).start();
        c.await();
        System.out.println("3");
    }
}

CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法 会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个 点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个 CountDownLatch的引用传递到线程里即可。

9 Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
  Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        s.acquire();
                        System.out.println("save data");
                        s.release();
                    } catch (InterruptedException e) {
                    }
                }
            });
        }
        threadPool.shutdown();
    }
}

在代码中,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。

10 CyclicBarrier

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

import java.util.Map;
import java.util.concurrent.*;

public class BankWaterService implements Runnable {
    /*** 创建4个屏障,处理完之后执行当前类的run方法 */
    private CyclicBarrier c = new CyclicBarrier(4, this);
    /*** 假设只有4个sheet,所以只启动4个线程 */
    private Executor executor = Executors.newFixedThreadPool(4);
    /*** 保存每个sheet计算出的银流结果 */
    private ConcurrentHashMap<String, Integer> sheetBankWaterCount = new ConcurrentHashMap<String, Integer>();

    private void count() {
        for (int i = 0; i < 4; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    // 计算当前sheet的银流数据,计算代码省略
                    sheetBankWaterCount.put(Thread.currentThread().getName(), 1);
                    // 银流计算完成,插入一个屏障
                    try {
                        c.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    @Override
    public void run() {
        int result = 0;
        // 汇总每个sheet计算出的结果
        for (Map.Entry<String, Integer> sheet : sheetBankWaterCount.entrySet()) {
            result += sheet.getValue();
        }
        // 将结果输出
        sheetBankWaterCount.put("result", result);
        System.out.println(result);
    }

    public static void main(String[] args) {
        BankWaterService bankWaterCount = new BankWaterService();
        bankWaterCount.count();
    }
}

1、共享锁/排他锁,乐观锁/悲观锁的区别和应用场景

共享锁【S锁】:又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
排他锁【X锁】:又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
共享锁与排它锁区别
1.共享锁只用于表级,排他锁用于行级。 共享锁保证了其他事务不能写,排他锁保证了其他事物不能读。
2.加了共享锁的对象,可以继续加共享锁,不能再加排他锁。加了排他锁后,不能再加任何锁。 
3.比如一个DML操作,就要对受影响的行加排他锁,这样就不允许再加别的锁,也就是说别的会话不能修改这些行。同时为了避免在做这个DML操作的时候,有别的会话执行DDL,修改表的定义,所以要在表上加共享锁,这样就阻止了DDL的操作。 
4.当执行DDL操作时,就需要在全表上加排他锁

悲观锁:顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
乐观锁:反之,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

当我们对共享资源竞争非常激烈的情况下,这时候不管乐观锁还是悲观锁,他们的性能都是很低的。​ 自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。在高并发的情况下,如果多个线程同时更新一个值不成功,就会一直自旋,增大cpu压力。
所以并发不激烈的情况下用乐观锁,并发激烈情况下用悲观锁。(根据资源竞争的情况和锁的粒度)


2、线程池数量如何配置

性能和资源最大化利用的一个平衡,要考虑资源限制的问题。动态化线程池:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html


3、单核CPU是否存在原子性、可见性、有序性问题

存在原子性和有序性问题(CPU缓存考虑)


4、缓存锁和缓存一致性协议是同一个东西吗

缓存锁与缓存一致性协议并不是相同的。相比总线锁,缓存锁即降低了锁的力度,核心机制是基于缓存一致性协议来实现的。缓存一致性协议优化导致性能问题,引入了写缓冲和无效队列,出现了指令重排,volatile底层是直接加屏障来解决有序性问题的,其实通过volatile加了一个缓存锁去禁用了缓存一致性协议的优化。


5、synchronized为什么有锁升级的设计

其实大部分情况下,锁不会去升级的。Synchronized增加了锁的升级这样一个机制,来平衡数据安全性和性能的一个关系。


6、volatile的真正作用是什么

加缓存锁;禁止指令重排序


7、AQS的抢锁过程

核心变量:锁有没有被抢占,被谁抢占。当有人过来抢的时候,看它有没有没抢占,没有的话 直接给它,如果被抢占,让这个人去排队。


8、CHM中put方法的执行过程
9、CHM中有哪些优秀的设计

size++  ---> 分片思想

helpTransfer 协助扩容  ---> 分片思想

高低位链  降低时间复杂度


10、AQS队列和Condition队列的区别,为什么要有两个

AQS队列存储的是 阻塞状态的线程,Condition队列 里面的线程是等待状态的。

11. JDK里面已经有一个synchronized锁了,为什么还要 有JUC包下的这些锁。

涉及到了JDK的一些历史,JDK在1.6之前其实它只能做 重量级锁,性能是不太好的,每次加锁都是要和操作系统, CPU内核打交道,所以那时候有位大师Doug Dea干脆就自己 写了一个Lock接口,这个Lock接口下的锁会减少跟计算机内 核之间的交互,可以降低这种操作的性能损耗。 但是JDK1.6以后,JDK的作者是对synchronized锁做了优化 的,加了偏向锁和轻量级锁后,synchronized锁的性能又还 可以了。 所以,性能并不是JUC包下的锁出现的主要原因。

主要原因是synchronized锁是一个非公平锁,想要通过 synchronized锁实现公平锁那是不可能的。还有就是 synchronized锁是没有超时机制的,假如发生了死锁之类的 问题,就只能重启服务来解决了。所以,synchronized锁是 非常不灵活的,JUC下的Lock锁,灵活性就很高了,比如说可 以实现公平锁的功能,还能够给锁设置超时机制。

原子性:synchronized, AtomicXXX, Lock

可见性: synchronized, volatile

有序性: synchronized,volatile

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值