白话解析synchronized关键字

1.引言

synchronized关键字是JDK提供的线程并发安全方案,本文将使用尽可能直白的描述,结合图表,对synchronized进行解析。

2.用法

2.1修饰静态方法

修饰静态方法时,锁对象为静态方法所在类的class对象,意味着,如果当前类的多个静态方法被synchronized关键字修饰,则这些方法是同步执行的,同一时刻只能有一个线程执行该类的静态方法,并且,若在其他代码块中对该类的class对象进行加锁,该代码块也会与静态方法同步,示例代码如下。

package share.synch;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 静态方法/代码块
 *
 * @author shanpao
 */
public class SyncStatic {

    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 1,
            TimeUnit.SECONDS, new LinkedBlockingDeque<>(), r -> new Thread(r));

    public static void main(String[] args) {
        executor.submit(SyncStatic::sayHello);
        executor.submit(SyncStatic::sayBye);
        executor.submit(SyncStatic::sayYes);

        SyncStatic syncStatic = new SyncStatic();
        executor.submit(() -> syncStatic.sayNo());

        executor.shutdown();
    }

    /**
     * lock with class
     */
    private static synchronized void sayHello() {
        System.out.println("=== hello enter ===");
        System.out.println(Thread.currentThread().getId() + " say hello!");
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("=== hello exit ===");
    }


    /**
     * lock with class
     */
    private static synchronized void sayBye() {
        System.out.println("--- bye enter ---");
        System.out.println(Thread.currentThread().getId() + " say bye!");
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("--- bye exit ---");
    }

    /**
     * lock with class
     */
    private static synchronized void sayYes() {
        synchronized (SyncStatic.class) {
            System.out.println("+++ yes enter +++");
            System.out.println(Thread.currentThread().getId() + " say yes!");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("+++ yes exit +++");
        }
    }

    /**
     * lock with this
     */
    private synchronized void sayNo() {
        System.out.println("+++ no enter +++");
        System.out.println(Thread.currentThread().getId() + " say no!");
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("+++ no exit +++");
    }
}

2.2 修饰实例方法

修饰实例方法时,锁定对象为当前类对象,即this指向的对象,即当前对象中所有的实例方法调用与其他基于当前对象的同步代码块执行都是同步的,实例代码如下。

package share.synch;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 修饰实例方法
 * @author shanpao
 */
public class SyncDynamic {
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 1,
            TimeUnit.SECONDS, new LinkedBlockingDeque<>(), r -> new Thread(r));

    public static void main(String[] args) {
        SyncDynamic syncDynamic = new SyncDynamic();
        executor.submit(() -> syncDynamic.sayHello());
        executor.submit(() -> syncDynamic.sayBye());
        executor.submit(() -> syncDynamic.sayYes());
    }

    private synchronized void sayHello() {
        System.out.println("=== hello enter ===");
        System.out.println(Thread.currentThread().getId() + " say hello!");
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("=== hello exit ===");
    }

    private synchronized void sayBye() {
        System.out.println("--- bye enter ---");
        System.out.println(Thread.currentThread().getId() + " say bye!");
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("--- bye exit ---");
    }

    private synchronized void sayYes() {
        synchronized (this) {
            System.out.println("+++ yes enter +++");
            System.out.println(Thread.currentThread().getId() + " say yes!");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("+++ yes exit +++");
        }
    }
}

2.3 修饰代码块

修饰代码块时,锁定对象为括号内的参数,可以为任意类型的对象,同步代码块使用的对象相同时,各代码块同步执行。此处需要注意单例对象的加锁,以及系统中缓存的对象,如Integer,Boolean对象等,实例代码如下。

package share.synch;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 修饰代码块
 *
 * @author shanpao
 */
public class SyncBlock {
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 1,
            TimeUnit.SECONDS, new LinkedBlockingDeque<>(), r -> new Thread(r));

    public static void main(String[] args) {
        SyncBlock syncBlock = new SyncBlock();
        executor.submit(() -> syncBlock.sayHello());
        executor.submit(() -> syncBlock.sayBye());
        executor.submit(() -> syncBlock.sayYes());
        executor.submit(() -> syncBlock.sayNo());
        executor.submit(() -> syncBlock.sayNothing());
        executor.shutdown();
    }

    private void sayHello() {
        synchronized (this) {
            System.out.println("=== hello enter ===");
            System.out.println(Thread.currentThread().getId() + " say hello!");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("=== hello exit ===");
        }
    }

    private void sayBye() {
        synchronized (this) {
            System.out.println("--- bye enter ---");
            System.out.println(Thread.currentThread().getId() + " say bye!");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("--- bye exit ---");
        }
    }

    private void sayYes() {
        synchronized (this) {
            System.out.println("+++ yes enter +++");
            System.out.println(Thread.currentThread().getId() + " say yes!");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("+++ yes exit +++");
        }
    }

    private void sayNo() {
        Object obj = new Object();
        synchronized (obj) {
            System.out.println("+++ no enter +++");
            System.out.println(Thread.currentThread().getId() + " say no!");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("+++ no exit +++");
        }
    }

    private void sayNothing() {
        synchronized (new Integer(1)) {
            System.out.println("+++ nothing enter +++");
            System.out.println(Thread.currentThread().getId() + " say nothing!");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("+++ nothing exit +++");
        }
    }
}

3.实现

