Java
Java多线程

Java多线程 02 - synchronized关键字

简介:在Java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了obj这个对象的同步锁。

1. synchronized简介

在Java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了obj这个对象的同步锁。

不同线程对同步锁的访问是互斥的。也就是说,某个时间点对象的同步锁只能被一个线程获取到!通过同步锁我们就能在多线程中实现对对象或方法的互斥访问。例如,现在有两个线程A和线程B,它们都会访问对象obj的同步锁。假设,在某一时刻,线程A获取到obj的同步锁并在执行一些操作;而此时,线程B也企图获取obj的同步锁,线程B将会获取失败,它必须等待,直到线程A释放了obj对象的同步锁之后线程B才能获取到obj对象的同步锁从而才可以运行。

2. synchronized方法和synchronized代码块

synchronized方法是用synchronized修饰方法,而synchronized代码块则是用synchronized修饰代码块,如下代码:

  • // synchronized方法
  • public synchronized void test1() {
  • System.out.println("synchronized methoed");
  • }
  • // synchronized代码块
  • public void test2() {
  • synchronized (this) {
  • System.out.println("synchronized methoed");
  • }
  • }

synchronized代码块中的this是指当前对象,也可以将this替换成其他对象,例如将this替换成obj,则test2()在执行synchronized(obj)时就获取的是obj的同步锁。

synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率。下面通过一个示例来演示:

  • public class SyncMethodAndSyncCodeBlock {
  • public synchronized void syncMethod() {
  • for (int i = 0; i < 10000000; i++)
  • ;
  • }
  • public void syncBlock() {
  • synchronized (this) {
  • for (int i = 0; i < 10000000; i++)
  • ;
  • }
  • }
  • public static void main(String[] args) {
  • SyncMethodAndSyncCodeBlock syncMethodAndSyncCodeBlock = new SyncMethodAndSyncCodeBlock();
  • // 测试synchronized方法性能
  • long startTime = System.nanoTime();
  • syncMethodAndSyncCodeBlock.syncMethod();
  • long endTime = System.nanoTime();
  • System.out.println("Sync Method cost: " + (endTime - startTime) / 1000000000.0 + " s.");
  • // 测试synchronized代码块性能
  • startTime = System.nanoTime();
  • syncMethodAndSyncCodeBlock.syncBlock();
  • endTime = System.nanoTime();
  • System.out.println("Sync Block cost: " + (endTime - startTime) / 1000000000.0 + " s.");
  • }
  • }

随机执行结果如下:

  • Sync Method cost: 0.003479 s.
  • Sync Block cost: 0.001739 s.

3. synchronized基本规则

我们将synchronized的基本规则总结为下面3条,并通过实例对它们进行说明。

  1. 当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的该synchronized方法或者synchronized代码块的访问将被阻塞。
  2. 当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程仍然可以访问该对象的非synchronized方法或代码块。
  3. 当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的其他的synchronized方法或者synchronized代码块的访问将被阻塞。

3.1. 第一条规则验证

当一个线程访问某对象synchronized方法或者synchronized代码块时,其他线程对该对象的该synchronized方法或者synchronized代码块的访问将被阻塞

