浅谈volatile ,synchronized,CAS,锁升级

1.volatile

俩个功能:1.保证线程可见性 2.禁止指令重排序

前言

并发编程的3个条件

1.原子性:要实现原子性方式较多,可用synchronized、lock加锁,AtomicInteger等,但volatile关键字是无法保证原子性的;

2.可见性:要实现可见性,也可用synchronized、lock,volatile关键字可用来保证可见性;

3.有序性:要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性;

1.1保证线程可见性

假设A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道,使用volatile关键字,会让所有线程都会读到变量的修改值

在下面的代码中,running是存在于堆内存的t对象中,当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样当主线程修改running的值之后,t1线程感知不到,所以start会先运行一段时间才end,而在running前面加上volatile后,将会强制所有线程都去堆内存中读取running的值,所以很快会到end。但同时,volatile并不能保证多个线程共同修改running变量时所带来的不一致问题

复制代码

//没有volatile,start会过一段时间才停下来,因为线程t1不会每次都去读取堆内存
  private static volatile boolean running = true;
    private static void m() {
        System.out.println("m start");
        while (running) {
            System.out.println("hello");
        }
        System.out.println("m end!");
    }
    public static void main(String[] args) throws IOException {
        new Thread(T01_HelloVolatile::m, "t1").start();
        SleepHelper.sleepSeconds(1);
        running = false;
        System.in.read();
    }

复制代码

1.2.禁止指令重排序

对于非常经典的单例模式中双重检查,要不要加volatile呢?答案是肯定的,要加。

在说明为什么要加之前我们先来看下new一个对象的时候到底发生了什么。

从字节码可以看到创建一个对象实例我们需要三个步骤,例如Object a = new Object(3)

正常来说的话我们不会发生问题,但在超高并发的情况下,

假如线程1执行到了第二步,线程2此时执行到了第一步,这时候就会出错。

所以在单例模式中,我们在方法上使用 了synchronized,但是这么做就会导致每次方法

调用都需要获取与释放锁,开销很大。所以正确的双重检查锁定模式我们需要使用volatile。

这里主要由于volatile的第二个特性:禁止指令重排序

由于volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的

对象,从而保证安全性。

复制代码

//volatile虽然达到了按需初始化的目的,但也带来了线性不安全问题
//通过synchronized解决,但也带来了效率低下
public static M getInstance(){
    if(INSTANCE == null){
       //双重检查
       synchronized(M.class){
       try(){
       }catch{}
       //如果只有volatile,别的线程也会new,加上synchronized只有等当前线程已经初始化成功别的线程才会访问,此时已经new过了就不会在new了
       INSTANCE = new M();
       }
    }
}

复制代码

2.synchronized

简介:用于给某个对象上锁,保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,

是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保线程互斥的访问同步代码

2.1synchronized的目的

举栗子:

复制代码