3.1 虚拟机指令

以同步代码块为例,反编译查看虚拟机指令。

package share.synch;

/**
 * 同步代码块虚拟机指令
 *
 * @author shanpao
 */
public class SyncJavac {

    public static void main(String[] args) {
        synchronized (SyncJavac.class) {
            String a = "a";
        }
    }
}
javap -v SyncJavac
Compiled from "SyncJavac.java"
public class share.synch.SyncJavac {
  public share.synch.SyncJavac();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class share/synch/SyncJavac
       2: dup
       3: astore_1
       4: monitorenter
       5: ldc           #3                  // String a
       7: astore_2
       8: aload_1
       9: monitorexit
      10: goto          18
      13: astore_3
      14: aload_1
      15: monitorexit
      16: aload_3
      17: athrow
      18: return
    Exception table:
       from    to  target type
           5    10    13   any
          13    16    13   any
}

由反汇编得到的Java指令集可以看出,同步代码块中的逻辑与monitorenter monitorexit两条指令之间逻辑对应,这两条指令表示程序进入临界区/退出临界区,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,该对象将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

3.2 Mark Word

synchronized存的锁是在对象头里的,对象头存储的数据内容如下表所示。

 

32位机/64位机Java对象头的数据内容《java并发编程的艺术》

 

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如表所示

Java对象头的存储结构《java并发编程的艺术》

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据

Mark Word的状态变化

 3.3 锁状态

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

3.3.1 偏向锁

Biased locking is an optimization technique used in the HotSpot Virtual Machine to reduce the overhead of uncontended locking. It aims to avoid executing a compare-and-swap atomic operation when acquiring a monitor by assuming that a monitor remains owned by a given thread until a different thread tries to acquire it. The initial lock of the monitor biases the monitor towards that thread, avoiding the need for atomic instructions in subsequent synchronized operations on the same object. When many threads perform many synchronized operations on objects used in a single-threaded fashion, biasing the locks has historically led to significant performance improvements over regular locking techniques. OPEN JDK

偏向锁定是HotSpot虚拟机中使用的一种优化技术,可以减少无竞争锁定的开销。它的目的是通过假定监视器一直归给定线程拥有,直到另一个线程尝试获取它为止,从而避免在获取监视器时执行比较交换原子操作。监视器的初始锁定将监视器偏向该线程,从而避免了对同一对象进行后续同步操作时需要原子指令。当许多线程对以单线程方式使用的对象执行许多同步操作时,数据显示,对锁施加偏向已导致与常规锁定技术相比,性能得到显着改善。

偏向锁更像一个标记位,对持有偏向锁的线程而言,只要mark word中的线程指向自己,就不再需要加锁操作,当其他线程尝试去获取偏向锁的时候,发现偏向锁指向了其他线程,偏向锁就会升级为轻量级锁。

加锁:测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

释放锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,(如果线程不处于活动状态/已退出同步代码块,则将对象头设置成无锁状态,撤销偏向锁;如果线程仍然活着且未退出同步代码块,此时锁会膨胀为轻量级锁)最后唤醒暂停的线程。

3.3.2 轻量级锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced MarkWord。

加锁:线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;

释放锁:使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

3.3.3 重量级锁

当锁对象被设置为重量级锁状态,其他来获取锁的线程会被阻塞,整体synchronized加锁的流程如下图(来自网络,侵删)。

synchronized原理 (来自  https://blog.dreamtobe.cn)

 4.总结

JVM团队在尽可能减少并发带来的线程上下文切换,估引入了偏向锁和轻量级锁,通俗得讲,在没有任何线程冲突时,采用不加锁策略,只是检查锁对象是否当前线程的偏向对象,当发生冲突时,升级为轻量级锁,通过自旋检查的方式避免冲突,当自旋也无法规避冲突时,锁升级为重量级锁,多线程互斥访问。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1. synchronized关键字在使用层面的理解 synchronized关键字Java中用来实现线程同步关键字,可以修饰方法和代码块。当线程访问被synchronized修饰的方法或代码块时,需要获取对象的,如果该已被其他线程获取,则该线程会进入阻塞状态,直到获取到为止。synchronized关键字可以保证同一时刻只有一个线程能够访问被定的方法或代码块,从而避免了多线程并发访问时的数据竞争和一致性问题。 2. synchronized关键字在字节码中的体现 在Java代码编译成字节码后,synchronized关键字会被编译成monitorenter和monitorexit指令来实现。monitorenter指令对应获取操作,monitorexit指令对应释放操作。 3. synchronized关键字在JVM中的实现 在JVM中,每个对象都有一个监视器(monitor),用来实现对象。当一个线程获取对象后,就进入了对象的监视器中,其他线程只能等待该线程释放后再去竞争synchronized关键字的实现涉及到对象头中的标志位,包括标志位和重量级标志位等。当一个线程获取后,标志位被设置为1,其他线程再去获取时,会进入自旋等待或者阻塞等待状态,直到标志位被设置为0,即被释放后才能获取。 4. synchronized关键字在硬件方面的实现 在硬件层面,的实现需要通过CPU指令和总线来实现。当一个线程获取时,CPU会向总线发送一个请求信号,其他CPU收到该信号后会进入自旋等待状态,直到被释放后才能获取。总线可以保证多个CPU之间的原子操作,从而保证的正确性和一致性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值