我们编写以下代码:

  • class Rule_1 implements Runnable {
  • @Override
  • public void run() {
  • synchronized (this) {
  • for (int i = 0; i < 3; i++) {
  • try {
  • // 稍作停顿,尝试让其他线程抢占CPU资源
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(Thread.currentThread().getName() + ": " + i);
  • }
  • }
  • }
  • }
  • public class RuleTest {
  • public static void main(String[] args) {
  • Rule_1 rule_1 = new Rule_1();
  • // 创建两条线程
  • Thread thread1 = new Thread(rule_1);
  • Thread thread2 = new Thread(rule_1);
  • // 启动线程
  • thread1.start();
  • thread2.start();
  • }
  • }

在上述的代码中,run()方法中存在synchronized(this)代码块,而且两条都是基于rule_1这个Runnable对象创建的线程。这就意味着,我们可以将synchronized(this)中的this看作是rule_1这个Runnable对象;因此,两条共享rule_1对象的同步锁。所以当一个线程运行的时候,另外一个线程必须等待正在运行的线程释放rule_1的同步锁之后才能运行。打印结果如下:

  • Thread-0: 0
  • Thread-0: 1
  • Thread-0: 2
  • Thread-1: 0
  • Thread-1: 1
  • Thread-1: 2

当我们以下面的方法运行两条线程:

  • public class RuleTest {
  • public static void main(String[] args) {
  • Rule_1 rule_1 = new Rule_1();
  • Rule_1 rule_2 = new Rule_1();
  • Thread thread1 = new Thread(rule_1);
  • Thread thread2 = new Thread(rule_2);
  • thread1.start();
  • thread2.start();
  • }
  • }

会得到下面的打印结果:

  • Thread-0: 0
  • Thread-1: 0
  • Thread-0: 1
  • Thread-1: 1
  • Thread-0: 2
  • Thread-1: 2

可以发现,这次是两条线程交替执行的,这是由于创建两条线程时使用的是不同的Runnable对象rule_1rule_2,所以在synchronized(this)中的this分别表示的是rule_1rule_2的同步锁,两者是不同的,所以两条线程并不会互相干扰运行。

3.2. 第二条规则验证

当一个线程访问某对象synchronized方法或者synchronized代码块时,其他线程仍然可以访问该对象的非synchronized方法或代码块

这里需要注意的是,两个线程访问的是某个对象,在这个特定对象中存在synchronized方法或者synchronized代码块和非synchronized方法或代码块。有以下的测试代码:

  • class ShareInstance {
  • public synchronized void syncMethod() {
  • for (int i = 0; i < 3; i++) {
  • System.out.println(Thread.currentThread().getName() + " invoked ShareInstance syncMethod " + i);
  • try {
  • Thread.sleep(300);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • public void nonSyncMethod() {
  • System.out.println(Thread.currentThread().getName() + " invoked ShareInstance nonSyncMethod");
  • }
  • }
  • public class RuleTest {
  • public static void main(String[] args) {
  • // 同一个对象
  • final ShareInstance shareInstance = new ShareInstance();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • shareInstance.syncMethod();
  • }
  • }).start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • shareInstance.nonSyncMethod();
  • }
  • }).start();
  • }
  • }

在上述代码中,两个线程都是使用同一个Runnable对象shareInstance创建的,第一个线程调用了synchronized方法,第二个线程调用的是普通方法,运行结果如下:

  • Thread-0 invoked ShareInstance syncMethod 0
  • Thread-1 invoked ShareInstance nonSyncMethod
  • Thread-0 invoked ShareInstance syncMethod 1
  • Thread-0 invoked ShareInstance syncMethod 2

可以看到,第二个线程在第一个线程访问synchronized方法期间还是可以访问其他非synchronized方法的。

3.3. 第三条规则验证

当一个线程访问某对象synchronized方法或者synchronized代码块时,其他线程对该对象的其他的synchronized方法或者synchronized代码块的访问将被阻塞

第三条规则与第一条规则非常相似,但有很大的区别。第三条规则表明:一个对象的synchronized关键字修饰的所有方法和代码块都会在某个线程执行时被保护。有以下的测试代码:

  • class ShareInstance {
  • public synchronized void syncMethod() {
  • for (int i = 0; i < 3; i++) {
  • System.out.println(Thread.currentThread().getName() + " invoked ShareInstance syncMethod" + i);
  • try {
  • Thread.sleep(300);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • public void nonSyncMethod() {
  • System.out.println(Thread.currentThread().getName() + " invoked ShareInstance nonSyncMethod");
  • }
  • public void syncBlockMethod() {
  • System.out.println(Thread.currentThread().getName() + " entered ShareInstance syncBlockMethod");
  • synchronized (this) {
  • for (int i = 0; i < 3; i++) {
  • System.out.println(Thread.currentThread().getName() + " invoked ShareInstance syncBlockMethod " + i);
  • try {
  • Thread.sleep(300);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }
  • }
  • public class RuleTest {
  • public static void main(String[] args) {
  • // 同一个对象
  • final ShareInstance shareInstance = new ShareInstance();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • // 调用synchronized方法
  • shareInstance.syncMethod();
  • }
  • }).start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • // syncBlockMethod方法中有synchronized代码块
  • shareInstance.syncBlockMethod();
  • }
  • }).start();
  • }
  • }

