volatile(二)模式与规则

目录

两阶段终止

说明 

利用共享标记打断 

Balking 模式

DCL 优化

线程不安全 —— 懒汉式单例

线程安全 —— 懒汉式单例

DCL —— 懒汉式单例

DCL 简介

DCL 实现单例(不安全) 

DCL 实现单例(安全) 

总结 

happens - before

定义

规则总结 

volatile 场景使用 


 

两阶段终止

说明 

Two Phase Termination 在线程 t1 中优雅地终止线程 t2

错误方法:

① 使用线程对象的 stop 方法,会真正杀死线程,但是线程持有锁的话,它就无法释放,其他线程也就无法获得锁

System.exit(int) 会将整个进程杀死

利用共享标记打断 

@Slf4j(topic = "c.TwoPhaseTermination")
public class TwoPhaseTermination {
    private Thread thread;
    private volatile boolean interrupted = false;

    public final void start(){
        thread = new Thread(() -> {
            log.debug("正常启动......");
            while (true){
                if(interrupted){
                    log.debug("结束运行......");
                    break;
                }
                try {
                    log.debug("正常运行......");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }, "执行线程");
        thread.start();
    }

    public final void stop(){
        interrupted = true;
        thread.interrupt();
    }

    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        log.debug("4s 后,关闭");
        Thread.sleep(4000);
        tpt.stop();
    }
}

此处 interrupted 标记,会被“执行线程”反复读取,需要保证它的可见性

Balking 模式

犹豫模式;用于一个线程想做某件事,另一个线程已经做了;那么该线程就不再重复,直接结束返回

    static void balking(){
        for(int i = 0; i < 10; i++){
            new Thread(() -> {
                start();
            }, "t" + i).start();
        }
    }

    static void start(){
        log.debug("尝试启动...");
        synchronized (VolatileTest.class){
            if(starting){
                log.debug("已经启动...不可重复启动");
                return;
            }
        }
        // 只有一个线程,会执行到这里
        log.debug("启动成功!");
        starting = true;
    }

① 其实这里不需要用到 volatile,主要保证的是一个可见性

② 在这里,starting 是会被所有线程读到的;所以这里必须保证它的可见性 

starting synchronized 内部被读取,保证了可见,从主存读取

④ 线程 t8 首先执行了 start,启动了;后面的线程发现已经启动,则直接结束返回

⑤ 对 starting 的修改操作,放在锁外面;因为只有线程 t8到这一步后面线程过不了 if

DCL 优化

线程不安全 —— 懒汉式单例

@Slf4j(topic = "c.Singleton")
public final class Singleton {
    private static Singleton INSTANCE = null;
    private Singleton(){log.debug("创建......");} // 创建一个对象,就打印一次

    public static Singleton instance(){
        if(INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

@Slf4j(topic = "c.SingletonTest")
class SingletonTest{
    public static void main(String[] args) {
        for(int i = 0; i < 5; i++){
            new Thread(() -> {
                Singleton.instance();
            }, "t" + i).start();
        }
    }
}

并发情况下,多个线程同时判断 INSTANCE 是否为 null  

② 都发现不为 null,于是都进入 if创建出一个新对象

线程安全 —— 懒汉式单例

public static synchronized Singleton instance()
//---------------------------------------------
synchronized(Singleton.class){
    if(INSTANCE == null){
        INSTANCE = new Singleton();
    }
}

 

优点:实现简单

缺点:所有线程都需要获取锁执行同步代码块,即使对象已经创建;降低并发度 

DCL —— 懒汉式单例

DCL 简介

Double - Checked Locking 双重检查锁:保证不出现,所有线程都来获取锁;假如对象已经创建,则不再获取锁提高了并发度

DCL 实现单例(不安全) 

@Slf4j(topic = "c.Singleton")
public final class Singleton {
    private static Singleton INSTANCE = null;
    private static int count = 0;
    private Singleton(){}

    public static Singleton instance(){
        if(INSTANCE == null){ // 第一次检查,最多前面的线程会获取锁
            synchronized(Singleton.class){
                log.debug("进入...... {}", ++count); // 进入了同步块,就记一次数
                if(INSTANCE == null){ // 第二次检查
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

@Slf4j(topic = "c.SingletonTest")
class SingletonTest{
    public static void main(String[] args) {
        for(int i = 0; i < 200; i++){
            new Thread(() -> {
                Singleton.instance();
            }, "t" + i).start();
        }
    }
}

① 200 个线程,并没有都进入同步代码块;提高并发度

②  但是这种写法,线程不安全 

DCL 实现单例(安全) 

如上产生线程不安全原因如下: 

① 对象创建有四步(并且我们都知道,INSTANCE 只是一个引用地址,找到对象)

1. new
2. dup
3. invokespecial
4. putstatic

    1. 产生对象引用地址,此时对象还没真正创建

    2. 复制一份引用地址

    3. 通过复制的引用地址调用构造方法,此时才创建对象

    4. 将引用地址符值给 INSTANCE

② 此时可能发生指令重排执行 4执行 3

③ 线程 t1 中,引用地址符值给了 INSTANCE,然后再调用构造方法;正好线程 t2 进入外层 if(没有在锁内部,不受到保护),此时 INSTANCE 已经不为空了,于是直接 return;对象还没有创建完,就已经被用了,肯定会出现问题

解决:使用 volatile 保证有序性;引用地址符值给 INSTANCE 属于写操作,则 4 后面会加入写屏障,保证前面的指令不会重排到后面

private static volatile Singleton INSTANCE = null;

总结 

上面发生的指令重排情况,在单线程下,是没有影响

② ⭐ synchronized 不能阻止指令重排发生; 它之所以能保证内部的有序性,是因为同一时刻,只有一个线程获得锁,就有点类似单线程那种效果

③ 此处,由于 synchronized{...} 外层还有一个 if 判断,这是不受它保护的;同时可以有多个线程可以执行的;那么此时,假如创建对象发生指令重排,就会出现问题

④ 当共享资源不是完全受 synchronized 保护时,就需要考虑到会不会受到指令重排影响;需要时,使用 volatile 

happens - before

定义

规定了对共享变量的写操作对其他线程可见,抛开这些规则,JMM 不能保证一个线程对共享变量写操作对其他线程可见 

规则总结 

① synchronized

② volatile

③ 线程 start 前对变量进行写操作,线程开始后对该变量读可见

static int x;

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

④ 线程 t1 等待 线程 t2 结束t2 结束前对变量进行写操作, t1 对该变量读可见

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            x = 200;
        }, "t2");

        Thread t1 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //while(t2.isAlive()){}
            log.debug("x : {}", x);
        }, "t1");
        t2.start();
        t1.start();

⑤ 线程 t2 t1 打断,并且 t1 修改了变量;主线程对变量读可见

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1500);
                x = 300; // 写
                t2.interrupt(); // 打断 t2
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t2.start();
        t1.start();
        while(!t2.isInterrupted()){}
        log.debug("x : {}", x);

⑥ 对变量默认值(0,false,null)的写,其他线程对这些变量读可见

⑦ 传递性(volatile 读写屏障)

volatile 场景使用 

多个线程进行读取单个线程进行修改(如:标记的可见性)

锁保护不到的范围,防止指令重排使线程不安全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值