全网最详细并发编程(1)---进阶篇

全网最详细并发编程(1)—进阶篇

在这里插入图片描述

一、共享模型之内存

  之前的入门篇主要讲解的Monitor主要关注的是访问共享变量时,保证临界区代码的原子性,本文我们将进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题。

1.1 、 Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
在这里插入图片描述
1.2、可见性

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

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

为什么呢?分析一下:

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

解决方法

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

可见性 vs 原子性
  前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:

getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错。

// 假设i的初始值为0 
getstatic i // 线程2-获取静态变量i的值 线程内i=0 
getstatic i // 线程1-获取静态变量i的值 线程内i=0 
iconst_1 // 线程1-准备常量1 
iadd // 线程1-自增 线程内i=1 
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 
iconst_1 // 线程2-准备常量1 
isub // 线程2-自减 线程内i=-1 
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

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

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?
:因为System.out.println()底层加了synchronize锁从而保证了可见性。

模式之两阶段终止(使用Bulking犹豫模式volatile改进)

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TwoPhaseTermination")
public class Test13 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        tpt.start();
        tpt.start();

        /*Thread.sleep(3500);
        log.debug("停止监控");
        tpt.stop();*/
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    // 监控线程
    private Thread monitorThread;
    // 停止标记
    private volatile boolean stop = false;  //设置成volatile 因为只有一个线程对stop进行操作
    // 判断是否执行过 start 方法
    private boolean starting = false;  

    // 启动监控线程
    public void start() {
        synchronized (this) {    //需要加锁防止多个线程同时改变starting状态
            if (starting) { // false
                return;
            }
            starting = true;
        }
        monitorThread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 是否被打断
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                }
            }
        }, "monitor");
        monitorThread.start();
    }

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

1.3 、有序性-----指令重排序
在这里插入图片描述
测试:

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {

    int num = 0;
    boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

}

在这里插入图片描述
结果:
在这里插入图片描述
禁止重排序:

  volatile boolean ready = false;//在ready上加volatile关键字可以保证ready=true以上的代码不被指令重排序

二、volatile原理

1、volatile保证可见性原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
  ● 对 volatile 变量的写指令后会加入写屏障
  ● 对 volatile 变量的读指令前会加入读屏障

(1) 如何保证可见性

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

public void actor2(I_Result r) {
	 num = 2;
	 ready = true; // ready 是 volatile 赋值带写屏障
	 // 写屏障
}

  ● 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

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

在这里插入图片描述
(2)如何保证有序性

  ● 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  ●读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(3). double-checked locking 问题
以著名的 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 方法对应的字节码为:

0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
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 "<init>":()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

在这里插入图片描述
也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
在这里插入图片描述在这里插入图片描述
double-checked locking 解决

 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;
	 }
}

以上解决了:
① 使用synchronized 对Singleton.class进行了加synchronized 锁,解决了多个线程访问到 INSTANCE = new Singleton();会创建多个单例了。
② 对Singleton实例INSTANCE 加了volatile关键字从而保证不会指令重排。

小结:① synchronized 既能保证原子性、可见性、有序性,其中有序性是在该共享变量完全被synchronized 所接管(包括共享变量的读写操作),上面的例子中synchronized 外面的 if (INSTANCE == null) 中的INSTANCE读操作没有被synchronized 接管,因此无法保证INSTANCE共享变量的有序性(即不能防止指令重排)。
   ② 对共享变量加volatile关键字可以保证可见性有序性,但是不能保证原子性(即不能防止指令交错)。
在这里插入图片描述
(4) happens-before
  happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

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

static int x;
static Object m = new Object();
new Thread(()->{
	 synchronized(m) {
	 x = 10;
 }
},"t1").start();
new Thread(()->{
	 synchronized(m) {
	 System.out.println(x);
 }
},"t2").start();

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

volatile static int x;
new Thread(()->{
 	x = 10;
},"t1").start();
new Thread(()->{
 	System.out.println(x);
},"t2").start();

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

static int x; x = 10;
new Thread(()->{
 	System.out.println(x);
},"t2").start();

线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)

static int x;
Thread t1 = new Thread(()->{
 	x = 10;
},"t1");
	t1.start();
	t1.join();
	System.out.println(x)

线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
在这里插入图片描述

具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
在这里插入图片描述
(4) 线程安全单例习题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
三、ThreadLocal的使用
   ThreadLocal:用于实现线程内部的数据共享叫线程共享(对于同一个线程内部数据一致),即相同的一段代码 多个线程来执行 ,每个线程使用的数据只与当前线程有关。
   实现原理:ThreadLocal相当于一个map 当前线程 存储当前的变量的时候 map.put(确定线程的唯一值(比如变量名称),变量),然后获取的时候直接拿过来就行
   一般用法:定义一个全局变量ThreadLoacl t 将新建线程要使用的变量 存进去 比如:

1.当存储的为基本变量或者包装对象时

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

    /*定义一个全局变量 来存放线程需要的变量*/
    public static ThreadLocal<Integer> ti = new ThreadLocal<Integer>();
    public static void main(String[] args) {
        /*创建两个线程*/
        for(int i=0; i<2;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Double d = Math.random()*10;
                    /*存入当前线程独有的值*/
                    ti.set(d.intValue());
                    new A().get();
                    new B().get();
                }
            }).start();
        }
    }
    static class A{
        public void get(){
            /*取得当前线程所需要的值*/
            System.out.println("A"+ti.get());
        }
    }
    static class B{
        public void get(){
            /*取得当前线程所需要的值*/
            System.out.println("B"+ti.get());
        }
    }
}

输出:

A5
B5
A4
B4

2.当存储的为对象时 就是数据集合 比如前台传过来的参数,每一个人传过来的 都是这个人独有的,才能保证数据准确性,抽取业务数据为一个对象:

class ThreadLocalDemo{
    /*把线程相关的部分内聚到 类里面  相当于map 每个类是对应key*/
    private static ThreadLocal<ThreadLocalDemo> t = new ThreadLocal<ThreadLocalDemo>();
    private ThreadLocalDemo(){}
    public static ThreadLocalDemo getThreadInstance(){
        ThreadLocalDemo threadLocalDemo  = t.get();
        if(null == threadLocalDemo){//当前线程无绑定的对象时,直接绑定一个新的对象
            threadLocalDemo = new ThreadLocalDemo();
            t.set(threadLocalDemo);
        }
        return threadLocalDemo;
    }
 
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

3、ThreadLocal 和 synchronized的比较

ThreadLocal

  1. 先说下 ThreadLocal不能解决多线程间共享数据,他是一个隔离多线程间共享数据的好帮手
  2. ThreadLocal是本地线程共享数据
  3. 他是以空间换线程的安全性(每个线程都有一个副本)

synchronized

  1. 解决多线程间共享数据安全的问题
  2. 他是以时间换空间(monitor锁)的方案,效率差(适用并发量小的时候)

注:
  ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别:
  synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问;
  而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
  而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
  Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离


欢迎关注公众号Java技术大联盟,会不定期分享BAT面试资料等福利。

在这里插入图片描述


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值