尝试多次执行,循环体内的打印顺序都会保持不变:

  • Thread-0 invoked ShareInstance syncMethod 0
  • Thread-1 entered ShareInstance syncBlockMethod
  • Thread-0 invoked ShareInstance syncMethod 1
  • Thread-0 invoked ShareInstance syncMethod 2
  • Thread-1 invoked ShareInstance syncBlockMethod 0
  • Thread-1 invoked ShareInstance syncBlockMethod 1
  • Thread-1 invoked ShareInstance syncBlockMethod 2

这是由于当第一个线程进入syncMethod()方法后,就会获取shareInstance对象的同步锁,此时第二个线程执行syncBlockMethod(),当进入该方法后,会首先打印Thread-1 entered ShareInstance syncBlockMethod,而接下来的代码是一个synchronized代码块,且也是使用的shareInstance对象作为同步锁,但此时syncMethod()方法还在执行,第一个线程还没有释放锁,所以第二个线程只能等待第一个线程执行完后将锁释放,才能继续往下执行。

4. 实例锁和全局锁

  • 实例锁:锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。
  • 全局锁 :该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class字节码对象(SomeClass.class)或者ClassLoader对象上)。

下面是关于实例锁和全局锁的例子:

  • public class LockTypes {
  • public synchronized void syncA() { ... }
  • public synchronized void syncB() { ... }
  • public static synchronized void syncC() { ... }
  • public static synchronized void syncD() { ... }
  • }

假设,LockTypes有两个实例x和y,有下面4组表达式获取的锁的情况:

  1. x.syncA()x.syncB():不能同时访问
  2. x.syncA()y.syncA():可以同时访问
  3. x.syncC()y.syncD():不能同时访问
  4. x.syncA()LockTypes.syncC():可以同时访问

下面将分别对这四种情况进行测试,以验证结果。

  1. 首先是x.syncA()x.syncB(),不能同时访问,因为x.syncA()x.syncB()使用的是同一个同步锁(即对象x)。有下面的测试代码:
  • public class LockTypes {
  • public synchronized void syncA() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncA, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public synchronized void syncB() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncB, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public static void main(String [] args) {
  • final LockTypes x = new LockTypes();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • x.syncA();
  • }
  • }, "Thread 1").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • x.syncB();
  • }
  • }, "Thread 2").start();
  • }
  • }

多次运行,得到的运行结果都一致,如下:

  • Thread 1 : syncA, 0
  • Thread 1 : syncA, 1
  • Thread 1 : syncA, 2
  • Thread 1 : syncA, 3
  • Thread 1 : syncA, 4
  • Thread 2 : syncB, 0
  • Thread 2 : syncB, 1
  • Thread 2 : syncB, 2
  • Thread 2 : syncB, 3
  • Thread 2 : syncB, 4

这表示第二个线程只能等待第一个线程对syncA()方法执行结束后才能执行syncB()方法。

  1. 接下来是x.syncA()y.syncA(),可以同时访问,因为x.syncA()y.syncA()使用的是不同的同步锁(一个是对象x,一个是对象y)。有下面的测试代码:
  • public class LockTypes {
  • public synchronized void syncA() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncA, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public synchronized void syncB() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncB, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public static void main(String [] args) {
  • final LockTypes x = new LockTypes();
  • final LockTypes y = new LockTypes();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • x.syncA();
  • }
  • }, "Thread 1").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • y.syncA();
  • }
  • }, "Thread 2").start();
  • }
  • }

