多线程与高并发(二):synchronized、volatile和CAS

一、synchronized

1、一些基本概念

1、synchronized可以在任意对象和方法上加锁,加锁的这段代码称为“互斥区”或“临界区”。当一个线程想要执行同步方法里面的代码时,会首先尝试拿到这把锁,如果拿到则能执行,如果不能拿到,则会不断去拿这把锁,直到拿到为止。

2、如果多个线程同时对同一个对象中的同一个实例变量进行操作时,可能会出现值被更改、值不同步的情况,进而影响程序的执行流程,这也叫做非线程安全。

3、synchronized既能保证了原子性,又能保证可见性。

4、synchronized是非公平锁。当一个线程想获取锁时,先试图插队,如果占用锁的线程释放了锁,下一个线程还没来得及拿锁,那么当前线程就可以直接获得锁;如果锁正在被其它线程占用,则排队,排队的时候就不能再试图获得锁了,只能等到前面所有线程都执行完才能获得锁。

5、synchronized是可重入锁。如果是一个同步方法调用另一个同步方法,它们同一个线程在调用且加的是同一把锁,则该线程在第二个同步方法申请锁时仍然会得到该对象的锁。每得到该锁一次,标记会+1,释放一次,标记会-1,当标记为0时,会完全释放该锁,其它线程可以去争抢该锁。

6、程序在执行过程中,如果出现异常,默认情况下锁会被释放。在并发处理的过程中,有异常时需要多加小心,不然很可能会发生不一致的情况。 例如多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。

7、以对象为锁时,最好在其引用前加final关键字,防止对象改变。

8、禁止以基本类型作为锁对象。例如以String类型为锁对象,但String类型是存在常量池中,可能造成多个锁对象其实是同一个锁对象。

2、synchronized实现同步的基础:

java中每一个对象都可以作为锁,具体表现为以下三个形式:

① 对于普通同步方法:锁的是当前实例对象,等同于synchronized(this)。一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它线程无论调用哪一个同步方法都只能等待,换句话说,只能有一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它synchronized方法(对象锁)
② 对于静态同步方法:所得是当前类的Class对象,等同于synchronized(T.class)。和普通同步方法一样,一个类中如果有多个静态同步方法时,只要有一个线程去调用了其中的一个静态方法时,其它线程无论调用哪一个静态方法都只能等待(类锁)。

注:T.class是单例吗?如果是在同一个ClassLoader空间T.class是单例,但不在同一个类加载器时,T.class不是单例。不同的类加载器互相之间不能访问。如果能访问它,T.class则是单例。
③ 对于同步方法块:锁的是synchronized括号里面的对象。

3、synchronized的底层实现

1、早期:

在jdk早期时,synchronized的底层实现是重量级锁,也就是synchronized每次都需要去找操作系统申请锁,导致synchronized效率非常低。

2、锁升级:

由于synchronized效率很低,在Java SE 1.6后对synchronized进行了一些改进,引入了锁升级的概念。在HotSpot的实现中,synchronized有四种锁状态,分别为:无锁、偏向锁、轻量级锁(自旋锁)、重量级锁。

了解四种锁状态前,首先了解一下对象头里的mark word

锁状态mark word
bit fieldtag bits
25bit4bit1bit2bit
23bit2bit是否可偏向锁标志位
无锁对象hashcode对象分代年龄001
偏向锁线程IDEpoch对象分代年龄101
轻量级锁指向线程栈中锁记录(LockRecord)的指针00
重量级锁指向互斥量(重量级锁的指针)10
GC标志11

① 无锁:对象没有被synchronized锁住时,为无锁状态。

② 偏向锁:当只有一个线程访问同步块并获取锁时,会在对象头中储存当前获取锁的线程的id,这时有其他线程来申请锁资源时,偏向锁会升级为轻量级锁。偏向锁是可重入锁,如果对象头中的线程id为当前线程的id,当前线程再次申请锁时,无需重新进行加锁和解锁。当前线程释放锁后,锁对象并没有真正地释放,只有当其他线程尝试竞争偏向锁时,偏向锁才会释放锁,且此时锁对象会升级为轻量级锁。

