概览
在并发编程中,锁是一种常用的保证线程安全的方法。Java 中常用的锁主要有两类,一种是 Synchronized
修饰的锁,被称为 Java 内置锁或监视器锁。另一种就是在 J2SE 1.5 版本之后的 java.util.concurrent
包(下称 j.u.c 包)中的各类同步器,包括 ReentrantLock(可重入锁),ReentrantReadWriteLock(可重入读写锁),Semaphore(信号量),CountDownLatch 等。这些同步器都是基于 AbstractQueuedSynchronizer(下称 AQS)这个简单的框架来构建的,而 AQS 类的核心数据结构是一种名为 Craig, Landin, and Hagersten locks(下称 CLH 锁)的变体。
Synchronized锁
Synchronized 锁的底层类别
不同锁下对象头中的内容
这里主要是与锁相关的内容
偏向锁:线程 ID,锁标志为 01
轻量级锁:指向栈帧中锁记录的指针,锁标志为 00
重量级锁:指向互斥量(重量级锁)的指针,锁标志为 10
实现偏向锁两个额外的域,第一个域用于记录线程id,第二个域用于记录锁的状态。每次都是将自己的线程号放到标记字段中,这样每次如果每次线程号都是自己的,那么就不用进行锁处理了。
如果不是自己的,同时状态又是被锁,那么就会进行锁是升级,也就是升级为轻量级锁,以保证只有一个线程能获得锁。
偏向锁
背景:大多数时候都不会遇到锁,并且往往需要锁的都是同一个线程
加锁:当一个线程访问同步块并获得锁时,会在对象的头部和栈帧中记录存储该线程的ID
解锁:
如果存储的ID不是自己的ID,说明其它线程持有了锁,那么就会发起竞争,如果存储的线程没有使用,那么就会释放锁
如果是自己的ID,那么说明没有其他线程持有锁,就可以直接访问数据了
这样只有发生锁竞争的时候才会释放锁,从而提高锁的性能
轻量级锁
通过自旋的方式来对线程进行阻塞
加锁:在线程的栈帧中创建存储锁记录的空间,将对象头的mark word复制到锁记录中,尝试将对象头的mark word替换为指向锁记录的指针,如果成功,说明没有竞争直接获得锁,如果失败,说明锁已经被占用,通过自旋获得锁
解锁:用栈帧中的锁记录替换对象头的mark word,如果成功,说明没有竞争,如果失败,说明有其他线程在尝试获得锁,存在竞争,锁会膨胀为重量级锁,并不再恢复
重量级锁:获得锁失败时就会陷入阻塞,等待被唤醒。
轻量级锁加锁过程
线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用于存储锁记录的空间,然后将对象头中的 Mark Word 复制到锁记录中。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。
锁记录:用于存储锁对象目前的Mark Word的拷贝(也就是对象自身的 HashCode,分代年龄等信息)
偏向锁则只是将 Mark Word 中的线程 ID 修改为自己的 ID
重量级锁则是让线程进入等待状态,而不是自旋状态
字节码层面
synchronized关键字最主要的三种使⽤⽅式
修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁
修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀ 个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管new了多少个对象,只有 ⼀份)。所以如果⼀个线程A调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程B需要调⽤ 这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前 实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。
synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method