zoukankan      html  css  js  c++  java
  • java 并发相关(2)

    多线程间的同步与锁

    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(内存屏障))

    关于学习到的一些记录与知识总结
  • 相关阅读:
    loadNibNamed 的使用
    重写UIPageControl实现自定义按钮(转)
    乔布斯办公室语录
    学ACM算法题有用吗?
    基于文法分析的表达式计算器的实现
    我的程序员之路(5)——工作一年
    XCode实用快捷键,谁用谁知道
    LR(1)语法分析表生成
    九大定律,四大原则
    汉字为何不能用笔画编码信息论系列
  • 原文地址:https://www.cnblogs.com/Zxq-zn/p/14788459.html
Copyright © 2011-2022 走看看