五.共享模型之内存
1、Java 内存模型(JMM)
JMM 即 Java Memory Model,它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
JMM 体现在以下几个方面
原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受 cpu 缓存的影响
有序性 - 保证指令不会受 cpu 指令并行优化的影响
2、可见性
public static boolean run = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(run) {
}
}, "t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1 Stop");
run = false;
}
首先 t1 线程运行,然后过一秒,主线程设置 run 的值为 false,想让 t1 线程停止下来,但是 t1 线程并没有停,这就是可见性问题
图
大致就是JIT即时编译器的优化让线程没办法获取到最新的主内存的数据的变化
/////////////////////////////
解决方法: 对变量加上Volatile关键字
public static volatile boolean run = true; // 保证内存的可见性
原理是: 不会被JIT优化,每次读取都是从主内存中读取,而不是创建一个工作内存内的高速缓存区,这样会损失一部分效率,但是保证了每次都能读取到最新的数据值,syn锁也能保证数据的可见性
可见性vs原子性:
Volatile只能保证可见性,但是不能保证原子性,对于i++和i–造成的指令交错问题,我们不能使用volatile来保证线程安全,加了也会产生交错
Volatile仅用在一个写线程,多个读线程的情况,用Volatile比syn的性能好,运行也更快
3.有序性
1)指令重排
指令重排简单来说可以,在程序结果不受影响的前提下,可以调整指令语句执行顺序。
多线程下指令重排会影响正确性。
比如i=1,j=2,jvm底层运行可能是先对j赋值后再对i赋值
2)多线程下指令重排问题
Jvm会对串行执行的代码进行优化,实现CPU指令级别的并发度
图:流水线技术
本质上流水线技术不能缩短单条指令的执行时间,但变相的提高了指令的吞吐率
重排序出错的例子:
如果按图中的例子出现上下颠倒,则会使得先进入if语句,最后r1的值赋为0
3)解决方法
volatile 修饰的变量,可以禁用指令重排,禁止的是加 volatile 关键字变量之前的代码重排序
4、volatile 原理**
保证了共享变量的可见性(不让jit即时编译器优化)和有序性(禁止指令重排)
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对 volatile 变量的写指令后会加入写屏障
对 volatile 变量的读指令前会加入读屏障
--------如何保证可见性
写屏障 保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是被 volatile 修饰的,赋值带写屏障
// 写屏障
}
读屏障 保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// ready是被 volatile 修饰的,读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
图:
--------如何保证有序性
写屏障 会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是被 volatile 修饰的,赋值带写屏障
// 写屏障
}
读屏障 会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) {
// 读屏障
// ready 是被 volatile 修饰的,读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
注意:volatile 不能解决指令交错
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其它线程的读跑到它前面去。
而有序性的保证也只是保证了本线程内相关代码不被重排序
5.double-checked locking
问题:
// 最开始的单例模式是这样的
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 首次访问会同步,而之后的使用不用进入synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
但是这个性能很低,对第一次调用来说,加锁可以保护多线程下的线程安全,但当创建了实例对象后,以后每次获取都需要加锁再判断,无疑效率非常低,因为除了第一次以外,以后的每次调用都不会出现线程出错的情况,没必要加锁。
因此出现了double-checked locking 双重判断的优化
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
懒惰实例化
首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外,但在多线程环境下,上面的代码是有问题的
getInstance 方法对应的字节码为:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
// ldc是获得类对象
6: ldc #3 // class cn/itcast/n5/Singleton
// 复制操作数栈栈顶的值放入栈顶, 将类对象的引用地址复制了一份
8: dup
// 操作数栈栈顶的值弹出,即将对象的引用地址存到局部变量表中
// 将类对象的引用地址存储了一份,是为了将来解锁用
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
// 新建一个实例
17: new #3 // class cn/itcast/n5/Singleton
// 复制了一个实例的引用
20: dup
// 通过这个复制的引用调用它的构造方法
21: invokespecial #4 // Method “”😦)V
// 最开始的这个引用用来进行赋值操作
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
Sync锁内保证有序性的前提是变量都被sync锁住,变量如果在外面也引用了,那sync就不能保证这个变量的有序性,会出现指令重排
4)double-checked locking 解决 对instance变量加上volatile
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
6.Happens-before
下面说的变量都是指成员变量或静态成员变量
1)线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
2)线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
3)线程 start 前对变量的写,对该线程开始后对该变量的读可见
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
4)线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
5)线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
6)对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
7)具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
7.练习总结
1)balking 模式习题
希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
volatile 可以保存线程的可见性,有序性,但是不能保证原子性,doInit 方法没加锁,可能会被调用多次,应该对init()方法加sync锁,保证原子性
2)DCL懒汉式 实现一
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
3)DCL懒汉式 最终版
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?为了防止重排序问题
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现1, 说出这样做的意义:提高了效率,第二次之后就不用加锁了
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗?
这是为了第一次判断时的并发问题。
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
小结DCL:
1对instance加上volatile 防止sync代码块内发生重排序
2第一个if判断是优化提高效率的作用,让第二次获取不需要加锁操作
3加上sync锁是为了保证第一次创建的时候线程操作的原子性
4第二个if是为了防止等待在entryset的线程一进入就创建singleton
实现5:静态内部类懒汉式
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
结论
本章重点讲解了 JMM 中的
- 1.可见性 - 由 JVM 缓存优化引起
- 2.有序性 - 由 JVM 指令重排序优化引起
- 3.happens-before 规则
- 4.volatile原理
- 5.两阶段终止模式的 volatile 改进
- 6.同步模式之 balking(单例模式)
六.共享模型之无锁并发
1、无锁解决线程安全问题
如下代码,通过 synchronized 解决线程安全问题。
public class Code_04_UnsafeTest {
public static void main(String[] args) {
Account acount = new AccountUnsafe(10000);
Account.demo(acount);
}
}
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return this.balance;
}
@Override
public void withdraw(Integer amount) {
synchronized (this) { // 加锁。
this.balance -= amount;
}
}
}
interface Account {
// 获取金额的方法
Integer getBalance();
// 取款的方法
void withdraw(Integer amount);
static void demo(Account account) {
List<Thread> list = new ArrayList<>();
long start = System.nanoTime();
for(int i = 0; i < 1000; i++) {
list.add(new Thread(() -> {
account.withdraw(10);
}));
}
list.forEach(Thread::start);
list.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end-start)/1000_000 + " ms");
}
}
如上代码加锁会造成线程堵塞,堵塞的时间取决于临界区代码执行的时间,这使用加锁的性能不高,我们可以使用无锁来解决此问题。
class AccountSafe implements Account{
AtomicInteger atomicInteger ;
public AccountSafe(Integer balance){
this.atomicInteger = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return atomicInteger.get();
}
@Override
public void withdraw(Integer amount) {
// 核心代码
while (true){
int pre = getBalance();
int next = pre - amount;
if (atomicInteger.compareAndSet(pre,next)){
break;
}
}
}
}
2、CAS 与 volatile
1)cas
前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
其中的关键是 compareAndSwap(比较并设置值),它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
图:
如图所示,它的工作流程如下:
当一个线程要去修改 Account 对象中的值时,先获取值 preVal(调用get方法),然后再将其设置为新的值 nextVal(调用 cas 方法)。在调用 cas 方法时,会将 pre 与 Account 中的余额进行比较。
如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
如果两者不相等,就不设置值,重新获取值 preVal(调用get方法),然后 再将其设置为新的值 nextVal(调用cas方法),直到修改成功为止。
2)volatile
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
注意
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到新值,但不能解决指令交错问题(不能保证原子性)
CAS 是原子性操作借助 volatile 读取到共享变量的新值来实现【比较并交换】的效果
3)为什么无锁效率高
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
线程数小于CPU核心数,用CAS是非常合适的
4)CAS 的特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发(运行的时候上下文切换非常的少,遇到失败不会变成blocking停下来,而是while循环一直在跑),请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响
3、原子整数
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
以 AtomicInteger 为例测试使用它的api接口:
public class Test7 {
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(0);
//简洁的原子操作
System.out.println(a.incrementAndGet()); // ++i
System.out.println(a.getAndIncrement()); // i++
//变化多个值
System.out.println(a.getAndAdd(5)); //2, 7
System.out.println(a.addAndGet(5)); //12, 12
//实现乘除等灵活运算
a.updateAndGet( value-> value*10); //12*10 = 120 先运算后获取值
//updateAndGet实现
while(true) {
int prev = a.get();
int next = prev*10;
if(a.compareAndSet(prev,next)){
break;
}
}
//获取值
System.out.println(a.get());
}
}
updateAndSet的底层实现源码:
4、原子引用
为什么需要原子引用类型?保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人)。
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。
AtomicReference:引用类型原子类
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起。
1)AtomicReference
class DecimalAccountCas implements DecimalAccount {
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal preVal = balance.get();
BigDecimal nextVal = preVal.subtract(amount);
if(balance.compareAndSet(preVal, nextVal)) {
break;
}
}
}
}
2)ABA 问题
看如下代码:
public static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
String preVal = ref.get();
other();
TimeUnit.SECONDS.sleep(1);
log.debug("change A->C {}", ref.compareAndSet(preVal, "C"));
}
private static void other() throws InterruptedException {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
输出结果显示主线程不能感知到ABA的执行过程,compareandset的返回值是true
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况
这时,需要再加一个版本号。使用AtomicStampedReference来解决。
3)AtomicStampedReference
Stamped 是时间戳的意思,可以理解为版本号
对上面的atomicRefrence改成AtomicStampedReference即可解决ABA问题
这个类的compareAndSet不仅要比较获取到的值,而且要比较版本号
@Slf4j(topic = "aba.test")
public class Test8 {
public static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
String preVal = ref.getReference(); // 获取最新值
int stamp = ref.getStamp(); //获取最新版本号
log.info("main 拿到的版本号{}",stamp);
other();
Thread.sleep(1000);
log.info("修改后的版本号{}",ref.getStamp());
log.info("change A->C:{}",ref.compareAndSet(preVal,"C",stamp,stamp+1));
}
private static void other() throws InterruptedException {
new Thread(()->{
int stamp = ref.getStamp();
log.info("{}",stamp);
log.info("change A->B {}",ref.compareAndSet(ref.getReference(),"B",stamp,stamp+1));
}).start();
Thread.sleep(1000);
new Thread(()->{
int stamp = ref.getStamp();
log.info("{}",stamp);
log.info("change B->A {}",ref.compareAndSet(ref.getReference(),"A",stamp,stamp+1));
}).start();
}
}
测试成功
4)AtomicMarkableReference
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A ->C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference 。
5、原子数组
使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray :引用类型数组原子类
上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍,代码如下:
public class Code_10_AtomicArrayTest {
public static void main(String[] args) throws InterruptedException {
/**
* 结果如下:
* [9934, 9938, 9940, 9931, 9935, 9933, 9944, 9942, 9939, 9940]
* [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
*/
demo(
() -> new int[10],
(array) -> array.length,
(array, index) -> array[index]++,
(array) -> System.out.println(Arrays.toString(array))
);
demo(
() -> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
(array) -> System.out.println(array)
);
}
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer) {
ArrayList<Thread> ts = new ArrayList<>(); // 创建集合
T array = arraySupplier.get(); // 获取数组
int length = lengthFun.apply(array); // 获取数组的长度
for(int i = 0; i < length; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j % length);
}
}));
}
ts.forEach(Thread::start);
ts.forEach((thread) -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
printConsumer.accept(array);
}
}
AtomicIntegerArray原子数组保证了元素的线程安全
6、字段更新器
AtomicReferenceFieldUpdater // 域 字段
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
注意:利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
测试代码:
public class Code_11_AtomicReferenceFieldUpdaterTest {
public static AtomicReferenceFieldUpdater ref =
AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
public static void main(String[] args) throws InterruptedException {
Student student = new Student();
new Thread(() -> {
System.out.println(ref.compareAndSet(student,null,"list"));
}).start();
System.out.println(ref.compareAndSet(student, null, "张三"));
System.out.println(student);
}
}
class Student {
public volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
} }
字段更新器就是为了保证类中某个属性线程安全问题。
7、原子累加器
1)AtomicLong Vs LongAdder
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
demo(() -> new AtomicLong(0), //正常的原子类
(ref) -> ref.getAndIncrement());
}
for(int i = 0; i < 5; i++) {
demo(() -> new LongAdder(), //jdk8新的自增原子类
(ref) -> ref.increment());
}
}
private static <T> void demo(Supplier<T> supplier, Consumer<T> consumer) {
ArrayList<Thread> list = new ArrayList<>();
T adder = supplier.get();
// 4 个线程,每人累加 50 万
for (int i = 0; i < 4; i++) {
list.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
consumer.accept(adder);
}
}));
}
long start = System.nanoTime();
list.forEach(t -> t.start());
list.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(adder + " cost:" + (end - start)/1000_000);
}
执行代码后,发现使用 LongAdder 比 AtomicLong 快2,3倍,使用 LongAdder 性能提升的原因很简单:
设置多个累加单元(但不会超过cpu的核心数),Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
8、LongAdder原理
LongAdder 类有几个关键域
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;
原理之伪共享
其中 Cell 即为累加单元
// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
}
下面讨论 @sun.misc.Contended 注解的重要意义
得从缓存说起,缓存与内存的速度比较
因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。缓存离 cpu 越近速度越快。 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long),缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效。
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因 此缓存行可以存下 2 个的 Cell 对象。这样问题来了: Core-0 要修改 Cell[0],Core-1 要修改 Cell[1]
无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
3)add 方法分析
LongAdder 进行累加操作是调用 increment 方法,它又调用 add 方法。
public void increment() {
add(1L);
}
第一步:add 方法分析,流程图如下
第二步:longAccumulate 方法分析,流程图如下:
4)sum 方法分析
获取最终结果通过 sum 方法,将各个累加单元的值加起来就得到了总的结果。
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
5、Unsafe
像原子类的getandadd等原子操作方法底层都是调用unsafe对象来操作
1)Unsafe 对象的获取
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。LockSupport 的 park 方法,cas 相关的方法底层都是通过Unsafe类来实现的。
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true); //设置运行访问私有变量
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
System.out.println(unsafe);
2)Unsafe 模拟实现 cas 操作
public class Code_14_UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 创建 unsafe 对象
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
// 拿到偏移量
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
// 进行 cas 操作
Teacher teacher = new Teacher();
unsafe.compareAndSwapLong(teacher, idOffset, 0, 100);
unsafe.compareAndSwapObject(teacher, nameOffset, null, "lisi");
System.out.println(teacher);
}
}
@Data
class Teacher {
private volatile int id;
private volatile String name;
}
小结
1.CAS 与 volatile
2.juc 包下 API
- .原子整数
- .原子引用
- .原子数组
- .字段更新器
- .原子累加器
3.Unsafe
4.原理方面 - .LongAdder 源码
- .伪共享
七、共享模型之不可变
不可变类都是线程安全的,因为不存在修改数据的操作,不会被同时操作
例如String类的所有API方法都是线程安全的
1、日期转换的问题
由于 SimpleDateFormat 不是线程安全的,有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果
代码演示:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
在代码中,我们synchronized(sdf)锁住sdf这个日期类,则不会出现异常,是通过互斥来阻塞其他线程解决上下文切换带来的指令交错的错误,但是会使得执行效率很低,推荐使用 不可变日期对象DateTimeFormatter
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
log.debug("{}", date);
}).start(); }
2、不可变设计
String类中不可变的体现
public final class String //保证不会被继承,则方法不会被覆盖
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[]; //引用不可修改
private int hash; //hash私有,不可修改
}
保护性拷贝: 例如subString方法
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 上面是一些校验,下面才是真正的创建新的String对象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen); //创建了新的对象
}
3、模式之享元
1)简介
简介定义英文名称:Flyweight pattern.
当需要重用数量有限的同一类对象时,归类为:Structual patterns
2)体现
包装类
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法。
例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象:
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset]; //从缓存池中获取
}
return new Long(l);
}
Byte, Short, Long 缓存的范围都是 -128~127
Character 缓存的范围是 0~127
Integer 的默认范围是 -128~127,最小值不能变,但最大值可以通过调整虚拟机参数 "-Djava.lang.Integer.IntegerCache.high "来改变
Boolean 缓存了 TRUE 和 FALSE
String串池在JVM笔记内有整理,可以去查看,BigDecimal,BigInteger也是不可变了,优化内存也是使用了享元共享对象模式
不是说不可变类在多线程操作就不需要使用原子引用AtomicRrefence,不可变类只能保证单个线程下的每个方法是原子性的线程安全,但是不能保证多个方法调用的原子性。
3)DIY 实现简单的数据库连接池
如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,原理也是享元模式
以上实现没有考虑:
连接的动态增长与收缩 (connection的数组的扩容)
连接保活(可用性检测)
等待超时处理 (获取不到就停止wait)
分布式 hash
4、final的原理
1)设置 final 变量的原理
理解了 volatile 原理,再对比 final 的实现就比较简单了
public class TestFinal {
final int a = 20;
}
字节码
0: aload_0
1: invokespecial #1 // Method java/lang/Object.""😦)V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 写屏障 在写屏障之前不会指令重排到后面,写屏障之后会同步到主存
10: return
2)获取 final 变量的原理
参考如下
https://weihubeats.blog.youkuaiyun.com/article/details/87708198?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control
引用此博客的一个例子:
public class Main {
public static void main(String[] args) {
String a = “xiaomeng2”;
final String b = “xiaomeng”;
String d = “xiaomeng”;
String c = b + 2;
String e = d + 2;
System.out.println((a == c));
System.out.println((a == e));
}
}
结果是:true false
原因是final修饰的变量b是一个常量,b+2的结果也是一个常量,所以和常量a比较,都是字符串常量池内的同一个字符串
而变量d的创建不是一个常量,而是一个堆的字符串对象,持有的是引用,所以最终e也是一个堆的字符串对象,不等于常量池内的字符串
结论
- 不可变类使用
- 不可变类设计
- 原理方面:final
- 模式方面
- 享元模式-> 设置线程池
八.共享模型之并发工具
1、自定义线程池
优点:减少创建线程的过程,降低线程的上下文切换,充分利用已有线程
2、ThreadPoolExecutor
1)线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量,ThreadPoolExecutor 类中的线程状态变量如下:
状态名称 高3位 描述
RUNNING 111 接收新任务,同时处理任务队列中的任务
SHUTDOWN 000 不接受新任务,但是处理任务队列中的任务
STOP 001 中断正在执行的任务,同时抛弃阻塞队列中的任务
TIDYING 010 任务执行完毕,活动线程为0时,即将进入终结阶段
TERMINATED 011 终结状态
线程池状态和线程池中线程的数量由一个原子整型ctl来共同表示
3)构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
构造参数解释:
corePoolSize:核心线程数
maximumPoolSize:最大线程数
maximumPoolSize - corePoolSize = 救急线程数
keepAliveTime:救急线程空闲时的最大生存时间
unit:时间单位
workQueue:阻塞队列(存放任务)
有界阻塞队列 ArrayBlockingQueue
无界阻塞队列 LinkedBlockingQueue
最多只有一个同步元素的队列 SynchronousQueue
优先队列 PriorityBlockingQueue
threadFactory:线程工厂(给线程取名字)
handler:拒绝策略
工作方式:
1 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
2 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 阻塞队列排队,直到有空闲的线程。
3 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线 程来救急。
4 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 下面的前 4 种实现,其它著名框架也提供了实现
- ThreadPoolExecutor.AbortPolicy 让调用者抛出异 常,这是默认策略
- ThreadPoolExecutor.CallerRunsPolicy 让调用者运行任务
- ThreadPoolExecutor.DiscardPolicy 放弃本次任务
- ThreadPoolExecutor.DiscardOldestPolicy放弃队列中最早任务 本任务取而代之
Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方 便定位问题
Netty 的实现,是创建一个新线程来执行任务
ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义 的拒绝策略
PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝 策略
5 当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束救急线程节省资源,这个时间由 keepAliveTime 和 unit 来控制。
有界队列才有所谓的救急线程的概念
newFixedThreadPool
这个是 Executors 类提供的静态的工厂方法来创建线程池!Executors 是 Executor 框架的工具类,newFixedThreadPool 创建的是固定大小的线程池。
实现代码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
测试代码:
// 创建大小为 2 的固定线程池, 自定义线程名称
ExecutorService executorService =
Executors.newFixedThreadPool(2,newThreadFactory() {
private AtomicInteger atomicInteger = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "my_thread_" + atomicInteger.getAndIncrement());
}
});
// 开 3 个线程, 线程池大小为 2 , 第三个线程执行时, 如果前两个线程任务没执行完, 会加入任务队列.
executorService.execute(() -> {
log.info("1");
});
executorService.execute(() -> {
log.info("2");
});
executorService.execute(() -> {
log.info("3");
});
特点:
核心线程数 == 最大线程数(没有救急线程被创建,因此也无需超时时间
阻塞队列是无界的(LinkedBlockingQueue),可以放任意数量的任务
适用于任务量已知,相对耗时的任务
newCachedThreadPool
ExecutorService executorService = Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //没有核心线程
60L, TimeUnit.SECONDS, //全员救急线程,超时时间为60秒
new SynchronousQueue());
}
特点
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
全部都是救急线程(60s 后没有任务就回收)
救急线程可以无限创建 - 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)SynchronousQueue
- 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。
- 适合任务数比较密集,但每个任务执行时间较短的情况
5)newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1, //有核心线程,没有超时线程
0L, TimeUnit.MILLISECONDS, //没有救急线程,因此不需要设置超时间
new LinkedBlockingQueue()));
}
使用场景:
1 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
和自己创建单线程执行任务的区别:
自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
Executors.newSingleThreadExecutor() 线程个数始终为 1 ,不能修改
FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因 此不能调用 ThreadPoolExecutor 中特有的方法
和Executors.newFixedThreadPool(1) 初始时为1时的区别:
Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
6)提交任务
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果,Future的原理就是利用我们之前讲到的保护性暂停模式来接受返回结果的,主线程可以执行 FutureTask.get()方法来等待任务执行完成
Future submit(Callable task);
// 提交 tasks 中所有任务
List<Future> invokeAll(Collection<? extends Callable> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
List<Future> invokeAll(Collection<? extends Callable> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
T invokeAny(Collection<? extends Callable> tasks)
throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
T invokeAny(Collection<? extends Callable> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
7)关闭线程池
shutdown
/* 线程池状态变为 SHUTDOWN
- 不会接收新任务
- **但已提交任务会执行完,包括等待队列里面的
- 此方法不会阻塞调用线程的执行** */
shutdownNow
/* 线程池状态变为 STOP
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务 */
其它方法
// 不在 RUNNING 状态的线程池,此方法就返回 true
boolean isShutdown();
// 线程池状态是否是 TERMINATED
boolean isTerminated();
// 调用 shutdown 后,由于调用使线程结束线程的方法是异步的并不会等待所有任务运行结束就返回,因此如果它想在线程池 TERMINATED 后做些其它事情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
创建多少线程池合适
CPU核数+1 +1是保证CPU时钟周期不被浪费
ScheduledThreadPool
在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用
但 由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
scheduleAtFixedRate 方法的使用,是 一段时间 的 期间
scheduleWithFixedDelay 方法的使用,是 一段时间 的 间隔
9)正确处理执行任务异常
可以发现,如果线程池中的线程执行任务时,如果任务抛出了异常,默认是中断执行该任务而不是抛出异常或者打印异常信息。
方法1:主动捉异常
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
try {
log.debug(“task1”);
int i = 1 / 0;
} catch (Exception e) {
log.error(“error:”, e);
}
});
方法2:使用 Future,错误信息都被封装进submit方法的返回方法中
ExecutorService pool = Executors.newFixedThreadPool(1);
Future f = pool.submit(() -> {
log.debug(“task1”);
int i = 1 / 0;
return true;
});
log.debug(“result:{}”, f.get());
10)Tomcat 线程池
LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore
Acceptor 只负责【接收新的 socket 连接】接收
Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】 读取
一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
Executor 线程池中的工作线程最终负责【处理请求】 处理
Tomcat不同于ThreadPoolExecutor的是:在超过设置的最大线程数后,不会立即抛出拒绝策略的异常,而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常。
Connector 配置如下:
Executor 线程池配置如下:
虽然是无界的队列,但还是会用到救济队列,当提交任务大于核心线程数并小于最大线程数,则会开启救急线程来工作,如果大于最大线程,则会加入队列
3.Fork/join
把单线程任务通过“**分治”**的思想,拆分成多线程并行执行任务
如果使用递归方法,则每个线程都需要等待前一个线程执行完返回结果才能执行,这样其实并行度不高,举例来说:
对 1到5 的累加计算过程:
如果使用分治法,则会有效的减少依赖其他线程结果的情况
分成1+2 3 4+5 这样任务的并行度就高了
执行结果