synchronized和CAS算法

让多线程从并行状态变为串行状态 谁获取了锁 谁进来执行

可能很多人觉得synchronized笨重,但是从java1.6后,已经对synchronized进行了优化改良,引入了锁升级和自旋锁 锁消除等内容。在一些情况下它就已经没那么重了

当一个线程访问同步代码块的时候,它首先需要获取到锁才能执行,退出或者抛出异常时必须释放锁

基本使用

对于线程来说,如果多个线程只是相互间单独执行的话,本身是没有太大意义的,一般来说都是需要多个线程相互间协作工作这样才会对系统带来实际的一样

java中支持多个线程同时访问一个对象或者对象中的成员变量,但是如果没有任何保护机制的话,就会出现数据不一致的情况

synchronized可以修饰方法或者以同步代码块的形式进行使用,主要确保多个线程在同一时刻只能有一个线程处于方法或者同步代码块中,保证了线程对变量访问的可见性和排他性,又称为内置锁机制

synchronized可以加在方法上或者类上,或者添加同步代码块

synchronized的对象锁一定包保证多个线程的操作的是同一把锁

synchronized也可以加载类上使用,为类锁 此时加锁的就是一个class对象。只是概念上的东西,并不真实存在,类锁其实锁的是每个类的对应的class对象,类锁和对象锁之间互不干扰

synchronized使用时 建议锁定范围越小越好,否则容易造成大量资源被锁定

synchronized锁升级

前面说了,1.6后 synchronized引入了锁升级,那么什么是锁升级?锁怎样升级的?

synchronized锁升级是不可逆的,只升不降

升级过程:无锁--->偏向锁--->轻量级锁--->重量级锁

从java1.6后为了减少获得锁和释放锁而带来的性能消耗,引入了偏向锁和轻量级锁,此时锁会存在四种状态,级别从低到高:无锁、偏向锁、轻量级锁、重量级锁。状态间的转换会随着竞争情况逐渐升级。升级是不可逆的

锁状态:

没加锁时:无锁

加上synchronized:初始状态偏向锁(用户态进行的CAS的操作)

偏向锁满足一定竞争条件后:轻量锁(用户态进行的CAS的操作)

轻量锁满足一定竞争条件后:重量锁(内核态经过CPU)

偏向锁:

加锁:

1.当线程第一次执行到synchronized代码块时,会通过自旋的方式修改MarkWordk的锁标志,代表锁对象为偏向锁 线程ID通过自旋方式保存到MarkWord中

2.执行完同步代码块后,线程并不会主动释放偏向锁

3.当第二次执行同步代码块时,首先会判断MarkWord中的线程ID是否为当前线程

4.如果是相同,正常执行同步代码块,由于之前没有释放锁,就不需要重新加锁。如果自始至终使用锁的线程只有一个,偏向锁几乎没有额外开心,性能极高

5.如果线程ID并未指向当前线程,则通过CAS操作替换MarkWork中的线程ID,如果替换成功,则执行同步代码块,如果替换失败

6.如果CAS替换失败,则表示有竞争。当到大全局安全点时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码

撤销:

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销需要等待全局安全点(这个时间点上没有字节码正在执行) 它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为01) 或者轻量级锁(标志位为00)的状态

使用场景:

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其他线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作

在有锁竞争时,偏向锁会做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,安全点会到时stw,导致性能下降

轻量级锁:

轻量级锁是偏向锁的升级,偏向锁运行在一个线程进入同步代码块的情况下,当第二个线程加入锁争用时,偏向锁会升级为轻量级锁

加锁:

1.在进入同步代码块前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录中

2.然后线程尝试使用自旋将对象头中的MarkWord替换为指向锁记录的指针

3.如果成功,当前线程获得轻量级锁,执行同步代码块

4.如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁,当自旋次数达到一定次数时,锁就会升级为重量级锁,并阻塞线程

解锁:

解锁时,会使用自旋操作将锁记录替换回到对象头,相当于做一个对比,如果成功,表示没有竞争发送;如果失败表示当前锁存在竞争,锁被升级为重量级锁,释放锁并唤醒等待线程

