synchronize和CAS

程序有两种不同的级别:用户态、内核态
原来的操作系统中没有这种级别之分,因此一个程序可能访问其他程序的内存,就可能把整个机器都干掉。一般来说,操作系统跑在内核态,内核态的程序可以访问硬件(如内存,进行线程调度等),当用户态的程序需要访问硬件时,就需要向操作系统申请。在CPU硬件上分为ring0 - 1 - 2 -3,应用程序一般跑在ring3级,操作系统跑在ring0级,ring0级的程序可以直接访问硬件而ring3级不行。

有轻量级锁和重量级锁之分,轻量级锁都是在用户态完成的,而重量级锁需要进过操作系统。

1. CAS

CAS的全称为CompareAndSwap,它是一条CPU并发原语
功能:比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作。如果不相同则继续比较直到工作内存中的值和主内存中的值相等为止。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS的汇编指令(lock cmpxchg指令)。这是一种完全依赖于硬件的功能(锁总线),通过它实现了原子操作。原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,线程安全。

2.Unsafe类

是CAS的核心类,存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,所有方法都被native修饰。变量valueoffset,表示该变量值在内存中的偏移地址,Unsafe就根据内存的偏移地址获取数据,value被volatile修饰,保证了多线程之间的内存可见性。

CAS.png

不断的循环看当前内存地址中的值是否是我期待的值,如果是则进行下面的操作,如果不是则继续循环。

缺点:1. 循环时间长开销大(如果CAS失败,会一直进行尝试)
​ 2. 只能保证一个共享变量的原子操作
3. ABA问题

2. synchronized的锁升级过程

Hotspot的锁实现.png

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。
在1.6中,锁一共有4中状态

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态
  1. 偏向锁意义:据调查,大多数情况下,锁总是被同一线程调用,所以就出现了偏向锁。
    偏向锁总是偏向于第一次获取锁的线程,一般无竞争状态时使用的就是偏向锁,可以使用-XX:+/-UseBiasedLocking参数启用/关闭偏向锁,默认是开启的(线程数多的时候,适合关闭偏向锁,因为竞争激烈,锁会升级,关闭后可以省略升级过程,直接使用轻量级锁)。

偏向锁加锁过程:
1)执行到monitorenter(使用monitorenter和monitorexit两个指令实现同步)时,首先检查锁标志是否为01(01表示未上锁)
2)其次检查偏向锁位是否为0(0表示无锁,1表示当前已是偏向锁)
3)如果检查成功,通过CAS操作,将Mark Word中的线程ID设置为自己的线程ID,然后将偏向锁位设置为1
4)如果第二次仍是该线程进入同步区,那么不再需要执行第三步
5)如果第二次是其他线程进入,那么锁升级
竞争激烈,多个线程同时争锁,锁升级为轻量级锁
第一个线程进入后设置为偏向锁,第二次再来的线程不是该线程而是其他线程,锁升级为轻量级锁
 偏向锁是默认启用的,它会在程序启动几秒后才激活,可以通过JVM参数:-XX:BiasedLockingStartupDelay=0关闭延迟。也可以使用参数:-XX:UserBiasedLocking=false关闭偏向锁

  • 2.轻量级锁
    加锁过程:
    1)当某个线程想要占用这把锁的时候,它会首先在自己的栈(帧)中创建一个锁记录(LockRecord)
    2)然后将Mark Word信息复制(hashCode、分代年龄等)到LockRecord中
    3)用CAS操作将Mark Word替换掉LR指针,即将Mark Word中除了标志位外的部分替换成一个指针
    4)指向自己的LockRecord哪个线程替换成功,哪个线程就成功得到了锁,轻量级锁自旋次数超过10(任意一个线程),或者正在自旋的线程超过CPU核数一半,那么轻量级锁升级为重量级锁(jdk1.6以前,jdk1.6以后默认启动自适应自旋),可以通过-XX:+PreBlockSpin指定自旋次数。

3.重量级锁
因为需要阻塞,并且需要向操作系统级别申请,所以说是重量级。重量级锁是操作系统来决定由哪个线程来获得锁的:(用户态到内核态的访问
1)生成或者复用monitor对象
2)Mark Word不再指向LR,而是指向monitor对象
3)线程阻塞,进入_EntryList排队,由操作系统决定唤醒谁
4)操作系统唤醒线程后该线程的Mark Word替换为monitor pointer,然后执行

monitor对象
 monitor可以理解为一个操作系统级别的互斥变量,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。
在hotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的

Java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象

锁升级.png

整个过程:new对象的时候首先看是否开启了偏向锁,启动了就匿名偏向(即没有偏向任何线程),匿名偏向直接升级为偏向锁,偏向锁轻度竞争升级为轻量级锁,再重度竞争则升级成重量级锁。没有启动偏向锁就生成普通对象,普通对象一般直接升级为轻量级锁,轻量级锁重度竞争升级成重量级锁。

3.打开偏向锁效率是否一定提高?为什么?

在明确知道有线程竞争的情况下,因为偏向锁是存在竞争的,启动偏向锁不是一个合适的选择。默认4s后启动偏向锁是因为JVM在启动的时候一定会产生多线程的竞争。因此默认情况下延迟4s启动偏向锁。

4. 如何解决ABA问题

使用AtomicStampedReference,带有时间戳(版本号)。

你可能感兴趣的:(synchronize和CAS)