详解volatile和Synchronized

普通Demo

public class Test {
    public static void main(String[] args) {
        Autumn a=new Autumn();
        a.start();
        for(;;){
            if (a.isFlag()){
                System.out.println("我是Autumn");
            }
        }
    }
}

class Autumn extends Thread{
    private boolean flag=false;

    public boolean isFlag(){
        return flag;
    }

    @Override
    public void run(){
        try {
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            flag=true;
            System.out.println("flag:"+flag);
        }
    }
}

上述代码的并不能输出“我是Autumn”,但是为什么呢?我们明明已经在线程里将flag修改成了True,为什么无法打印?这就是涉及到了多线程中很经典的一种情况——可见性

JMM——Java内存模型

JMM规范
所有的共享变量都存储于主内存,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题

每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

线程对变量的所有操作(读/写)都必须在工作内存中进行,不能直接读写主存中的变量

不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

工作内存和主存的关系
在这里插入图片描述
正是因为上述的模型,才会导致可见性问题。

如何解决可见性——synchronized和volatile

1.用synchronized加锁

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Autumn a=new Autumn();
        a.start();
        for(;;){
            synchronized (a){
                if (a.isFlag()){
                    System.out.println("我是Autumn");
                }
            }
        }
    }
}

如上述代码所示,因为某一个线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。

所以使用加锁的方式就相当于每次执行操作前都会先清空自己工作内存中的变量值,然后再去主存中读取最新的变量值

而获取锁失败的线程会被阻塞,这样就保证当前获取的变量值是最新的。

2.volatile修饰变量

class Autumn extends Thread{
    private volatile boolean flag=false;

    public boolean isFlag(){
        return flag;
    }

    @Override
    public void run(){
        try {
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            flag=true;
            System.out.println("flag:"+flag);
        }
    }
}

但是加锁不可避免的会涉及到线程切换,会增加系统开销,那么此时我们可以使用volatile来修饰变量,如上图代码所示,用volatile修饰flag变量。

volatile的作用
每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写会了,他其他已经读取的线程的变量副本就会失效了,需要都数据进行操作又要再次去主内存中读取了。

volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

Tips:一句话来说,volatile修饰的变量是在主存上进行读写操作,所以可以保证可见性

但是这样也会存在一个问题,如果有多个CPU的运算任务都涉及到同一块主内存区域的时候,比如说变量在多个CPU之间的共享,我们该以哪个结果为标准呢?——这就涉及到了缓存一致性的问题。

为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等

Intel的MESI协议

当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

如何发现自己存储的变量是无效的?
1.嗅探
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

存在的缺点(总线风暴):由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS不断循环,无效交互会导致总线带宽达到峰值。所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。

volatile和synchronized的区别

volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。

volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。

volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。

volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

volatile是无锁操作,所以性能比synchronized更好

volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。

synchronized和Lock

  • synchronized是关键字,属于JVM层面,而Lock是一个接口,属于JDK层面
  • synchronized会主动释放锁,但是Lock必须手动释放
  • synchronized具有原子性,所以是不可中断的,而Lock可中断也可以不中断
  • synchronized可以锁住代码块和方法,而Lock只能锁住方法
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。

应用场景
比如我现在是滴滴,我早上有打车高峰,我代码使用了大量的synchronized,有什么问题?锁升级过程是不可逆的,过了高峰我们还是重量级的锁,那效率是不是大打折扣了?这个时候你用Lock就更好。

Synchronized升级机制

我们以前经常会听到一个说法——synchronized是重量级锁,但是近来却少有人再提起
具体的原因就是因为Java6引入的锁升级机制和锁优化技术,提高了锁的效率。

升级过程
在这里插入图片描述
Tips:无锁就是CAS,就不再赘述了

偏向锁
之前的文章提到过,对象头是由Mark Word和Klass pointer 组成,锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。

这个过程是采用了CAS乐观锁操作的,每次同一线程进入,虚拟机就不进行任何同步的操作了,对标志位+1就好了,不同线程过来,CAS会失败,也就意味着获取锁失败。
偏向锁在1.6之后是默认开启的,1.5中是关闭的,需要手动开启参数是
xx:-UseBiasedLocking=false。
那么有多个线程竞争锁的时候,我们该怎么解决呢?——轻量级锁应运而生

轻量级锁
还是跟Mark Work 相关,如果这个对象是无锁的,jvm就会在当前线程的栈帧中建立一个叫锁记录(Lock Record)的空间,用来存储锁对象的Mark Word 拷贝,然后把Lock Record中的owner指向当前对象。

JVM接下来会利用CAS尝试把对象原本的Mark Word 更新为Lock Record的指针,成功就说明加锁成功,改变锁标志位,执行相关同步操作。

如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,继续锁升级,修改锁的状态,之后等待的线程也阻塞。
在这里插入图片描述

重量级锁

那么升级为轻量级锁之后,允许多个线程竞争一个锁了,此时线程使用忙等待的方式(CPU自旋)来尝试获取锁,虽然忙等待的性能损耗比线程切换小,但是长时间的忙等待也会导致CPU的性能损耗过大,因为一次自旋就相当于一次CPU空转,那么默认的自旋次数是10次,当一个线程自旋次数超过10次,轻量级锁升级为重量级锁,也就是我们说的synchronized了。
在这里插入图片描述

References

敖丙——JavaFamily

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
通过以上引用的内容,我们可以得出以下结论: volatile关键字和synchronized关键字都是用来保证线程之间操作的有序性和可见性。 然而,volatile关键字不能保证操作的原子性,只能保证被修饰变量的可见性。它的主要作用是禁止指令重排序,即保证变量的写操作对其他线程的读操作可见。 而synchronized关键字可以保证被修饰的变量在解锁之前会被同步回主存,从而保证了变量的可见性。 此外,synchronized关键字还可以保证同一时刻只有一个线程能够对持有同一个对象锁的同步块进行操作,从而实现了线程的串行执行。 综上所述,volatilesynchronized关键字在保证线程之间操作的有序性和可见性方面具有不同的作用。使用时需要根据具体的需求选择合适的关键字。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [volatilesynchronized 详解](https://blog.csdn.net/ywlmsm1224811/article/details/103166419)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [volatilesynchronized详解](https://blog.csdn.net/LYQ20010417/article/details/124138846)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值