public class Test {
        volatile int count = 0;//解决方法:synchronized void m()。保证每个线程结束后另一个线程再拿到count的值,此时可以加到10w
        void m() {
            for (int i = 0; i < 10000; i++) count++;
        }
        public static void main(String[] args) {
            Test t = new Test();
            List<Thread> threads = new ArrayList<Thread>();
            //10个线程,每个线程调用m方法
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(t::m, "thread-" + i));
            }
            threads.forEach((o) -> o.start());
            threads.forEach((o) -> {
                try {
                    o.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println(t.count);
        }
}
==========
结果:45854

复制代码

在上面的例子中,m方法将count加到10000,我们创建了十个线程,每个线程调用一次m方法,正常来说我们的结果应该是10w,

那为什么结果会是45854呢?我们需要先了解count++发生了什么

可以看到count++需要三个步骤,如果线程1的count加到1,线程2拿到的是1,线程3拿到的也是1,线程2返回2,线程3返回2,

实际结果应该为3,所以丢失了1次更新。

导致最后的结果少加了很多次。解决办法就是在m方法前面加锁,保证一个线程结束后第二个线程才可以执行。

也可以采用AtomicInteger 类,因为AtomicInteger类是原子性操作的

复制代码

//结果是10w
AtomicInteger count = new AtomicInteger(0);
       //不需要在加synchronized
        /* synchronized */void m() {
                for (int i = 0; i < 10000; i++)
                        //if count1.get() < 1000
                        count.incrementAndGet(); //count1++
        }

复制代码

注意点:

1.程序当中如果出现异常,默认情况锁被释放,需要catch,不然容易被别的程序乱入,导致数据不一致

2.锁定对象的时候不能用String常量,Integer,Long,

3.锁的是对象,不是代码

4.无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

5.每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码

6.同步代码块中的语句越少越好,因为上锁后性能会变低

7.锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变应该避免将锁定对象的引用变成另外的对象

2.2synchronized的优化

我们已经知道使用synchronized虽然可以保证线程的安全行,但同时也变得效率低下,每次只能通过一个线程,既然每次只能通过一个,这种形式不能改变的话,那么我们能不能让每次通过的速度变快一点了。打个比方,只有一个厕所,我们一群人在排队等着,由于使用了synchronized,导致我们每次只能一个人去上厕所,但如果想我们尽快都上完厕所,就只能增加上厕所的速度了。笑😀。这种优化方式同样可以引申到锁优化上,缩短获取锁的时间。

synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS

3.CAS简介

3.1CAS的机制

CAS 操作包含三个操作数 —— 老值(V)、预期原值(A)和新值(B)。

简而言之就是V=A的情况,B给A。V!=A的情况,返回V。

V=A的情况:表明该值没有被其他线程更改过

V!=A的情况:表明该值已经被其他线程改过了则老值A不是最新版本的值了,所以不能将新值B赋给V,

当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。

3.2 CAS的问题

最典型的是ABA问题。那什么是ABA问题呢?

因为CAS会检查老值有没有变化,这里存在这样一个有意思的问题,比如一个int类型的老值A变为了成B,然后再变成A,刚好在做CAS时检查发现老值并没有变化依然为A,但是实际上的确发生了变化。打个比方,你的女朋友跟别人睡了一夜,第二天回来还是你原来的女朋友吗?人还是那个人,但可能心已经变了。同样的道理,这里就是A变为了成B,然后再变成A,懂了吧。笑😀。

解决方案如果是int类型当然无所谓,但如果是引用类型需要加版本号version,修改完后版本号+1即可,版本号在哪里加呢,在对象头里面。

3.3 对象头

对象头里包括(64位机器占96位):运行时元数据(Mark Word)(占64位)、类型指针(Klass Point)(占32位)

运行时元数据里又包括:哈希值(HashCode)、GC分代年龄、锁状态标志

① 哈希值:它是一个地址,用于栈对堆空间中对象的引用指向,不然栈是无法找到堆中对象的;

②GC分代年龄(占4位):记录幸存者区对象被GC之后的年龄age,一般age为15之后下一次GC就会直接进入老年代,要是还没有等到年龄为15,幸存者区就满了怎么办,

那就下一次GC就将大对象或者年龄大者直接进入老年代。

③ 锁状态标志:记录一些加锁的信息,在底层是锁的对象,而不是锁的代码,锁对象的话,那会改变什么信息来表示这个对象被改变了呢?

也就是怎么才算加锁了呢?答案就是改变这个对象的对象头的锁信息来标识已经加锁,下一个线程来获取是获取不到的,底层采用的就是CAS机制

4.锁的升级

我们已经知道采用synchronized的话虽然会保证线程的安全性,但同样的带来了极大的性能低下,其实在JDK1.6之前,synchronized就是一种重量级锁

4.1无锁

无锁是指没有对资源进行锁定,所有线程都可以访问并修改同一资源,但同时只有一个线程可以修改成功。如果有多个线程同时修改同一值,必定会有一个线程修改成功,其他失败的线程会不断尝试直到修改成功

4.2偏向锁

锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

简言之就是一个线程获取锁后,会在对象头记录这个线程的ID,下次来不需要再进行CAS操作就可以直接获得锁。

4.3轻量级锁(自旋锁)

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

简言之就是锁是偏向锁的时候,被别的线程所访问,锁会升级成轻量级锁,别的线程会通过自旋的形式尝试获取锁

4.4重量级锁

长时间的自旋是非常消耗资源的,一个线程持有锁,别的线程就只能忙等,但忙等是由限度的(循环里面进行的,默认进行10次),某个达到最大自旋的线程,轻量级 java教程锁就会升级为重量级锁,别的线程一旦遇到重量级锁,就不会忙等,直接会将自己挂起,等待被唤醒。

话不多说,直接上图:

总结:

volatile:1.保证线程可见性 2.禁止指令重排序

synchronized:给对象上锁,保证线程同步

CAS:三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。

锁的四种状态:无锁,偏向锁,轻量级锁,重量级锁

以上就是关于volatile ,synchronized,CAS,锁升级的介绍,这篇文章估计还有很多需要修改的地方,我也还有很多不清楚的地方,欢迎大家指出,我们一起进步。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值