③ 轻量级锁(自旋锁):为了解决重量级锁效率低下的问题,JVM引入了轻量级锁概念。JVM会在当前线程的栈针中创建一个用于存储锁记录的空间LockRecord,会将锁对象头中的mark word信息复制到LockRecord中,并让LockRecord的Owner指针指向锁对象,然后会尝试使用CAS将对象头中的mark word信息替换为LockRecord的指针,如果替换成功则获取到锁对象,失败表示锁对象被其它线程抢到,然后会继续通过CAS的方式尝试获取到锁对象,一定次数之后,若还没获取到锁对象,则锁对象升级为重量级锁。

④ 重量级锁:通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间较慢。

注:

1、锁升级后是无法降级的。如果并发量减少,且是重量级锁时,可以替换锁对象进行降级。

2、当一个锁对象是无锁时,如果调用过了hashcode方法,就不会再进入偏向锁,因为对象头中的bit位已经被hashcode占用(对象的hashcode一旦生成就不能被改变),没有空间再去储存线程id,如果此时有线程来申请锁时,会直接进入轻量级锁状态。同样地,如果当前锁状态为偏向锁时,调用hashcode方法会让锁升级为轻量级锁。

想要更深刻理解锁升级概念,可以查看马士兵老师写的文章:《没错,我就是厕所所长》


二、Volatile

 1、volatile作用:

① 使变量在多个线程中可见

为什么线程之间不可见:每个线程都有一块属于自己的区域,当线程去访问某一个变量时,会将它的值从共享空间复制一份放在自己的工作空间里,该线程对这个值进行了任何的改变,首先会在自己的工作空间里改变它,然后立马写回到共享空间里。但是其它线程不会立马收到这个值改变的消息。

volatile如何实现线程间的可见性:本质是MESI缓存一致性协议。

② 禁止指令重排序

为什么会指令重排序:由于CPU为了提高执行效率,会将指令并发地执行,就会导致没有相关性的指令乱序执行。

volatile如何实现禁止重排序:JVM在指令前后增加了读屏障和写屏障。

为什么懒汉式单例需要加volatile关键字:如果没有加volatile关键字,可能会导致创建对象后先赋值给引用,此时对象内的成员变量还未完成初始化,当有其它线程获取这个单例进行使用时,发现引用已经有值,于是直接拿去使用,而这时会使用到对象还未完成初始化的成员变量。


三、CAS

1、什么是CAS:

CAS是Java的Unsafe类中的一个方法:CompareAndSet,比较并设定。

2、CAS的实现原理:

CompareAndSet方法有三个参数:v(要改变值的对象)、expected(期望的当前值)、newValue(新的值)。当想要改变一个值时,会判断它的当前值和期望的值是否一致,若一致则将对象改为新值,若不一致,则说明其它线程对该对象进行过修改,此次修改失败,于是重新尝试修改这个对象,直到它满足期望为止。

注:cas是cpu的原语支持,比较并修改在cpu指令上是连续执行的,中间不能被打断,所以不必担心在比较一致后之后修改之前被其它线程修改的情况。

3、ABA问题:

如果一个对象从A变为B又变为A后,CAS是无法识别的,因为它只和当前值进行比较。如果是基础类型,由于不影响结果值,可以不用关注。若是引用类型,可以考虑加版本解决。

4、CAS的底层实现—Unsafe类:

Unsafe类提供了类似C++手动管理内存的能力,但由于Unsafe类为final类,且构造方法为private,故不能继承和实例化,只能通过反射的方式创建其实例对象。Unsafe类中的方法几乎都为native方法,它提供了直接操作内存、直接生成类实例、直接操作类或实例变量、CAS相关的功能。

5、一些基于CAS实现的Atomic类:

AtomicInteger(CAS)、AtomicLong(CAS)、LongAdder(CAS,分段式锁,在并发较高时,把线程分组进行CAS相加,最后把各段的和相加)

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值