美文网首页
深入并发原理和大厂面试(三):volatile和synchron

深入并发原理和大厂面试(三):volatile和synchron

作者: bug音音 | 来源:发表于2020-12-11 10:36 被阅读0次

    1. 基本定义

    1.1 synchronized

    synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。
    可见性:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁,执行完同步代码后,在释放锁之前会将对变量的修改刷新到主存中。

    原子性:一个操作一旦开始,就不会被其它线程干扰。简单粗暴点理解,要么不执行,要么执行到底。

    1.2 volatile

    volatile是变量修饰符,具有可见性。
    可见性的基本定义:假设某个线程修改了被volatile修饰的变量,修改后的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。

    本质:Java为了加快运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。

    追根溯源:volatile 的执行涉及到cpu的一个重要概念:指令重排

    指令重排:指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。即使指令的执行顺序未必与物理顺序一致 ,但它会保证程序最终执行结果和代码顺序执行的结果是一致的。
    Happen-Before先行发生规则
    如果光靠sychronized和volatile来保证程序执行过程中的原子性, 有序性, 可见性, 那么代码将会变得异常繁琐.
    JMM提供了Happen-Before规则来约束数据之间是否存在竞争, 线程环境是否安全,基本是基于语义(例如变量名定义顺序,写语句先于读语句执行等等)

    程序执行到volatile修饰变量的读操作或者写操作时,保证在其前面的操作已经完成,且结果已经对后面的操作可见,此时在其后面的操作还没有进行。

    1.3 总结

    (1)一次写入,到处读写。某一线程负责更新变量,其他线程只读取变量(不更新变量),并根据变量的新值执行相应逻辑
    (2)volatile具有可见性但并不保证原子性。
    (3)性能方面,synchronized执行效率低,而volatile通常性能优于synchronized。

    某些地方volatile关键字是无法替代synchronized,因为volatile关键字无法保证操作的原子性。

    2 基本用法

    只能使用synchronized,不能使用volatile的地方

    public class TestSync {
         public volatile int inc = 0;
            public synchronized void increase() {
                inc++;
            }
    
            public static void main(String[] args) {
                final TestSync test = new TestSync();
                for(int i=0;i<10;i++){
                    new Thread(){
                        public void run() {
                            for(int j=0;j<1000;j++)
                                test.increase();
                        };
                    }.start();
                }
    
                while(Thread.activeCount()>1)  //保证前面的线程都执行完
                    Thread.yield();
                System.out.println(test.inc);
            }
    }
    
    

    自增操作不是原子操作,同时volatile 也是不能保证原子性的。
    假设i= 0, A,B两个线程同时执行i++ 指令。 A 先读取i = 0; 切换到B 也开始读取i = 0; 再切回 A ,执行 i++ ,此时i = 1; 切到B,因为B已经读取过i = 0了,再执行i++, 这时i = 1。 同样的道理,inc 也是这样,经常发生冲突,导致最终的值< 10000。 对increase 加上synchronized,就可以解决这个问题。

    //线程1:
    context = initContext();   //语句1  context初始化操作
    inited = true;             //语句2
    
    //线程2:
    while(!inited ){
      context.dosomethinh()
      sleep()
    }
    
    

    因为指令重排序,有可能语句2会在语句1之前执行,可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

    这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了。

    3 synchronized 高级用法

    使用synchronized修饰静态方法和非静态方法有什么区别?
    Synchronized修饰非静态方法:对调用该方法的对象加锁,俗称“对象锁”。

    A:同一个对象在两个线程中分别访问该对象的两个同步方法
    结果:会产生互斥。
    通俗讲,类似一个房子,有很多房间,但只有一把钥匙。

    B:不同对象在两个线程中调用同一个同步方法
    结果:不会产生互斥。
    通俗讲,相当于两个房子,两把钥匙

    Synchronized修饰静态方法:对类对象加锁(.class),俗称“类锁”。

    A:用class对象直接在两个线程中调用两个不同的同步方法
    结果:会产生互斥

    B:用一个类的静态对象在两个线程中调用静态方法或非静态方法
    结果:会产生互斥。

    C:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法
    结果:不会产生互斥。 (类所 与 对象所)

    相关文章

      网友评论

          本文标题:深入并发原理和大厂面试(三):volatile和synchron

          本文链接:https://www.haomeiwen.com/subject/evyegktx.html