学习目标

  • 理解Java中对象锁与类锁的概念与区别
  • 掌握synchronized修饰方法与代码块的不同使用方式
  • 了解synchronized的底层实现原理
  • 能够使用synchronized解决实际并发问题

1. 对象锁与类锁的概念

在多线程环境中,当多个线程同时访问共享资源时,可能会导致数据不一致。Java提供了synchronized关键字来解决这个问题,它通过锁机制确保同一时刻只有一个线程可以执行被保护的代码。在Java中,锁主要分为两种:对象锁和类锁。

1.1 对象锁(实例锁)

对象锁是Java中最常见的锁类型,它锁定的是特定的对象实例。当一个线程获取了某个对象的锁后,其他线程必须等待这个线程释放锁后才能获取该对象的锁。 对象锁的获取方式有两种:

  1. 同步实例方法:synchronized修饰非静态方法
  2. 同步代码块:synchronized(this)或synchronized(实例对象)

看一个简单的对象锁示例:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.TimeUnit;
  3. public class ObjectLockDemo {
  4. // 定义一个共享变量
  5. private int counter = 0;
  6. // 使用synchronized修饰实例方法,锁是当前对象(this)
  7. public synchronized void incrementSync() {
  8. counter++;
  9. System.out.println(Thread.currentThread().getName() + " - 计数器:" + counter);
  10. try {
  11. // 线程睡眠500毫秒,便于观察效果
  12. TimeUnit.MILLISECONDS.sleep(500);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. // 使用synchronized代码块,锁是当前对象(this)
  18. public void incrementSyncBlock() {
  19. synchronized (this) {
  20. counter++;
  21. System.out.println(Thread.currentThread().getName() + " - 计数器:" + counter);
  22. try {
  23. // 线程睡眠500毫秒,便于观察效果
  24. TimeUnit.MILLISECONDS.sleep(500);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. public static void main(String[] args) {
  31. final ObjectLockDemo demo = new ObjectLockDemo();
  32. // 创建5个线程并发执行同步方法
  33. for (int i = 0; i < 5; i++) {
  34. new Thread(() -> {
  35. demo.incrementSync();
  36. }, "线程-" + i).start();
  37. }
  38. }
  39. }

运行上述代码,你会发现5个线程是按顺序执行incrementSync方法的,而不是并发执行。这是因为synchronized保证了同一时刻只有一个线程能够获取到对象锁并执行同步方法。

提示: 对象锁只对同一个对象实例有效。如果创建了多个对象实例,每个实例都有自己的对象锁,不同实例的锁互不干扰。

1.2 类锁(静态锁)

类锁是Java中另一种重要的锁类型,它锁定的是类而不是对象实例。无论创建了多少个实例,一个类只有一个类锁。当一个线程获取了某个类的类锁后,其他线程必须等待这个线程释放锁后才能获取该类的类锁。 类锁的获取方式有两种:

  1. 同步静态方法:synchronized修饰静态方法
  2. 同步代码块:synchronized(类名.class)

看一个类锁的示例:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.TimeUnit;
  3. public class ClassLockDemo {
  4. // 静态计数器
  5. private static int counter = 0;
  6. // 使用synchronized修饰静态方法,锁是当前类的Class对象
  7. public static synchronized void incrementStatic() {
  8. counter++;
  9. System.out.println(Thread.currentThread().getName() + " - 计数器:" + counter);
  10. try {
  11. TimeUnit.MILLISECONDS.sleep(500);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. // 使用synchronized代码块锁定类对象
  17. public void incrementWithClassLock() {
  18. synchronized (ClassLockDemo.class) {
  19. counter++;
  20. System.out.println(Thread.currentThread().getName() + " - 计数器:" + counter);
  21. try {
  22. TimeUnit.MILLISECONDS.sleep(500);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. public static void main(String[] args) {
  29. // 创建两个不同的实例
  30. final ClassLockDemo demo1 = new ClassLockDemo();
  31. final ClassLockDemo demo2 = new ClassLockDemo();
  32. // 创建5个线程,使用不同实例的方法
  33. for (int i = 0; i < 5; i++) {
  34. if (i % 2 == 0) {
  35. new Thread(() -> {
  36. ClassLockDemo.incrementStatic(); // 使用静态方法(类锁)
  37. }, "静态方法线程-" + i).start();
  38. } else {
  39. new Thread(() -> {
  40. demo2.incrementWithClassLock(); // 使用实例方法中的类锁
  41. }, "类锁代码块-" + i).start();
  42. }
  43. }
  44. }
  45. }

运行上述代码,你会发现无论是通过静态方法还是通过类锁代码块访问,所有线程都必须排队执行,因为它们争夺的是同一个类锁。

重要概念: 类锁实际上是锁定类的Class对象。在Java中,无论一个类有多少个对象实例,这个类只有一个Class对象,所以类锁也只有一个。

1.3 对象锁与类锁的对比

下面通过一个示例来展示对象锁与类锁的区别:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.TimeUnit;
  3. public class LockComparisonDemo {
  4. // 实例变量
  5. private int instanceCounter = 0;
  6. // 静态变量
  7. private static int staticCounter = 0;
  8. // 对象锁方法
  9. public synchronized void incrementInstance() {
  10. instanceCounter++;
  11. System.out.println(Thread.currentThread().getName() + " - 实例计数器:" + instanceCounter);
  12. try {
  13. TimeUnit.SECONDS.sleep(1);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. // 类锁方法
  19. public static synchronized void incrementStatic() {
  20. staticCounter++;
  21. System.out.println(Thread.currentThread().getName() + " - 静态计数器:" + staticCounter);
  22. try {
  23. TimeUnit.SECONDS.sleep(1);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. public static void main(String[] args) {
  29. // 创建同一个实例
  30. final LockComparisonDemo demo = new LockComparisonDemo();
  31. // 线程1:访问实例方法(对象锁)
  32. new Thread(() -> {
  33. for (int i = 0; i < 3; i++) {
  34. demo.incrementInstance();
  35. }
  36. }, "对象锁线程").start();
  37. // 线程2:访问静态方法(类锁)
  38. new Thread(() -> {
  39. for (int i = 0; i < 3; i++) {
  40. LockComparisonDemo.incrementStatic();
  41. }
  42. }, "类锁线程").start();
  43. // 关键点:对象锁和类锁是相互独立的,不会互相阻塞
  44. }
  45. }

运行这段代码,你会发现”对象锁线程”和”类锁线程”是并发执行的,这说明对象锁和类锁互不干扰。这是因为它们锁定的是不同的对象:对象锁锁定的是实例对象,而类锁锁定的是Class对象。

2. synchronized修饰方法与代码块的区别

synchronized关键字可以修饰方法或代码块,它们有一些重要的区别。

2.1 修饰方法 vs 修饰代码块

  1. 锁的粒度不同
    • 修饰方法:锁的是整个方法体
    • 修饰代码块:只锁定特定的代码块
  2. 性能影响
    • 修饰方法:可能导致锁的持有时间过长
    • 修饰代码块:可以精确控制锁的范围,提高并发性能
  3. 灵活性
    • 修饰方法:简单直观,但不够灵活
    • 修饰代码块:可以指定不同的锁对象,更加灵活

下面是一个对比示例:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.TimeUnit;
  3. public class SynchronizedComparison {
  4. private int data = 0;
  5. // 修饰整个方法
  6. public synchronized void methodSync() {
  7. System.out.println(Thread.currentThread().getName() + " 开始执行方法同步...");
  8. // 执行一些非同步操作(假设是耗时的IO操作)
  9. try {
  10. System.out.println(Thread.currentThread().getName() + " 执行耗时操作...");
  11. TimeUnit.SECONDS.sleep(2);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. // 真正需要同步的只是这部分数据操作
  16. data++;
  17. System.out.println(Thread.currentThread().getName() + " 数据更新为:" + data);
  18. // 再次执行一些非同步操作
  19. try {
  20. System.out.println(Thread.currentThread().getName() + " 执行额外操作...");
  21. TimeUnit.SECONDS.sleep(1);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. System.out.println(Thread.currentThread().getName() + " 方法执行完毕");
  26. }
  27. // 只同步关键代码块
  28. public void blockSync() {
  29. System.out.println(Thread.currentThread().getName() + " 开始执行方法...");
  30. // 执行一些非同步操作(假设是耗时的IO操作)
  31. try {
  32. System.out.println(Thread.currentThread().getName() + " 执行耗时操作...");
  33. TimeUnit.SECONDS.sleep(2);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. // 只同步关键代码块
  38. synchronized(this) {
  39. data++;
  40. System.out.println(Thread.currentThread().getName() + " 数据更新为:" + data);
  41. }
  42. // 再次执行一些非同步操作
  43. try {
  44. System.out.println(Thread.currentThread().getName() + " 执行额外操作...");
  45. TimeUnit.SECONDS.sleep(1);
  46. } catch (InterruptedException e) {
  47. e.printStackTrace();
  48. }
  49. System.out.println(Thread.currentThread().getName() + " 方法执行完毕");
  50. }
  51. public static void main(String[] args) {
  52. SynchronizedComparison demo = new SynchronizedComparison();
  53. System.out.println("====== 测试方法同步 ======");
  54. Thread t1 = new Thread(() -> demo.methodSync(), "线程1");
  55. Thread t2 = new Thread(() -> demo.methodSync(), "线程2");
  56. t1.start();
  57. t2.start();
  58. // 等待两个线程执行完毕
  59. try {
  60. t1.join();
  61. t2.join();
  62. } catch (InterruptedException e) {
  63. e.printStackTrace();
  64. }
  65. System.out.println("\n====== 测试代码块同步 ======");
  66. demo.data = 0; // 重置数据
  67. Thread t3 = new Thread(() -> demo.blockSync(), "线程3");
  68. Thread t4 = new Thread(() -> demo.blockSync(), "线程4");
  69. t3.start();
  70. t4.start();
  71. }
  72. }

运行上述代码,你会发现:

  • 在方法同步中,第二个线程必须等第一个线程完全执行完整个方法才能开始执行。
  • 在代码块同步中,两个线程可以并发执行方法中的非同步部分,只有在执行同步代码块时才需要等待。

这说明了代码块同步相比方法同步可以提高并发性能,尤其是在方法中包含大量非同步操作的情况下。

2.2 不同锁对象的选择

在使用synchronized代码块时,可以选择不同的锁对象。锁对象的选择会影响并发行为:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.TimeUnit;
  3. public class LockObjectDemo {
  4. // 定义不同的锁对象
  5. private final Object lockA = new Object();
  6. private final Object lockB = new Object();
  7. private int counterA = 0;
  8. private int counterB = 0;
  9. // 使用lockA作为锁对象
  10. public void incrementA() {
  11. synchronized (lockA) {
  12. counterA++;
  13. System.out.println(Thread.currentThread().getName() + " - 计数器A:" + counterA);
  14. try {
  15. TimeUnit.SECONDS.sleep(1);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  21. // 使用lockB作为锁对象
  22. public void incrementB() {
  23. synchronized (lockB) {
  24. counterB++;
  25. System.out.println(Thread.currentThread().getName() + " - 计数器B:" + counterB);
  26. try {
  27. TimeUnit.SECONDS.sleep(1);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. public static void main(String[] args) {
  34. LockObjectDemo demo = new LockObjectDemo();
  35. // 线程1:访问A资源
  36. new Thread(() -> {
  37. for (int i = 0; i < 3; i++) {
  38. demo.incrementA();
  39. }
  40. }, "线程A").start();
  41. // 线程2:访问B资源
  42. new Thread(() -> {
  43. for (int i = 0; i < 3; i++) {
  44. demo.incrementB();
  45. }
  46. }, "线程B").start();
  47. // 线程3:也访问A资源
  48. new Thread(() -> {
  49. for (int i = 0; i < 3; i++) {
  50. demo.incrementA();
  51. }
  52. }, "线程C").start();
  53. }
  54. }

运行上述代码,你会观察到:

  • “线程A”和”线程C”会互相阻塞,因为它们使用了相同的锁对象lockA
  • “线程B”独立执行,不受其他线程影响,因为它使用了不同的锁对象lockB

这种方式称为”细粒度锁”,可以提高程序的并发性能。但是要注意,使用不同的锁对象也会增加程序的复杂性,可能导致死锁问题。

最佳实践: 选择锁对象时,应该遵循以下原则:

  1. 使用final修饰锁对象,防止锁对象被修改
  2. 不要使用String常量或基本类型的包装类作为锁对象
  3. 尽量避免使用公共对象(如全局变量)作为锁对象

3. synchronized的底层实现原理

要深入理解synchronized,我们需要了解它的底层实现原理。

3.1 Monitor监视器

synchronized的底层实现是基于Monitor(监视器)机制。在Java虚拟机中,每个对象都有一个与之关联的Monitor。当线程执行到synchronized代码块时,会尝试获取Monitor的所有权:

如果Monitor没有被占用,线程获取Monitor的所有权并继续执行 如果Monitor已被其他线程占用,当前线程会被阻塞,进入Monitor的等待队列

以下是这个过程的简化图示:

  1. 线程A 对象的Monitor 线程B
  2. | | |
  3. | 尝试获取Monitor所有权 | |
  4. | -------------------------> | |
  5. | | |
  6. | 获取成功,执行同步代码块 | |
  7. | | |
  8. | | 尝试获取Monitor所有权
  9. | | <-----------------
  10. | | |
  11. | | 被阻塞,进入等待队列
  12. | | |
  13. | 释放Monitor所有权 | |
  14. | -------------------------> | |
  15. | | |
  16. | | 获取Monitor所有权
  17. | | ----------------> |
  18. | | |
  19. | | 执行同步代码块 |

3.2 字节码层面的实现

在Java字节码层面,synchronized的实现依赖于monitorenter和monitorexit指令:

  • monitorenter:进入同步代码块时,尝试获取Monitor
  • monitorexit:退出同步代码块时,释放Monitor

我们可以通过javap命令反编译字节码来查看这些指令。以下是一个简单示例:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. public class SynchronizedBytecode {
  3. public void syncBlock() {
  4. synchronized (this) {
  5. System.out.println("同步代码块");
  6. }
  7. }
  8. public synchronized void syncMethod() {
  9. System.out.println("同步方法");
  10. }
  11. }

使用javap命令查看字节码(在编译后的目录中执行):

  1. javap -c org.devlive.tutorial.multithreading.chapter05.SynchronizedBytecode

你会看到类似下面的输出(简化版):

  1. public void syncBlock();
  2. Code:
  3. 0: aload_0
  4. 1: dup
  5. 2: astore_1
  6. 3: monitorenter // 进入同步块
  7. 4: getstatic #2 // System.out
  8. ...
  9. 12: aload_1
  10. 13: monitorexit // 退出同步块
  11. ...
  12. public synchronized void syncMethod();
  13. Code:
  14. 0: getstatic #2 // System.out
  15. ...
  16. // 注意没有明显的monitorenter和monitorexit指令

从字节码可以看出:

  • 同步代码块是通过monitorenter和monitorexit指令实现的
  • 同步方法是通过方法修饰符ACC_SYNCHRONIZED标记实现的,JVM会根据这个标记自动加锁和释放锁

3.3 锁的优化

随着Java版本的演进,synchronized的性能得到了极大的优化。JDK 1.6引入了锁的升级机制,将锁分为以下几种状态:

  1. 偏向锁:偏向于第一个获取锁的线程,如果该线程后续继续获取该锁,不需要进行同步操作
  2. 轻量级锁:使用CAS(Compare and Swap)操作尝试获取锁,避免线程阻塞
  3. 重量级锁:传统的锁机制,会导致线程阻塞和上下文切换

锁的状态会随着竞争的激烈程度自动升级,但不会降级。这种机制大大提高了synchronized的性能。

提示: JDK 1.6之前,synchronized被称为”重量级锁”,性能较差。但经过优化后,现代JVM中的synchronized性能已经非常好,在大多数场景下可以放心使用。

4. 实战案例:使用synchronized解决银行账户并发问题

现在,让我们通过一个实际案例来应用synchronized关键字解决并发问题。

4.1 问题描述:银行账户转账

假设有一个银行系统,需要处理账户之间的转账操作。在多线程环境下,如果不加锁控制,可能会出现以下问题:

  • 账户余额计算错误
  • 转账金额丢失
  • 账户余额为负数

4.2 不安全的实现

首先,我们看一个不使用同步机制的实现:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.CountDownLatch;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. public class BankAccountUnsafe {
  6. // 账户类
  7. static class Account {
  8. private int id;
  9. private double balance;
  10. public Account(int id, double initialBalance) {
  11. this.id = id;
  12. this.balance = initialBalance;
  13. }
  14. public void deposit(double amount) {
  15. double newBalance = balance + amount;
  16. // 模拟一些耗时操作,使问题更容易出现
  17. try {
  18. Thread.sleep(10);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. balance = newBalance;
  23. }
  24. public void withdraw(double amount) {
  25. if (balance >= amount) {
  26. double newBalance = balance - amount;
  27. // 模拟一些耗时操作
  28. try {
  29. Thread.sleep(10);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. balance = newBalance;
  34. } else {
  35. System.out.println("账户" + id + "余额不足,无法取款");
  36. }
  37. }
  38. public double getBalance() {
  39. return balance;
  40. }
  41. public int getId() {
  42. return id;
  43. }
  44. }
  45. // 转账操作
  46. static class TransferTask implements Runnable {
  47. private Account fromAccount;
  48. private Account toAccount;
  49. private double amount;
  50. public TransferTask(Account fromAccount, Account toAccount, double amount) {
  51. this.fromAccount = fromAccount;
  52. this.toAccount = toAccount;
  53. this.amount = amount;
  54. }
  55. @Override
  56. public void run() {
  57. System.out.println(Thread.currentThread().getName() + " 准备从账户" + fromAccount.getId() +
  58. "转账" + amount + "元到账户" + toAccount.getId());
  59. // 取款
  60. fromAccount.withdraw(amount);
  61. // 存款
  62. toAccount.deposit(amount);
  63. System.out.println(Thread.currentThread().getName() + " 完成转账!");
  64. }
  65. }
  66. public static void main(String[] args) throws InterruptedException {
  67. // 创建两个账户
  68. Account accountA = new Account(1, 1000);
  69. Account accountB = new Account(2, 1000);
  70. System.out.println("初始状态:");
  71. System.out.println("账户1余额: " + accountA.getBalance());
  72. System.out.println("账户2余额: " + accountB.getBalance());
  73. // 使用线程池创建10个线程
  74. ExecutorService executor = Executors.newFixedThreadPool(10);
  75. // 使用CountDownLatch等待所有线程完成
  76. CountDownLatch latch = new CountDownLatch(100);
  77. // 提交100个转账任务
  78. for (int i = 0; i < 50; i++) {
  79. // A账户向B账户转账
  80. executor.submit(() -> {
  81. new TransferTask(accountA, accountB, 10).run();
  82. latch.countDown();
  83. });
  84. // B账户向A账户转账
  85. executor.submit(() -> {
  86. new TransferTask(accountB, accountA, 10).run();
  87. latch.countDown();
  88. });
  89. }
  90. // 等待所有任务完成
  91. latch.await();
  92. executor.shutdown();
  93. System.out.println("\n最终状态:");
  94. System.out.println("账户1余额: " + accountA.getBalance());
  95. System.out.println("账户2余额: " + accountB.getBalance());
  96. System.out.println("总金额: " + (accountA.getBalance() + accountB.getBalance()));
  97. }
  98. }

运行上述代码,你会发现总金额可能不等于初始总金额(2000元)。这是因为多个线程同时修改账户余额导致的数据不一致问题。

4.3 使用synchronized解决问题

现在,让我们使用synchronized来解决这个问题:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.CountDownLatch;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. public class BankAccountSafe
  6. {
  7. // 安全的账户类
  8. static class Account
  9. {
  10. private int id;
  11. private double balance;
  12. public Account(int id, double initialBalance)
  13. {
  14. this.id = id;
  15. this.balance = initialBalance;
  16. }
  17. // 使用synchronized修饰存款方法
  18. public synchronized void deposit(double amount)
  19. {
  20. double newBalance = balance + amount;
  21. // 模拟一些耗时操作
  22. try {
  23. Thread.sleep(10);
  24. }
  25. catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. balance = newBalance;
  29. }
  30. // 使用synchronized修饰取款方法
  31. public synchronized void withdraw(double amount)
  32. {
  33. if (balance >= amount) {
  34. double newBalance = balance - amount;
  35. // 模拟一些耗时操作
  36. try {
  37. Thread.sleep(10);
  38. }
  39. catch (InterruptedException e) {
  40. e.printStackTrace();
  41. }
  42. balance = newBalance;
  43. }
  44. else {
  45. System.out.println("账户" + id + "余额不足,无法取款");
  46. }
  47. }
  48. public synchronized double getBalance()
  49. {
  50. return balance;
  51. }
  52. public int getId()
  53. {
  54. return id;
  55. }
  56. }
  57. // 安全的转账操作
  58. static class TransferTask
  59. implements Runnable
  60. {
  61. private Account fromAccount;
  62. private Account toAccount;
  63. private double amount;
  64. public TransferTask(Account fromAccount, Account toAccount, double amount)
  65. {
  66. this.fromAccount = fromAccount;
  67. this.toAccount = toAccount;
  68. this.amount = amount;
  69. }
  70. @Override
  71. public void run()
  72. {
  73. System.out.println(Thread.currentThread().getName() + " 准备从账户" + fromAccount.getId() +
  74. "转账" + amount + "元到账户" + toAccount.getId());
  75. // 使用synchronized代码块,确保同时锁定两个账户
  76. // 注意:这里需要按固定顺序获取锁,避免死锁
  77. synchronized (fromAccount.getId() < toAccount.getId() ? fromAccount : toAccount) {
  78. synchronized (fromAccount.getId() < toAccount.getId() ? toAccount : fromAccount) {
  79. // 取款
  80. fromAccount.withdraw(amount);
  81. // 存款
  82. toAccount.deposit(amount);
  83. }
  84. }
  85. System.out.println(Thread.currentThread().getName() + " 完成转账!");
  86. }
  87. }
  88. public static void main(String[] args)
  89. throws InterruptedException
  90. {
  91. // 创建两个账户
  92. Account accountA = new Account(1, 1000);
  93. Account accountB = new Account(2, 1000);
  94. System.out.println("初始状态:");
  95. System.out.println("账户1余额: " + accountA.getBalance());
  96. System.out.println("账户2余额: " + accountB.getBalance());
  97. // 使用线程池创建10个线程
  98. ExecutorService executor = Executors.newFixedThreadPool(10);
  99. // 使用CountDownLatch等待所有线程完成
  100. CountDownLatch latch = new CountDownLatch(50);
  101. // 提交100个转账任务
  102. for (int i = 0; i < 50; i++) {
  103. // A账户向B账户转账
  104. executor.submit(() -> {
  105. new TransferTask(accountA, accountB, 10).run();
  106. latch.countDown();
  107. });
  108. }
  109. // 等待所有任务完成
  110. latch.await();
  111. executor.shutdown();
  112. System.out.println("\n最终状态:");
  113. System.out.println("账户1余额: " + accountA.getBalance());
  114. System.out.println("账户2余额: " + accountB.getBalance());
  115. System.out.println("总金额: " + (accountA.getBalance() + accountB.getBalance()));
  116. }
  117. }

运行上述代码,你会发现总金额始终等于初始总金额(2000元)。这是因为我们使用了synchronized确保了账户操作的线程安全。

在这个实现中,我们做了以下改进:

  1. 使用synchronized修饰deposit、withdraw和getBalance方法,确保对单个账户的操作是线程安全的。
  2. 在转账操作中,使用嵌套的synchronized代码块同时锁定两个账户,确保转账过程的原子性。
  3. 使用固定顺序获取锁(按账户ID排序),避免死锁问题。

注意: 在上述实现中,我们使用了一个重要的技巧来避免死锁:按照固定顺序获取锁。如果不这样做,可能会出现线程A持有账户1的锁并等待账户2的锁,而线程B持有账户2的锁并等待账户1的锁,从而形成死锁。

4.4 进一步优化:减小锁粒度

虽然上面的实现解决了线程安全问题,但锁的粒度还可以进一步优化。我们可以只在需要修改余额的关键代码处加锁,而不是整个方法:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. import java.util.concurrent.CountDownLatch;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. public class BankAccountOptimized {
  6. // 优化锁粒度的账户类
  7. static class Account {
  8. private int id;
  9. private double balance;
  10. // 使用final对象作为锁
  11. private final Object balanceLock = new Object();
  12. public Account(int id, double initialBalance) {
  13. this.id = id;
  14. this.balance = initialBalance;
  15. }
  16. public void deposit(double amount) {
  17. // 只在修改余额时加锁
  18. synchronized (balanceLock) {
  19. double newBalance = balance + amount;
  20. balance = newBalance;
  21. }
  22. // 模拟其他非关键操作(如日志记录等)
  23. try {
  24. Thread.sleep(10);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. public boolean withdraw(double amount) {
  30. boolean success = false;
  31. // 只在检查和修改余额时加锁
  32. synchronized (balanceLock) {
  33. if (balance >= amount) {
  34. double newBalance = balance - amount;
  35. balance = newBalance;
  36. success = true;
  37. }
  38. }
  39. // 模拟其他非关键操作
  40. try {
  41. Thread.sleep(10);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. if (!success) {
  46. System.out.println("账户" + id + "余额不足,无法取款");
  47. }
  48. return success;
  49. }
  50. public double getBalance() {
  51. synchronized (balanceLock) {
  52. return balance;
  53. }
  54. }
  55. public int getId() {
  56. return id;
  57. }
  58. }
  59. // 转账操作
  60. static class TransferTask implements Runnable {
  61. private Account fromAccount;
  62. private Account toAccount;
  63. private double amount;
  64. public TransferTask(Account fromAccount, Account toAccount, double amount) {
  65. this.fromAccount = fromAccount;
  66. this.toAccount = toAccount;
  67. this.amount = amount;
  68. }
  69. @Override
  70. public void run() {
  71. System.out.println(Thread.currentThread().getName() + " 准备从账户" + fromAccount.getId() +
  72. "转账" + amount + "元到账户" + toAccount.getId());
  73. // 使用两阶段锁定协议:先锁定源账户,再锁定目标账户
  74. // 注意:为了避免死锁,我们按照账户ID的顺序获取锁
  75. Account firstLock = fromAccount.getId() < toAccount.getId() ? fromAccount : toAccount;
  76. Account secondLock = fromAccount.getId() < toAccount.getId() ? toAccount : fromAccount;
  77. synchronized (firstLock) {
  78. synchronized (secondLock) {
  79. // 先检查余额是否足够
  80. if (fromAccount.withdraw(amount)) {
  81. // 取款成功后存入另一个账户
  82. toAccount.deposit(amount);
  83. System.out.println(Thread.currentThread().getName() + " 完成转账!");
  84. } else {
  85. System.out.println(Thread.currentThread().getName() + " 转账失败!");
  86. }
  87. }
  88. }
  89. }
  90. }
  91. public static void main(String[] args) throws InterruptedException {
  92. // 创建两个账户
  93. Account accountA = new Account(1, 1000);
  94. Account accountB = new Account(2, 1000);
  95. System.out.println("初始状态:");
  96. System.out.println("账户1余额: " + accountA.getBalance());
  97. System.out.println("账户2余额: " + accountB.getBalance());
  98. // 使用线程池创建10个线程
  99. ExecutorService executor = Executors.newFixedThreadPool(10);
  100. // 使用CountDownLatch等待所有线程完成
  101. CountDownLatch latch = new CountDownLatch(50);
  102. // 提交100个转账任务
  103. for (int i = 0; i < 50; i++) {
  104. // A账户向B账户转账
  105. executor.submit(() -> {
  106. new TransferTask(accountA, accountB, 10).run();
  107. latch.countDown();
  108. });
  109. // B账户向A账户转账
  110. executor.submit(() -> {
  111. new TransferTask(accountB, accountA, 10).run();
  112. latch.countDown();
  113. });
  114. }
  115. // 等待所有任务完成
  116. latch.await();
  117. executor.shutdown();
  118. System.out.println("\n最终状态:");
  119. System.out.println("账户1余额: " + accountA.getBalance());
  120. System.out.println("账户2余额: " + accountB.getBalance());
  121. System.out.println("总金额: " + (accountA.getBalance() + accountB.getBalance()));
  122. }
  123. }

在这个优化版本中,我们做了以下改进:

  1. 使用专门的锁对象(balanceLock)而不是this对象
  2. 只在真正需要同步的代码块(余额操作)上加锁,提高并发性能
  3. 将withdraw方法的返回值改为boolean,便于调用者知道取款是否成功
  4. 使用两阶段锁定协议,按固定顺序获取锁,避免死锁

这种方式既保证了线程安全,又提高了并发性能。

5. 常见问题与解决方案

5.1 死锁问题

死锁是指两个或更多线程永远阻塞,等待对方持有的锁。

死锁示例:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. public class DeadlockDemo {
  3. // 资源A
  4. private static final Object resourceA = new Object();
  5. // 资源B
  6. private static final Object resourceB = new Object();
  7. public static void main(String[] args) {
  8. // 线程1:先获取资源A,再获取资源B
  9. Thread thread1 = new Thread(() -> {
  10. synchronized (resourceA) {
  11. System.out.println(Thread.currentThread().getName() + " 获取了资源A");
  12. try {
  13. Thread.sleep(1000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(Thread.currentThread().getName() + " 等待获取资源B");
  18. synchronized (resourceB) {
  19. System.out.println(Thread.currentThread().getName() + " 获取了资源B");
  20. }
  21. }
  22. }, "线程1");
  23. // 线程2:先获取资源B,再获取资源A
  24. Thread thread2 = new Thread(() -> {
  25. synchronized (resourceB) {
  26. System.out.println(Thread.currentThread().getName() + " 获取了资源B");
  27. try {
  28. Thread.sleep(1000);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. System.out.println(Thread.currentThread().getName() + " 等待获取资源A");
  33. synchronized (resourceA) {
  34. System.out.println(Thread.currentThread().getName() + " 获取了资源A");
  35. }
  36. }
  37. }, "线程2");
  38. thread1.start();
  39. thread2.start();
  40. }
  41. }

运行上述代码,你会发现程序会永远卡住,这就是死锁。线程1持有资源A的锁并等待资源B的锁,而线程2持有资源B的锁并等待资源A的锁。

死锁解决方案:

  1. 按固定顺序获取锁:确保所有线程按照相同的顺序获取锁,可以避免循环等待。

    1. package org.devlive.tutorial.multithreading.chapter05;
    2. // 修复死锁的代码
    3. public class DeadlockFixed {
    4. // 资源A
    5. private static final Object resourceA = new Object();
    6. // 资源B
    7. private static final Object resourceB = new Object();
    8. public static void main(String[] args) {
    9. // 线程1:先获取资源A,再获取资源B
    10. Thread thread1 = new Thread(() -> {
    11. synchronized (resourceA) {
    12. System.out.println(Thread.currentThread().getName() + " 获取了资源A");
    13. try {
    14. Thread.sleep(1000);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. System.out.println(Thread.currentThread().getName() + " 等待获取资源B");
    19. synchronized (resourceB) {
    20. System.out.println(Thread.currentThread().getName() + " 获取了资源B");
    21. }
    22. }
    23. }, "线程1");
    24. // 线程2:也是先获取资源A,再获取资源B,遵循相同的顺序
    25. Thread thread2 = new Thread(() -> {
    26. synchronized (resourceA) {
    27. System.out.println(Thread.currentThread().getName() + " 获取了资源A");
    28. try {
    29. Thread.sleep(1000);
    30. } catch (InterruptedException e) {
    31. e.printStackTrace();
    32. }
    33. System.out.println(Thread.currentThread().getName() + " 等待获取资源B");
    34. synchronized (resourceB) {
    35. System.out.println(Thread.currentThread().getName() + " 获取了资源B");
    36. }
    37. }
    38. }, "线程2");
    39. thread1.start();
    40. thread2.start();
    41. }
    42. }
  2. 使用锁超时:设定获取锁的超时时间,超时后放弃当前锁并重试。(不过synchronized不支持超时,需要使用Lock接口及其实现类)
  3. 死锁检测:使用工具(如jstack)检测死锁并强制解决。

5.2 性能问题

synchronized在保证线程安全的同时可能会带来性能问题。

常见性能问题:

  1. 锁粒度过大:整个方法加锁可能导致大部分时间都在等待锁
  2. 锁竞争激烈:多个线程频繁争抢同一把锁
  3. 锁持有时间过长:同步代码块中包含耗时操作

解决方案:

  1. 减小锁粒度:只在必要的代码块上加锁
  2. 使用不同的锁对象:将不相关的操作分到不同的锁上
  3. 避免在同步代码块中进行耗时操作:如IO操作、复杂计算等
  4. 考虑使用CAS操作:对于简单的原子操作,考虑使用java.util.concurrent.atomic包中的类

    1. // 减小锁粒度的示例
    2. public class SmallerLockGranularity {
    3. private final Object stateLock = new Object();
    4. private final Object listLock = new Object();
    5. private int state;
    6. private List<String> list = new ArrayList<>();
    7. // 使用不同的锁对象保护不同的资源
    8. public void updateState() {
    9. synchronized (stateLock) {
    10. state++;
    11. }
    12. }
    13. public void updateList(String item) {
    14. synchronized (listLock) {
    15. list.add(item);
    16. }
    17. }
    18. }

5.3 活锁问题

活锁是指线程不断重试一个总是失败的操作,导致无法继续执行,但线程并没有阻塞。

活锁示例:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. public class LivelockDemo {
  3. static class Worker {
  4. private String name;
  5. private boolean active;
  6. public Worker(String name, boolean active) {
  7. this.name = name;
  8. this.active = active;
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. public boolean isActive() {
  14. return active;
  15. }
  16. public void work(Worker otherWorker, Object sharedResource) {
  17. while (active) {
  18. // 等待对方不活跃
  19. if (otherWorker.isActive()) {
  20. System.out.println(getName() + ": " + otherWorker.getName() + "正在工作,我稍后再试");
  21. try {
  22. Thread.sleep(500);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. continue;
  27. }
  28. synchronized (sharedResource) {
  29. System.out.println(getName() + ": 我开始工作了");
  30. active = false;
  31. // 通知对方可以工作了
  32. otherWorker.active = true;
  33. System.out.println(getName() + ": 我通知" + otherWorker.getName() + "可以工作了");
  34. }
  35. }
  36. }
  37. }
  38. public static void main(String[] args) {
  39. final Object sharedResource = new Object();
  40. final Worker worker1 = new Worker("工人1", true);
  41. final Worker worker2 = new Worker("工人2", true);
  42. new Thread(() -> {
  43. worker1.work(worker2, sharedResource);
  44. }).start();
  45. new Thread(() -> {
  46. worker2.work(worker1, sharedResource);
  47. }).start();
  48. }
  49. }

运行上述代码,你会发现两个工人一直互相谦让,都无法开始工作。这就是活锁。

活锁解决方案:

  1. 引入随机等待时间:打破重试的同步性
  2. 优先级调整:为竞争者分配不同的优先级
  1. // 通过随机等待时间解决活锁
  2. public void work(Worker otherWorker, Object sharedResource) {
  3. while (active) {
  4. // 等待对方不活跃
  5. if (otherWorker.isActive()) {
  6. System.out.println(getName() + ": " + otherWorker.getName() + "正在工作,我稍后再试");
  7. try {
  8. // 引入随机等待时间,打破同步性
  9. Thread.sleep((long)(Math.random() * 1000));
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. continue;
  14. }
  15. // 其余代码保持不变
  16. }
  17. }

5.4 嵌套锁问题

嵌套锁是指在已持有锁的情况下再次获取锁。Java的synchronized是可重入锁,允许嵌套锁,但仍需谨慎使用。

可重入性示例:

  1. package org.devlive.tutorial.multithreading.chapter05;
  2. public class ReentrantLockDemo {
  3. public synchronized void outer() {
  4. System.out.println("进入outer方法");
  5. inner(); // 在持有锁的情况下调用另一个同步方法
  6. System.out.println("退出outer方法");
  7. }
  8. public synchronized void inner() {
  9. System.out.println("进入inner方法");
  10. // 这里可以再次获取同一个锁(this对象的锁)
  11. System.out.println("退出inner方法");
  12. }
  13. public static void main(String[] args) {
  14. ReentrantLockDemo demo = new ReentrantLockDemo();
  15. demo.outer();
  16. }
  17. }

运行上述代码,你会发现程序能够正常执行,而不会死锁。这是因为synchronized是可重入锁,允许同一个线程多次获取同一把锁。

嵌套锁的问题与解决方案:

  1. 死锁风险:如果存在交叉调用,可能导致死锁

    • 解决方案:避免复杂的锁嵌套关系,或使用统一的锁获取顺序
  2. 性能开销:每次获取锁都有开销

    • 解决方案:减少不必要的锁嵌套
  3. 代码可读性下降:嵌套锁使代码更难理解和维护

    • 解决方案:重构代码,降低复杂性

6. 小结

在本章中,我们深入学习了synchronized关键字的使用和原理:

  1. 理解了对象锁与类锁的概念和区别:

    • 对象锁锁定特定实例,通过synchronized修饰实例方法或synchronized(this)实现
    • 类锁锁定整个类,通过synchronized修饰静态方法或synchronized(类名.class)实现
  2. 掌握了synchronized修饰方法与代码块的区别:

    • 修饰方法:锁的范围是整个方法体,简单但粒度大
    • 修饰代码块:可以精确控制锁的范围,性能更好
  3. 了解了synchronized的底层实现原理:

    • 基于Monitor监视器机制
    • 通过monitorenter和monitorexit指令实现
    • JDK 1.6后引入锁升级机制,大幅提升性能
  4. 通过银行账户转账案例掌握了如何使用synchronized解决实际并发问题

  5. 学习了使用synchronized的常见问题与解决方案:

    • 死锁问题:按固定顺序获取锁
    • 性能问题:减小锁粒度,使用不同的锁对象
    • 活锁问题:引入随机等待时间
    • 嵌套锁问题:注意可重入性,避免复杂嵌套

通过本章的学习,你应该能够理解并正确使用synchronized关键字来解决多线程环境下的同步问题。在下一章中,我们将学习volatile关键字,它是另一种解决并发问题的重要工具。

源代码地址:https://github.com/qianmoQ/tutorial/tree/main/java-multithreading-tutorial/src/main/java/org/devlive/tutorial/multithreading/chapter05