Loading

java 并发相关(2) - synchronized和volatile关键字

多线程间的同步与锁

1、线程问题

多个线程并发执行可以提高我们程序的执行速度和效率,但也会带来缓存不一致、执行顺序无序等问题,java提供了一些锁和同步的机制,一些原子类,线程安全集合等手段来保证线程之间的安全执行。

2、保证线程安全的三个方面:原子性、可见性、有序性

  • 原子性:原子性一般指一组操作是一个整体,要么全部成功,要么全部失败。

多线程下的原子性表现为线程对共享变量的操作是不可分割的,即操作共享变量的其他线程,只能看到执行前或执行后的结果,不能看到操作的中间状态。

  • 可见性:一个线程修改了共享变量的值,其他线程能立即得知这个修改。

  • 有序性:指的是程序执行的指令按代码顺序执行,因为在java中编译器和处理器会对指令进行重排序,可能会对多线程任务结果造成影响。

3、synchronized

  • synchronized定义

java提供同步机制的关键字,当它用来修饰一个方法或代码块的时候,能保证该代码块的同步执行,及多个线程同一时间只能有一个执行同步代码块。

synchronized锁有两类范围,一个是实例锁,一个是类锁,也就是synchronized(this or Object.class)。

  • synchronized的锁的简单表现

1、当一个线程持有synchronized锁后,其他线程不能访问该对象的synchronized方法,但可以访问非synchronized方法,体现锁的互斥性。

2、当一个线程持有synchronized锁之后,可以任意调用该对象的其他synchronized方法,体现锁的可重入性。

3、类锁和实例锁,synchronized的两个作用域,两者之间是不会相互影响的。

4、使用synchronized锁时,要注意锁的粒度,即要注意不要用synchronized同步没必要保证同步的代码块。

  • synchronized的原理

synchronized的语义底层是通过一个Object的monitor的监视器对象来完成对代码块的锁定,每个对象都有自己的一个monitor监视器对象,如果线程没有获得对象监视器,就会处于阻塞状态(Blocking)。

示例方法:

    public synchronized String getName(){
        return name;
    }

    public void setName(String name) throws InterruptedException {
        synchronized (this){

            this.name = name;

            this.wait();
        }
    }

注:javac Some.java -> javap -verbose Some

反编译后:

经过反编译后可以发现synchronized修饰后的代码块前后加上了monitorenter和monitorexit这两条指令。

线程执行synchronized方法块的流程:对应monitorenter和monitorexit指令

monitorenter:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

monitorexit:

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

从这里我们可以看到synchronized锁是排他的,是可重入的。

4、volatile

  • JMM模型 线程和主内存之间的抽象关系,实际上跟CPU的高速缓存有关。

共享变量存储在主内存中,每个线程有一个私有的内存空间(CPU内的高速缓存)保存着共享变量的副本,线程对共享变量操作是直接对本地内存进行操作,这会造成共享变量的不一致性。

  • volatile 保证共享变量的可见性

volatile修饰的共享变量,当一个线程对这个变量进行修改操作后,jvm会把线程本地内存中变量的新值刷新到主内存中,持有这个变量的其他线程,在私有内存中变量会失效,从主内存重新获取。

基础例子:通过volatile修饰的变量isStop变量才能保证两个线程对isStop的可见性

        static boolean isStop = false;//加不加volatile

        public static void main(String[] args) throws InterruptedException {

            Thread thread1 = new Thread(() -> {
                isStop = true;
                System.out.println("thread1 is end");
            });

            Thread thread2 = new Thread(() -> {
                while(!isStop){
                }

                System.out.println("thread2 is end");
            });

            thread2.start();
            Thread.sleep(1000);
            thread1.start();

        }
  • volatile 保证操作的有序性

重排序:

编译器和处理器对于操作指令没有数据依赖的情况下,可能会发生指令序列的重排序。

volatile保证有序性:禁止编译器的优化和重排、通过内存屏障限制处理器重排。

内存屏障的作用:

1、确保指令重排序时不会把屏障后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面。
2、强制把写缓冲区/高速缓存中的数据等写回主内存,让缓存中相应的数据失效;

  • volatile 内存屏障

Load Barrier 读屏障
在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据;

Store Barrier 写屏障
利用缓存一致性机制强制将对缓存的修改操作立即写入主存,让其他线程可见,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据。

5、synchronized+volatile实现懒汉式单例的双重检查

加synchronized的作用:保证多线程状态下,只有一个线程执行new SingletonThread2 操作

第二个if(instance == null)作用: 如果A、B两个线程都通过第一次(instance == null)检查,进入同步块执行,A执行了同步块,创建了SingletonThread2 对象,B线程就不能在执行new操作了。

加volatile作用:禁止重排序,instance = new SingletonThread2()操作并不是原子的,可以拆分为3步,java编译器和处理器可能会进行指令重排序,会造成instance已经有内存地址,却没有初始化,这种情况下会获取一个空的instance对象。

public class SingletonThread2 {

    // 禁止指令重排序
    private volatile static SingletonThread2 instance;

    public static SingletonThread2 getInstance() {
        if (instance == null) {

            // 加同步锁
            synchronized (SingletonThread2.class){
                if(instance == null){
                    //防止多个线程在外边等待进入
                    instance = new SingletonThread2();

                    // 指令重排序可能会变成 1 -> 3 ->2
//                    1、memory = allocate(); //分配SingletonThread2对象的内存空间地址
//                    2、ctorInstance(memory); //初始化对象,赋初值
//                    3、instance = memory; //设置instance指向刚分配的内存地址
                }
            }
        }
        return instance;
    }
}

6、补充知识: MESI 缓存一致性协议

MESI是一种比较常用的缓存一致性协议,MESI表示缓存行的四种状态,分别是:

1、M(Modify) 表示共享数据只缓存在当前 CPU 缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
2、E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
3、S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致
4、I(Invalid) 表示缓存已经失效

在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听(snoop)其它CPU的读写操作。
对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则:
CPU读请求:缓存处于 M、E、S 状态都可以被读取,I 状态CPU 只能从主存中读取数据
CPU写请求:缓存处于 M、E 状态才可以被写。对于S状态的写,需要将其他CPU中缓存行置为无效才行。

参考:https://blog.csdn.net/mashaokang1314/article/details/88803900 (从硬件内存架构理解Volatile(内存屏障))

posted @ 2021-05-30 16:09  逝zxq  阅读(67)  评论(0编辑  收藏  举报