Java并发编程

Java并发编程

相关概念

  • 进程(Process)

    进程(Process)是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。

    进程就是用来加载指令,管理内存,管理IO。

    进程为线程提供共享内存空间。

    当要一个程序被运行,从磁盘加载这个程序的代码到内存,这时就开启了一个进程。

  • 线程(thread)

    线程(thread)是操作系统能够进行运算调度的最小单位。

    一个进程可以包含多个线程

    一个线程就是一个指令流,并以一定顺序交给CPU执行

  • 并发(concurrent) & 并行(parallel)

    并发(concurrent)是同一时间应对(dealing with)多件事情的能力。

    并行(parallel)是同一时间动手做(doing)多间事情的能力。

  • 同步(Synchronous) & 异步(Asynchronous)

    同步(Synchronous) 需要等待结果返回才可以继续运行。

    异步 (Asynchronous)不需要等待结果的返回就可以继续运行。

注:

  1. 单核CPU下多线程不能实际提高程序运行效率,只是可以再不同线程直接切换,轮流执行。

  2. IO操作不占用CPU,但是需要等待IO结束。所有需要使用【非阻塞式IO】和【异步IO】

Java 线程

创建和运行

Thread内部会有一个Runnable对象,通过构造函数传入。Thread.run()会调用Runnable接口具体实现的run(),即程序员编写的实现方法。

利用FuntureTask对象创建线程。

查看

**Windows **

  • 任务管理器可以查看进程和线程

  • 控制台tasklist命令查看进程,taskkill杀死进程

Linux

  • ps -fe 查看所有进程

  • ps -fT -p <PID> 查看某个进程

  • kill 杀死进程

  • top 查看所有进程

Java

  • 控制台输入jps查看所有Java进程

  • jstack <PID>查看某个Java进程的线程状态

  • jconsole查看Java进程中线程的运行情况

线程运行原理

栈与栈帧 Java Virtual Machine Stacks (Java 虚拟机栈)

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

  • 每个栈由多个**栈帧(Frame)**组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

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

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

  • 线程的 CPU时间片用完

  • 垃圾回收

  • 有更高优先级的线程需要运行

  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态.

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

Java 中的程序计数器(Program Counter Register)是线程私有的,用于记住下一条 JVM指令的执行地址。

常用方法

start & run

调用run(),并没有开启线程运行,只是当前线程调用了run()的方法实现

输出结果:

调用start(),开启一个新的线程运行。

输出结果:

sleep & yield

sleep

  • 调用sleep会让当前线程从 **Running **进入 **Timed Waiting(阻塞) **状态

  • 其它线程可以调用**Timed Waiting **状态线程发 interrupt()方法来打断该线程的阻塞,这时 sleep 方法会抛出InterruptedException

  • 睡眠结束后的线程未必会立刻得到执行

  • 建议用TimeUnitsleep()代替Threadsleep() 来获得更好的可读性

输出结果:

yield

  • 调用yiedl()方法会让当前线程从 Running 进入 Runnable(就绪) 状态

  • 操作系统的任务调度器会自己决定下一个 Runnable 的线程的运行。

线程优先级

  • 线程优先级会提示(hint)调度器优先处理,具体还是看调度器运行那个线程。

  • 如果CPU较忙,优先级高的线程会获得更多的时间片,CPU较闲时,优先级几乎没作用。

输出结果:

join

该方法用于等待线程结束,以实现同步。且可设置最大等待时间。

原理可见保护性暂停 Join源码分析

输出结果:

interrupt

可以打断阻塞状态的线程。打断阻塞状态的线程会将其属性interrupted设置会初始值falsesleep()wait()join()等方法可以让线程进入阻塞状态。

也可以打断运行中的线程,但对于正常运行的线程,只是将其属性interrupted设置为true,不会终止其运行。可以用来停止线程。

输出结果:

输出结果:

利用interrupt()打断park()方法

两阶段终止模式

详见模式->两阶段终止模式

守护线程

  • 默认情况下,Java进程需要等待所有线程都运行结束才会结束。但守护线程只要等到其他的非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

线程状态

操作系统层面

img
  • 初始状态: 线程刚刚创建, 这个时候它只是对象, 没有执行start函数

  • 可运行状态: 线程执行了start函数, 但是还未获得时间片

  • 运行状态: 线程获得了时间片

  • 阻塞状态: 线程读取文件或者IO操作, 该线程不会实际使用到cpu, 会导致上下文切换, 进入阻塞状态

  • 终止状态: 线程结束, 生命周期已经结束

1584714702390

