一、概念

是利用锁的机制来实现同步的。锁机制有如下两种特性:

  • 互斥性:即在同一时间**只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性**。

  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

二、用法

  • 按修饰的对象来分类:synchronized 关键字可以修饰静态方法,非静态方法、同步代码块
  • 按获取锁的分类:synchronized 可以获取锁对象(非静态方法),也可以获取锁(静态方法)

对象锁(用于修饰非静态方法)

  1. synchronized(this|object) {}

在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。

类锁(用于修饰静态方法)

  1. synchronized(类.class) {}

在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。

监视器

在 Java 中,每个对象都会有一个 monitor 对象(监视器)

  • 某一线程占有这个对象的时候,先 monitor计数器是不是 0,如果是 **0 就表示还线程还没有**占有,这个时候可以有线程占有这个对象,并且对这个对象的 monitor + 1;如果不为 **0,表示这个对象已经被其他线程占有,当前线程进入等待。当线程释放占有权的时候 monitor - 1**;
  • 同一线程可以对同一对象进行多次加锁,+1,+1,重入性

二、synchronized 原理分析

  1. public class Sync {
  2. public void assess() {
  3. synchronized (this) {
  4. try {
  5. System.out.println(Thread.currentThread().getName() + " running");
  6. TimeUnit.MINUTES.sleep(2);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. }
  12. public static void main(String[] args) {
  13. Sync sync = new Sync();
  14. for (int i = 0; i < 5; i++) {
  15. new Thread(sync::assess).start();
  16. }
  17. }
  18. }

线程堆分析

可用工具 jconsole.exe 或命令 jstack pid 来查看线程状态

  • 通过 jconsole 来查看

image.png

image.png

  • 通过 jstack pid 来查看
  1. PS C:\Users\y> jstack 17004

image.png

image.png

JVM 指令分析

  1. javap -c Sync > Sync.txt
  • 针对 synchronized 包裹的代码块(代码块的加锁 monitorentermonitorexit 配合使用)

image.png

  • synchronized 修饰的方法(ACC_SYNCHRONIZED 标记为互斥方法)

image.png

synchronized 优化

偏向锁 - 轻量锁 - 重量级锁

一个对象实例包含了对象头、实例变量、填充数据

image.png

image.png

  1. |--------------------------------------------------------------|
  2. | Object Header (64 bits) |
  3. |------------------------------------|-------------------------|
  4. | Mark Word (32 bits) | Klass Word (32 bits) |
  5. |------------------------------------|-------------------------|
  1. |-------------------------------------------------------|--------------------|
  2. | Mark Word (32 bits) | State |
  3. |-------------------------------------------------------|--------------------|
  4. | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
  5. |-------------------------------------------------------|--------------------|
  6. | thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
  7. |-------------------------------------------------------|--------------------|
  8. | ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
  9. |-------------------------------------------------------|--------------------|
  10. | ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
  11. |-------------------------------------------------------|--------------------|
  12. | | lock:2 | Marked for GC |
  13. |-------------------------------------------------------|--------------------|

三、锁

偏向锁

线程竞争不是很强的时候使用,简单来说就是偏向于第一次占有它的线程,也就是第一次占有它的线程得到锁的几率比较大,偏向锁和无锁状态获取对象的时间非常接近

  • 在对象第一次被线程占有的时候,将是否偏向状态置为 1,锁标记:01,并写入线程 ID
  • 当其他的线程继续访问该对象的时候,就会产生竞争
    • 竞争成功:当线程进行竞争的该锁的时候,发现 线程 ID 与当前竞争线程 ID 相同,则竞争到锁
    • 竞争失败:当线程进行竞争的该锁的时候。发现 线程 ID 与当前竞争线程 ID 不同,则竞争失败
      • 失败以后偏向锁升级轻量级锁
      • 通过 CAS 算法变更 线程 ID,并将锁标记位设置为 00

**

轻量级锁

互斥性不是很强,且线程有交替执行的时候适用

  • 通过 CAS 竞争失败的话,就会升级重量级锁,将锁标记位设置为 10

重量级锁

强互斥,由于重量级锁是由操作系统实现的,所以等待时间长

自旋锁

由于从轻量级锁升级位重量级锁,所消耗的代价很大(用户线程调用操作系统线程),为了解决这种问题,可以让线程先等等,在尝试去拿锁,自旋锁就是在竞争失败的时候,不是马上进行锁的升级,而是执行几次空循环,再尝试去拿锁

锁消除

JIT 在进行编译的过程中,把不必要的锁去掉