多线程学习二十八:原子性,可见性,有序性

本文详细阐述了Java中可见性、有序性和原子性的概念,如何通过volatile、synchronized和final实现这些特性,以及它们在并发编程中的应用,包括两阶段终止模式、同步模式和线程安全单例。

可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的结果(值)。比如用volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存,所以对其他线程是可见的。
在Java中volatile、synchronized和final实现可见性。因为我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
有序性
即程序执行的顺序按照代码的先后顺序执行。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile是因为其本身包含“禁止指令重排序”的语义,synchronized是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则来保证的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。什么是指令重排序?是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。
总结:要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要前往1有一个没有被保证,就有可能会导致程序运行不正确。
原子性
原子是世界上的最小单位,具有不可分割性。即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。例如i=0;(i非long和double类型)这个操作是不可分割的,那么我们说这个操作是原子操作。但是例如i++;这个操作实际是i=i+1是可分割的,所以它不是一个原子操作。
非原子操作都会存在线程安全问题,所以需要我们使用同步技术(synchronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法,例如AtomicInteger、AtomicLong、AtomicReference等。在Java中synchronized和在lock、unlock中操作保证原子性。

下面详细介绍这三个特性

可见性

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

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不会如预想的停下来。因为初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。.因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
使用volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,将成员变量放入了主存中,volatile可以避免线程从自己的工作缓存中读取变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
volatile 原理
volatile 的底层实现原理是内存屏障,

  • 对 volatile 变量的写指令后会加入写屏障(写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中)
public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}
  • 对 volatile 变量的读指令前会加入读屏障(读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据)
public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

注意

  • 保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
  • 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0,(%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。
  • 不能保证其原子性。

volatile和synchronized的区别

  • volatile是用来修饰变量的,而synchronized是用来修饰方法和类,不能修饰变量!在它修饰的方法或者代码块中的变量只有当前线程可以访问,其他线程都会被阻塞。
  • volatile可以保证变量修改的可见性,但是无法保证线程的原子性。synchronized既可以保证变量修改的可见性,也可以保证线程的原子性。但缺点是synchronized 是属于重量级操作,性能相对更低
  • volatile不会造成线程阻塞,但synchronized却会。
  • volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化。

可见性应用
两阶段终止模式

/**
 *使用volatile优化两阶段打断
 */
@Slf4j(topic = "c.Test1:")
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
        twoPhaseTermination.start();
        TimeUnit.SECONDS.sleep(3);
        log.debug("主线程执行了打断");
        twoPhaseTermination.stop();
    }
}
@Slf4j(topic = "c.TwoPhaseTermination:")
class TwoPhaseTermination {
    Thread monitor; //监控线程
    private volatile static boolean falg= false;
    //启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                if (falg) {
                    log.debug("被打断了料理后事");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    log.debug("执行监控记录,线程此时正在正常运行中");
                } catch (InterruptedException e) {}
            }
        });
        monitor.setName("monitor");
        monitor.start();
    }

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

同步模式之犹豫模式
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

public class MonitorService {
 // 用来表示是否已经有线程已经在执行启动了
 private volatile boolean starting;
 public void start() {
 log.info("尝试启动监控线程...");
 synchronized (this) {
 if (starting) {
 return;
 }
 starting = true;
 } 
 // 真正启动监控线程...
 }
}

它还经常用来实现线程安全的单例,后面的5个例子中有对线程单例的更详细介绍

public final class Singleton {
 private Singleton() {
 }
 private static Singleton INSTANCE = null;
 public static synchronized Singleton getInstance() {
 if (INSTANCE != null) {
 return INSTANCE;
 }
 
 INSTANCE = new Singleton();
 return INSTANCE;
 }
}

对比一下保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待。

有序性
通过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;
 }
}

但是volatile仍然不能解决不同线程之间的指令交错:写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去。而有序性的保证也只是保证了本线程内相关代码不被重排序

线程安全单例习题
例一:

// Q1: 为什么加 final
// A: 防止子类继承他破坏他的单例属性
// Q2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例 
// A: 通过readResolve方法来防止反序列化破坏单例
public final class Singleton1 implements Serializable {
    //Q3:为什么设置为私有? 是否能防止反射创建新的实例?
    //A:防止无限创建它的实例,所以设置成私有,不能防止反射来创建实例
    private Singleton1() {
    }
    //Q4:这样初始化是否能保证单例对象创建时的线程安全?
    //A: 是线程安全的
    private static final Singleton1 INSTANCE = new Singleton1();
    //Q5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public,说出你知道的理由
    //A: 使用方法可以提供更好的封装,而且可以支持泛型,而使用属性不行
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
    public Object readResolve() {
        return INSTANCE;
    }
}

例二:

public enum Singleton2 {
// Q1:枚举单例是如何限制实例个数的
//A:反编译之后INSTANCE就是Singleton2枚举类的静态成员变量
//Q2:枚举单例在创建时是否有并发问题
//A:没有,他是静态成员变量,他的线程安全性是在内加载阶段完成的
// Q3:枚举单例能否被反射破坏单例
//A:不能
// Q4:枚举单例能否被反序列化破坏单例
//A:不能,虽然枚举类默认实现了序列化接口,但考虑到反序列化的影响他已经处理过来防止反序列化破坏单列
// Q5:枚举单例属于懒汉式还是饿汉式
//A:饿汉式
// Q6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
//A:加个构造方法
    INSTANCE;
}

例三:

public class Singleton3 {
        private Singleton3() { }
        private static Singleton3 INSTANCE = null;
        //Q:分析这里的线程安全, 并说明有什么缺点
        //A:每次进来都要加锁判断,所有性能较低
        public static synchronized Singleton3 getInstance() {
            if( INSTANCE != null ){
                return INSTANCE;
            }
            INSTANCE = new Singleton3();
            return INSTANCE;
        }
}

例四:

public final class Singleton4 {
    private Singleton4() { }
    // Q1:解释为什么要加 volatile ?
    // A: 因为构造方法的指令有可能跟赋值语句指令重排序
    private static volatile Singleton4 INSTANCE = null;

    //Q:对比实现3, 说出这样做的意义
    //A:只需第一次加锁,后续进来都不需要加锁并发的效率更高
    public static Singleton4 getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (Singleton4.class) {//t2
            //Q:为什么还要在这里加为空判断, 之前不是判断过了吗
            //A: 为了解决首次创建对象时多个线程并发访问的问题
            if (INSTANCE != null) { //如果没有这个当t1执行完成之后也会创建对象
                return INSTANCE;
            }
            INSTANCE = new Singleton4();//t1
            return INSTANCE;
        }
    }
}

例五:

public class Singleton5 {
    private Singleton5() { }
    //Q1:属于懒汉式还是饿汉式
    //A:懒汉式 
    private static class LazyHolder {
        static final Singleton5 INSTANCE = new Singleton5();
    }
    //Q2:在创建时是否有并发问题
    //A:没有并发问题是有JVM来保证它的线程安全性的
    public static Singleton5 getInstance() {
        return LazyHolder.INSTANCE;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值