Java线程状态

  • NEW: 线程对象已经创建,但是尚未启动的线程。

  • RUBBABLE: 正在 Java 虚拟机中执行的线程处于该状态。

  • BLOCKED:受阻塞并等待某个监视器锁的线程处于该状态。

  • WAITING:无限期地等待另一个线程来执行某个特定操作的线程处于该状态。

  • TIMED_WAITING:等待另一个线程来执行,取决于指定等待时间的操作的线程处于该状态。

  • TERMINATED:已经退出的线程处于该状态;

代码演示:

输出结果:

共享模型之管程

  • 临界区 (Critical Section)

    一段代码块内如果存在对共享资源的多线程读写操作,就会出现问题,这段代码就被称为临界区

  • 竞态条件 (Race Condition)

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

synchronized

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

语法

变量的线程安全分析

成员变量和静态变量

  • 如果没有共享,则线程安全

  • 如果有共享:

    • 如果都只读,则线程安全

    • 如果有读写,则这段代码是临界区,则需要考虑线程安全

局部变量

  • 局部变量是安全的

  • 但局部变量引用的对象未必安全

    • 如果该对象没有逃离方法的作用范围,则线程安全

    • 如果该对象逃离了方法的作用范围,则需要考虑线程安全

从以下例子可以看出 private 或 final 提供【安全】的意义所在,体现了开闭原则中的【闭】

常见线程安全类

  • String

  • Integer等包装类

  • StringBuffer

  • Random

  • Vector

  • Hashtable

  • java.util.concurrent包下的类

**注1:**这里说的线程安全是类的各个方法是线程安全的,但是方法的组合并不是安全的。

注2:final修饰基本数据类型的变量时,必须赋予初始值且不能被改变,修饰引用变量时,该引用变量不能再指向其他对象

Monitor

Java对象头

以32位虚拟机位例

普通对象

数组对象

其中Mark Word结构为

每个Java对象都可以关联一个操作系统提供的Monitor对象,如果使用synchronized关键字以后,该对象的Mark Word字段会被设置只想Monitor对象的指针。

Monitor结构:

Monitor结构
  • Thread-2执行synchronized(obj)时就会成为obj关联的Monitor唯一的Owner

  • 当Thread-3,Thread-4,Thread-5也来执行synchronized(obj)时,就会进入obj关联的MonitorEntryList队列进行等待,成为BLOCKED状态。

  • 当Thread-2完成同步代码块时,会唤醒EntryList中的其他线程来竞争该Monitor,但竞争是非公平的。

  • Thread-0,Thread-1是之前获得过锁,但条件不满足,进入WatiSet,成为WAITING状态

synchronized原理

轻量级锁

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

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

  • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

  • 如果 cas 失败,有两种情况

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

轻量级锁

锁膨胀

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

    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址

    • 然后自己进入 Monitor 的 EntryList BLOCKED

锁膨胀

自旋优化

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

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • Java 7 之后不能控制是否开启自旋功能

偏向锁

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

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

偏向锁的撤销

  • 调用了被锁对象的hashCode()方法。

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

  • 调用wait()/notify()方法

批量重偏向

  • 以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

  • 将类的偏向标记关闭,之后当该类已存在的实例获得锁时,就会升级为轻量级锁该类新分配的对象的Mark Word则是无锁模式

批量撤销

  • 当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM

    就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,

    直接走轻量级锁的逻辑。

参考资料:

死磕Synchronized底层实现--概论

死磕Synchronized底层实现--偏向锁

死磕Synchronized底层实现--轻量级锁

死磕Synchronized底层实现--重量级锁

wait & notify

  • Owner 线程发现条件不满足,调用 wait() 方法,即可进入 WaitSet 变为 WAITING 状态

  • BLOCKEDWAITING 的线程都处于阻塞状态,不占用 CPU 时间片

  • BLOCKED 线程会在 Owner 线程释放锁时唤醒

  • WAITING 线程会在 Owner 线程调用 notify()notifyAll() 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

API 介绍

用法如下:

使用方式

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

  • sleep()Thread方法,而 wait()Object的方法

  • sleep()不需要强制和 synchronized 配合使用,但 wait() 需要 和 synchronized 一起用

  • sleep()在睡眠的同时,不会释放对象锁的,但 wait() 在等待的时候会释放对象锁

共同点

  • 他们的状态都是TIME_WAITING

建议使用格式

输出结果:

保护性暂停

详见模式->保护性暂停模式

该模式与join()相比较,join()必须等到线程结束,而保护性暂停模式只需要等待某线程传递回返回值。

join原理分析

join()源码分析