重量锁:

竞争失败后,线程阻塞,释放锁后,唤醒阻塞线程,不使用自旋锁,不会那么消耗CPU 所以重量级锁适合在同步块执行时间长的情况下

锁升级总结

偏向锁: 在不存在多线程竞争的情况下,默认开启偏向锁

偏向锁升级轻量锁:当一个对象持有偏向锁,一旦第二个线程访问这个对象如果产生竞争,偏向锁升级为轻量级锁

轻量级锁升级重量级锁:一般两个线程对于同一个锁的操作都会错开,或者自旋等待一下,另一个线程就会释放锁,自旋超过一定次数或者一个线程在持有锁,一个在自旋又来了第三个线程访问时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以为的线程都阻塞,防止CPU空转

自旋锁和自适应自旋锁

自旋锁(CAS):

线程频繁的进行上下文切换会让CPU负担很重。从应用层面来说,对象锁的锁状态只会持续很短一段时间,为了这段很短的时间频繁的阻塞和唤醒线程非常不值得,所以要使用自旋锁

自旋锁就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁,等待方式就是执行一段无意义的循环

自旋等待不能替代阻塞,可以避免线程切换带来的开销,但是会占用CPU的资源。如果持有锁的线程很快释放了锁,那么自旋效率就非常好,反之自旋线程就会白白消耗掉处理的资源反而会带来性能的浪费。所有自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起

自旋锁在JDK1.4中引入,默认关闭,可以通过-XX:+UseSpinning开启

在JDK1.6中默认开启,默认次数为10,可以通过参数-XX:PreBlockSpin来调整

如果通过-XX:preBlockSpin来调整自旋锁的自旋次数,会带来很多不便,比如: 在自旋n次失败退出系统释放锁了,所有在JDK1.6引入自适应的自旋锁

自适应自旋锁:

次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功了,下次自旋的次数会更加多,虚拟机认为上次成功了,那么这次自旋也很有可能成功。反之如果某个锁很少有自旋成功的,那么在以后要获得这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费CPU资源

锁消除机制:

在当前并不需要使用锁的情况下加锁了,JVM在对代码进行编译执行的时候会自动把锁消除掉

CAS算法

CAS见名思意,比较并替换

比较并替换:

是用于实现多线程同步的原子操作

实现原子性操作可以使用锁,synchronized基于阻塞的机制,当一个线程拥有锁的时候,访问同一资源的其他线程需要等待,直到该线程释放锁

优先级低的线程抢到锁,优先级高的线程怎么办?

获取锁的线程一直不释放怎么办?

大量线程来竞争资源,很多自旋操作会花费大量时间和资源来处理这些竞争

死锁问题处理

锁机制是一种较为粗糙,粒度比较大的机制,对于一直简单需求有点过于笨重

CAS实现原理

现代处理器基本都是CAS指令,每个CAS操作过程都包含三个运算符:内部地址V(旧值)、期望值A、新值B

悲观锁、乐观锁:

悲观锁:

就是认为当前每次对数据的操作都有可能被其他人改变,每次在获取数据时都会加锁 这样别人想拿到这个数据就会阻塞,直到它获取到锁

乐观锁:

每次获取时都认为别人不会修改,在查询时不加锁,但是在更新时会判断在此期间别人有没有更新这个数据,可使用版本号实现。适用于读多写少的场景

CAS 的一些问题:

ABA:

一个线程将a值改成了b,接着又改成了a,CAS认为是没有改变的,其实是已经变化过了

解决方法: 通过添加版本号解决

CAS 循环时间长开销大:

CAS基于CPU进行自旋操作,如果CAS失败,就会一直进行尝试,如果自旋时间过长,CPU性能开销巨大

CAS只能保证一个共享变量的原子操作:

对一个变量执行操作时,可以使用CAS循环方式保证原子操作,但是对多个变量操作时,CAS无法保证操作的原子性

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code.song

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值