运行的结果每次都有变化,下面是随机的一次:

  • Thread 1 : syncA, 0
  • Thread 2 : syncA, 0
  • Thread 1 : syncA, 1
  • Thread 2 : syncA, 1
  • Thread 2 : syncA, 2
  • Thread 1 : syncA, 2
  • Thread 2 : syncA, 3
  • Thread 1 : syncA, 3
  • Thread 1 : syncA, 4
  • Thread 2 : syncA, 4
  1. 接下来是x.syncC()y.syncD(),不能同时访问,因为x.syncC()y.syncD()其实使用的是同一个同步锁(即LockTypes类对象)。有下面的测试代码:
  • public class LockTypes {
  • public static synchronized void syncC() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncC, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public static synchronized void syncD() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncD, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public static void main(String [] args) {
  • final LockTypes x = new LockTypes();
  • final LockTypes y = new LockTypes();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • x.syncC();
  • }
  • }, "Thread 1").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • y.syncD();
  • }
  • }, "Thread 2").start();
  • }
  • }

多次运行,得到的运行结果都一致,如下:

  • Thread 1 : syncC, 0
  • Thread 1 : syncC, 1
  • Thread 1 : syncC, 2
  • Thread 1 : syncC, 3
  • Thread 1 : syncC, 4
  • Thread 2 : syncD, 0
  • Thread 2 : syncD, 1
  • Thread 2 : syncD, 2
  • Thread 2 : syncD, 3
  • Thread 2 : syncD, 4
  1. 接下来是x.syncA()LockTypes.syncC(),可以同时访问,因为x.syncA()LockTypes.syncC()使用的是不同的同步锁(一个是对象x,一个是LockTypes)。有下面的测试代码:
  • public class LockTypes {
  • public synchronized void syncA() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncA, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public static synchronized void syncC() {
  • try {
  • for (int i = 0; i < 5; i++) {
  • // 休眠100ms
  • Thread.sleep(100);
  • System.out.println(Thread.currentThread().getName() + " : syncC, " + i);
  • }
  • } catch (InterruptedException ie) {
  • }
  • }
  • public static void main(String[] args) {
  • final LockTypes x = new LockTypes();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • x.syncA();
  • }
  • }, "Thread 1").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • LockTypes.syncC();
  • }
  • }, "Thread 2").start();
  • }
  • }

运行的结果每次都有变化,下面是随机的一次:

  • Thread 1 : syncA, 0
  • Thread 2 : syncC, 0
  • Thread 1 : syncA, 1
  • Thread 2 : syncC, 1
  • Thread 1 : syncA, 2
  • Thread 2 : syncC, 2
  • Thread 1 : syncA, 3
  • Thread 2 : syncC, 3
  • Thread 1 : syncA, 4
  • Thread 2 : syncC, 4

5. 从字节码指令看synchronized关键字

  • public class ThreadTest {
  • public int test() {
  • synchronized (this) {
  • int value = 1;
  • value++;
  • return value;
  • }
  • }
  • }

反编译结果:

  • λ javap -c ThreadTest
  • Compiled from "ThreadTest.java"
  • public class ThreadTest {
  • public ThreadTest();
  • Code:
  • 0: aload_0
  • 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  • 4: return
  • public int test();
  • Code:
  • 0: aload_0
  • 1: dup
  • 2: astore_1
  • 3: monitorenter
  • 4: iconst_1
  • 5: istore_2
  • 6: iinc 2, 1
  • 9: iload_2
  • 10: aload_1
  • 11: monitorexit
  • 12: ireturn
  • 13: astore_3
  • 14: aload_1
  • 15: monitorexit
  • 16: aload_3
  • 17: athrow
  • Exception table:
  • from to target type
  • 4 12 13 any
  • 13 16 13 any
  • }

从反编译代码可知,synchronized关键词是通过的monitorentermonitorexit这两条指令来实现同步锁的。