对于以上模式两个线程之间直接对GuardedObject进行操作,必须保证GuardedObject为同一对象,我们可以通过一个中间类统一管理所有GuardedObject

扩展

生成者/消费者模式

详见模式->生产者/消费者模式

park & unpark

属于Locksupport类中的方法,与Objectwait() & notify()相比

  • wait(),notify()notifyAll()必须配合Object Monitor使用,而park(),unprk()不用。

  • park(),unpark()是以线程为单位来阻塞唤醒线程,而notify()是以锁,并随机唤醒线程。

  • unpark()可以在park()之前执行,此后的park()则无效

park & unpark原理

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

  • 当前线程调用 Unsafe.park() 方法

    1. 检查 _counter ,如果为 0,这时,获得 _mutex互斥锁

    2. 线程进入_cond条件变量阻塞

    3. 设置 _counter = 0

  • 当现线程调用 Unsafe.unpark(thread_0)方法

    • 先调用park()的情况:

      1. 调用 Unsafe.unpark(thread_0) 方法,设置_counter = 1

      2. 唤醒 _cond 条件变量中的thread_0

      3. thread_0 恢复运行

      4. 设置 _counter = 0

    • 先调用unpark()的情况:

      1. 调用 Unsafe.unpark(thread_0) 方法,设置 _counter = 1

      2. 当前线程调用 Unsafe.park() 方法

      3. 检查_counter,如果为 1,这时线程无需阻塞,继续运行

      4. 设置 _counter = 0

Java线程间的相互转换

1584714702390

假设有线程 Thread t

**NEW --> RUNNABLE **

**情况 1 **

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

RUNNABLE <--> WAITING

**情况 2 **

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

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING

  • 调用 obj.notify()obj.notifyAll() t.interrupt()

    • 竞争锁成功,t 线程从 WAITING --> RUNNABLE

    • 竞争锁失败,t 线程从 WAITING --> BLOCKED

情况 3

  • 当前线程调用 t.join() 方法时,当前线程从RUNNABLE --> WAITING

    • 注意是当前线程在t 线程对象的监视器上等待

  • t 线程运行结束,或调用了当前线程的interrupt()时,当前线程从 WAITING --> RUNNABLE

情况 4

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING

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

RUNNABLE <--> TIMED_WAITING

**情况 5 **

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

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING

  • t 线程等待时间超过了 n 毫秒,或调用obj.notify()obj.notifyAll() t.interrupt()

  • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE

  • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING

    • 注意是当前线程在t 线程对象的监视器上等待

  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况 7

  • 当前线程调用Thread.sleep(long n),当前线程从 RUNNABLE --> TIMED_WAITING

  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE --> TIMED_WAITING

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

RUNNABLE <--> BLOCKED

情况 9

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED

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

RUNNABLE <--> TERMINATED

**情况 10 **

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

线程活跃性

死锁

两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。

四个必要条件

  • 互斥条件:一个资源每次只能被一个进程使用。

  • 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

定位死锁

可以通过jps定位进程id,再用jstack定位死锁

  • 或者使用jconsole工具

    连接运行的进程,选择线程界面查看。

活锁

任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

饥饿

一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

ReentrantLock

synchronized一样都支持可重入,即获得锁之后可以再次获得该锁。但相对于synchronizedReentrantLock具有以下特性

  • 可中断

  • 可设置超时时间

  • 可设置公平锁

  • 支持多个条件变量

基本语法:

利用ReentrantLock解决哲学家问题

输出结果:

条件变量

Synchronized也有条件变量,即waitSet,当不满足条件时进入waitSet等待。

ReentrantLock支持多个条件变量。可以指定具体唤醒哪些条件变量。

ReentrantLock修改wait& notify中的案例

共享模型之内存

Java内存模型

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

JMM体现在以下几个方面

  • 原子性-保证指令不会受到线程上下文切换的影响

  • 可见性-保证指令不会受CPU缓存的影响

  • 有序性=保证指令不会受CPU指令并行优化的影响

可见性

退不出的循环

main线程对flag变量修改对于t1线程不可见导致无法退出循环

原因:

  1. t1线程刚开始从主内存中读取了flag的值到工作内存中。

  2. 因为t1线程要频繁的从主内存中读取flag值, JIT编译器会将flag的值缓存到自己的工作内存中,以减少对主存的访问,提高效率。

  3. 1秒后main线程修改了flag的值,但是t1线程依旧从自己的工作内存中取值。

解决方法:

volatile关键字可以修饰成员变量和静态成员变量,线程操作volatile 修饰的变量都是直接操作主存。

注:volatile只能保证成员变量的可见性,并不能保证原子性。

