Java并发编程——volatile和synchronize详解

volatile

volatile是轻量级的synchronized,它在多线程的开发中保证了共享变量的"可见性",可见性的意思就是当一个线程修改一个共享变量的时候,另一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文切换和调度

一.什么是可见性

可能很多人搞不清楚什么是可见性,只能单纯的从字面意思去理解,下面通过一个例子带大家深刻理解一下

例子很简单,两个线程一个线程等待变量改变才结束否则就死循环,另一个线程去改变变量

private static boolean flag=true;

    private static void setFlag(){
        flag=false;
    }
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("还没变");
            while (flag){
                // flag不改变 就一直死循环直至发现值改变
            }
            System.out.println("我感知到变化了");
        }).start();
        try {
            //等待1s才执行下一个线程
            Thread.sleep(1000); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            setFlag();
            System.out.println("我修改了");
        }).start();
    }

变量flag没加volatile的结果:很明显线程1 进入了死循环一直退不出来,说明并没有发现变量值变了

在这里插入图片描述

让我们给flag变量加上volatile :从结果上看足以知道这个可见性是什么意思了吧

在这里插入图片描述

二.实现原理

我们都知道计算器最底层的语言就是汇编指令,java也不例外,在对声明了volatile的变量进行写操作的时候,JVM就会向处理器发送一条Lock前缀的指令,这个指令会做两件事:

  • 将当前处理器缓存行的数据写回系统内存中
  • 这个写会内存的操作会使其他CPU里缓存了该内存地址的数据无效

我们都知道变量都是放在系统内存中的,但每次取的时候都不会去系统内存中取,会去缓存中取,每个处理器都有自己的缓存。所以为了保证每个处理器的缓存是一致的,处理器会通过嗅探在总线上传播的数据来检查自己缓存的数据是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行操作的时候会重新从系统内存中读取到处理器缓存里

三.是否具有原子性

这是一个有争议的问题,因为很多人把变量的操作和变量的读取混为一谈了

对变量的读取具有原子性:因为保证了可见性,每次读取一定是内存里面最新值

对变量的操作不具备原子性:这里的操作指的是复合操作,例如(加、减等),因为复合操作代表有着三个过程读取变量的值、对变量进行修改操作、写入内存,整个过程不具备原子性

见下面例子:

private volatile int aa=0;

for (int i = 0; i < 10; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 1000; j++) {
                aa++;
            }
        }
    }).start();
}
Thread.sleep(3000);
System.out.println(aa);
// 正确结果应该是10000  
// 但结果是<=10000 而且每次运行结果还不同 

为什么呢?

操作分有三个过程,第一步肯定读取的是最新值毫无疑问,但假设同时有两个线程执行完第一步,此时两个线程获取到的值是一样的,再同时执行+1修改操作,此时的值还是一样的,再写入内存,是不是等于把两个相同的值写入了两次内存?这时问题就产生了

synchronized

这个关键字大家应该都非常的熟悉吧,简直是多线程并发编程里面的元老角色,很多人看到这个第一反应就是重量级锁,性能低,但是现在还可以这么说吗?使用方法实现原理、锁升级到底是什么鬼?

一.使用方法

1.修饰一个代码块

一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞

public void test() {
      synchronized(this) {
         // do something
      }
}

2.修饰一个方法

锁的是这个对象被修饰的方法

public synchronized void test() {

     // do something
 
}

3.修饰一个静态方法

这时候锁的是这个类的所有的对象,因为静态方法是属于类的而不属于对象

public synchronized static void test() {

     // do something
 
}

4.修饰一个类

与上面一样作用域依旧是类,但是上面只是针对类的方法,而这个是整个类

public void test() {

    synchronized(ClassName.class) {
         // do something
    }
}

二、实现原理

volatile类似,Synchronized的底层依赖于JVM的内置锁,JVM基于进入和退出Monitor对象来实现方法的同步和代码块的同步。加上锁的代码在执行时前后会增加两条指令 monitorenter monitorexit,代表着上锁和退出,其他线程执行到monitorenter时会尝试获取对象对应的monitor的所有权,即尝试获取锁

                                  在这里插入图片描述
Synchronized所用的锁是存在Java的对象头里。HotSpot虚拟机的对象头分为两部分信息,第一部分用于存储对象自身运行时数据,如哈希码、GC分代年龄等,这部分数据的长度在32位和64位的虚拟机中分别为32位和64位。官方称为Mark Word。另一部分用于存储指向对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分存储数组长度。

运行期间Mark Word里存储的数据会随着锁的标志位变化而变化,在32位虚拟机中,变化如下:

三、锁升级以及对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以Synchronized一共有着4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态这几个状态会随着竞争情况而升级,但为了提高获得锁和释放锁的效率,锁只能升级并不能降级,比如从偏向锁升级成轻量级锁后不能降级成偏向锁

1.偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且还总是由同一线程多次获得,所以为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁获取:当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步快的时候就不需要进行CAS操作来加锁和解锁,只需要简单的测试一下对象头里是否存储着指向当前线程的偏向锁。如果成功了就代表已经获得锁了,如果失败了,则需要再看看对象头里面偏向锁的标记是否是1:如果不是,则用CAS竞争锁,如果是则尝试使用CAS将对象头的偏向锁指向当前线程

偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还存活,如果不处于活动状态则将对象头设置成无锁状态;如果还活着则会遍历偏向对象的锁记录,要么对象头的MarkWord重新偏向其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程

在这里插入图片描述

2.轻量级锁

轻量级加锁:线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word 复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功则当前线程获取锁,如果失败表示其他线程竞争锁,当前线程则尝试自旋来获取锁,自旋次数上限还失败则锁升级为重量级锁

轻量级解锁:使用CAS将锁记录中的Mark Word替换回对象头,如果成功则表示竞争没有发生。如果失败则表示当前锁存在竞争,锁就会膨胀成为重量级锁

在这里插入图片描述

3.重量级锁

这个就不用说啦,就像开头那样直接交给了JVM用指令处理了

4.优缺点对比

优点缺点
偏向锁加锁和解锁不需要额外的消耗如果存在锁竞争就会带来额外的锁撤销的消耗
轻量级锁竞争的线程不会阻塞得不到锁会自旋消耗CPU
重量级锁线程竞争不用自旋,不会消耗CPU线程阻塞,响应慢
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值