两阶段终止模式的volatile实现

详见模式->两阶段终止模式-volatile

有序性

JVM会在不影响正确性的前提下,可以调整语句的执行顺序。

对于以上代码先后执行顺序不会对结果产生影响。

这种特性称之为指令重排,多线程下指令重排会影响正确性。

**指令级并行原理 **

  • Clock Cycle Time (时钟周期时间)

    等于主频的倒数,CPU 能 够识别的最小时间单位。

  • CPI(Cycles Per Instruction)

    指令平均时钟周期数

  • IPC (Instruction Per Clock Cycle)

    即 CPI 的倒数,表示每个时钟周期能够运行的指令数

  • **CPU 执行时间 **

    程序的CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示

现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。

指令还可以再划分成一个个阶段,例如,每条指令都可以分为:

术语参考:

  • instruction fetch (IF)

  • instruction decode (ID)

  • execute (EX)

  • memory access (MEM)

  • register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行

Java中的重排序

现有代码如下,可能存在m2()中1,2指令重排序,r1结果为0

可以通过 volatile 修饰 ready 来添加写屏障以防止 ready 的赋值操作和之前的指令进行重排

Volatile原理

保证可见性

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

  • 读屏障(lfence)保证该屏障之后对共享变量的读取,是从主存中加载。

保证有序性

  • 写屏障会确保指令重排时,写屏障之前的代码不会被重排到写屏障后面。

  • 读屏障会确保指令重排时,读屏障之后的代码不会被重排到读屏障前面。

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

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

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

double-checked locking问题

双重校验代码如下:

synchronized的代码块中也有可能存在指令重排,对于以上代码3可能存在先将引用地址赋值给singleton。如果此时有另外一个线程进入获取singleton发现不为空,于是返回了singleton并使用,但此时并没有调用singleton的构造函数。

解决

添加volatile之后代码2就不会再被重排。

happens-before

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

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

线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

线程 start 前对变量的写,对该线程开始后对该变量的读可见

线程结束前对变量的写,对其它线程得知它结束后的读可见。

线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见。

对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

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

单例模式的一些问题

实现一

实现二

共享模式之无锁

无锁解决线程安全问题

利用AtomicInteger实现线程安全

CAS & volatile

对于上面的解决办法,关键在于compareAndSet()方法,也就是CAS。

注:synchronized上锁过程中对Class Word的操作也是通过一种CAS操作实现的。

CAS 操作必须要有volatile来保证修改值和获取值对其他线程的可见性。

无锁状态下就算CAS操作失败,线程依旧在运行。synchronized中,如果没有或得到锁,或导致上下文切换,消耗较大。

但再无锁的情况下也需要CPU的运行支持,如果没有分到时间片依然会导致上下文切换。

CAS 的特点

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

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量。

  • CAS 体现的是无锁并发、无阻塞并发:

    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一

    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子对象

java.util.concurrent.atomic.*;包下提供了一些列原子对象

原子整数

以AtomicInteger为例

原子引用类型

AtomicReference包括一个泛型,可以传入任意类型对象。

虽然调用其compareAndSet(prev,next)会检测预期值,但是如果与气质被修改过再被修改回来,AtomicReference无法发现其中的修改。

AtomicStampedReference相较于AtomicReference在其构造方法中可以传入版本号,更新时也需要传入版本号以验证是否为最新一次修改。

AtomicMarkableReferenceAtomicStampedReference的简化,该方法会验证期望值是否被修改过。

原子数组

  • AtomicIntegerArray

  • AtomicLongArray

  • AtomicReferenceArray

输出结果:

字段更新器

字段更新器可以针对对象的某个域(Field)即属性,进行原子操作,同样必须配合volatile使用。

原子累加器

相较于AtomicLong,累加器LongAdder效率更高。

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

LongAdder

CAS锁

使用CAS也可也实现上锁

伪共享

由于CPU访问内存速度较慢,普遍采用的方案是在内存和CPU直接添加缓存,如果CPU没有从缓存中读取到需要的数据,再到内存中读取。

  • 缓存是以缓存行为单位存储的,一般是64bit

  • 多核CPU必须为各自设置一份缓存。

  • 要保证数据的一致性就必须在一个CPU更新数据后,让其他CPU重新到内存取得对应的缓存行。

防止为共享

  • @sun.misc.Contended注解会在对象或者字段前后各添加128字节的padding,CPU将对象预读到缓存时,就会将该对象预读到不同的缓存行,以防止一个数据的修改导致整个缓存行失效。

  • LongAdder中的Cell累加单元

LongAdder源码解析

Long Adder类中几个关键域:

add(long x)

longAccumulate()

2:

1:

sum()

Unsafe

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

利用Unsafe实现AtomicInteger

共享模式之不可变

不可变设计

String也是不可变的:

final原理

final变量的设置

  • final修饰的对象赋值时,会为其添加写屏障,保证对final对象读取值时出现读取到初始值。

final变量的获取

  • 对添加了final修饰的对象访问时,会将其复制一份到栈中。

  • 如果对象较大,会复制到使用类的常量池中。

  • 如果没有添加final修饰则是到堆中查找。

享元模式

享元模式(Flyweight pattern):尽可能的对相同值对象进行共享,已达到最小内存的使用。

包装类中都体现了享元模式。以Long为例

注:

  • Byte, Short, Long 缓存的范围都是 -128~127

  • Character 缓存的范围是 0~127

  • Integer的默认范围是 -128~127

  • 最小值不能变

  • 但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变

  • Boolean 缓存了 TRUEFALSE

享元模式实现连接池

详见模式->享元模式

无状态

成员变量保存的数据称为状态信息,没有成员变量称为无状态。

没有任何成员变量的类是线程安全的。

共享模式之工具

自定义线程池

自定义线程池需要在线程池中创建多个线程实例执行BlockQueue中的任务,同时BlockQueue的任务数量超过capcity时,添加拒绝策略。

阻塞队列:

策略接口:

线程池:

测试代码:

输出结果:

ThreadPoolExecutor

线程池状态

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

这些信息存储再要给原子变量ctl中,以实现一次CAS操作完成赋值。

状态名
高三位
接收新任务
处理阻塞队列任务
说明

RUNNING

111

Y

Y

SHUTDOWN

000

N

Y

不会接收新任务

STOP

001

N

N

会中断正在执行的任务

TIDYING

010

-

-

即将进入终结

TERMINATED

011

-

-

终结状态

构造方法

  • corePoolSize: 核心线程数

  • maximumPoolSize:最大线程数

  • keepAliveTime:生存时间-针对救急线程

  • unit:时间单位

  • workQueue:阻塞队列

  • thhreadFactory:线程工厂-可以位线程创建时起一个好名字

  • handler:拒绝策略,如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 JDK 提供了 4 种实现,其它著名框架也提供了实现

    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略

    • CallerRunsPolicy 让调用者运行任务

    • DiscardPolicy 放弃本次任务

    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之

    • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题

    • Netty 的实现,是创建一个新线程来执行任务

    • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略

    • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

救急线程数 = maximumPoolSize - corePoolSize,当阻塞队列满时,救急线程会被创建。在其完成任务后,等待一定时间会被销毁。

固定大小线程池newFixedThreadPool

缓存线程池newCachedThreadPool

核心线程数为0,最大线程为MAX_VALUE意味着创建的全是救急线程。

队列采用了 SynchronousQueue ,它没有容量,必须要有线程取任务,才可以存进去。

输出结果:

单线程连接池newSingleThreadExecutor

用于多个任务需要串行执行时。线程固定为1,任务执行完毕线程不会被释放。

  • 自己创建的单线程执行任务如果失败,之后的任务不会执行。单线程池会创建一个新的线程来继续执行之后的任务。

  • newSingleThreadExecutor()返回的是线程池的装饰类FinalizableDelegatedExecutorService,在该类中可以限制调用线程池特有的方法。如在固定线程池中,直接返回的是ThreadPoolExecutor对象,可以强转后调用setCoreSize()等方法修改其中参数。而装饰类则不行。


模式

两阶段终止模式

两阶段终止模式(Two Phase Termination):在一个线程中终止另外一个线程时,给予被终止线程处理终止前的一些操作。

保护性暂停

  • 用于结果在线程之间的传递。

  • 如果有结果不断的从一个线程到另一个线程可以使用消息队列。

  • JDK中的joinFutrue都是采用此模式。

生产者消费者模式

  • 相较于保护性暂停模式,不需要产生结果和消费结果的线程之间一一对应。

  • 消费队列可以用来平衡生成和消费的线程资源。

  • JDK中的各种阻塞队列,采用的就是这种模式。

输出结果:

两阶段终止模式-volatile

对于两阶段终止模式要点在于判断退出条件,对于之前的模式,通过线程的interrupted判断是否退出。通过volatile修饰的变量,对于线程间是可见的可以用作两阶段终止模式的退出条件判断。

输出结果:

享元模式

用享元模式实现连接池,请求时从连接池获取连接,结束后归还连接。

输出结果:

最后更新于

这有帮助吗?