Java Concurrency - synchronized 关键字
当有多个线程竞争共享资源时,对资源的访问顺序敏感,则可能造成数据不一致。为了保证共享资源不被多个线程同时访问,则需要将竞争共享资源的代码置于临界区,临界区保证在同一时间内最多只能有一个线程执行该代码段。
先看一段由竞争共享资源造成数据不一致的代码:
1 public class BankAccount { 2 private int balance; 3 4 public BankAccount(int balance) { 5 this.balance = balance; 6 } 7 8 public int getBalance() { 9 return balance; 10 } 11 12 public void withdraw(int amount) { 13 if (balance >= amount) { 14 try { 15 System.out.println(Thread.currentThread().getName() + " is going to sleep."); 16 Thread.sleep(50); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 System.out.println(Thread.currentThread().getName() + " woke up."); 21 22 balance -= amount; 23 System.out.println(String.format("%s drew %d, the balance is %d now.", Thread.currentThread().getName(), amount, balance)); 24 } else { 25 System.out.println("Sorry, not enough for " + Thread.currentThread().getName()); 26 } 27 } 28 } 29 30 31 public class DrawMoneyTask implements Runnable { 32 33 private BankAccount bankAccount; 34 35 public DrawMoneyTask(BankAccount bankAccount) { 36 this.bankAccount = bankAccount; 37 } 38 39 @Override 40 public void run() { 41 for (int i = 0; i < 3; i++) { 42 bankAccount.withdraw(10); 43 if (bankAccount.getBalance() < 0) { 44 System.err.println("Overdrawn!"); 45 } 46 } 47 } 48 49 } 50 51 52 public class Main { 53 public static void main(String[] args) { 54 BankAccount bankAccount = new BankAccount(50); 55 Runnable drawMoneyTask = new DrawMoneyTask(bankAccount); 56 57 Thread huey = new Thread(drawMoneyTask, "Huey"); 58 Thread jane = new Thread(drawMoneyTask, "Jane"); 59 60 huey.start(); 61 jane.start(); 62 } 63 }
运行结果:
Jane is going to sleep. Huey is going to sleep. Huey woke up. Jane woke up. Jane drew 10, the balance is 30 now. Huey drew 10, the balance is 30 now. Jane is going to sleep. Huey is going to sleep. Huey woke up. Jane woke up. Jane drew 10, the balance is 10 now. Jane is going to sleep. Huey drew 10, the balance is 20 now. Huey is going to sleep. Huey woke up. Jane woke up. Huey drew 10, the balance is 0 now. Jane drew 10, the balance is -10 now. Overdrawn! Overdrawn!
Huey 和 Jane 两个线程同时向账户取钱。虽然取款前先判断余额是否足够,但是仍出现超额提取的情况。这是因为两个线程同时访问修改账户信息造成数据不一致。当余额剩下 10 时,Huey 准备取款时,发现余额足够,然后进入第 14 行代码,线程休眠。这时 Jane 也来取款,同样发现余额还有 10,也进入第 14 行代码,然后休眠。Huey 醒来取了 10,余额便只剩下 0。Jane 醒来也取了 10,这时余额剩下 -10,发生超额。
为了避免超额,我们限制 Huey 和 Jane 两个线程不能同时执行取款操作。若当前有线程在执行取款操作,则其他线程挂起等待直到取款操作结束。我们可以使用 synchronized 关键字来实现这一功能。
public synchronized void withdraw(int amount) { if (balance >= amount) { try { System.out.println(Thread.currentThread().getName() + " is going to sleep."); Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " woke up."); balance -= amount; System.out.println(String.format("%s drew %d, the balance is %d now.", Thread.currentThread().getName(), amount, balance)); } else { System.out.println("Sorry, not enough for " + Thread.currentThread().getName()); } }
使用 synchronized 关键字修饰 withdraw 方法表示,整段 withdraw 方法代码为临界区,即同一时间内最多只能有一个线程执行 withdraw 方法,其他线程需等待直到当前线程执行完毕。使用 synchronized 修饰 withdraw 方法后,运行结果为:
Huey is going to sleep. Huey woke up. Huey drew 10, the balance is 40 now. Huey is going to sleep. Huey woke up. Huey drew 10, the balance is 30 now. Huey is going to sleep. Huey woke up. Huey drew 10, the balance is 20 now. Jane is going to sleep. Jane woke up. Jane drew 10, the balance is 10 now. Jane is going to sleep. Jane woke up. Jane drew 10, the balance is 0 now. Sorry, not enough for Jane
synchronized 关键字除了修饰方法(同步方法),还可以修饰对象(同步代码块)。使用 synchronized 修饰对象的引用,通常会修饰 this 关键字,也可以修饰其他对象的引用。
synchronized (this) { // ... }
synchronized 修饰 this 关键字的作用与修饰普通方法类似,差别在于:当修饰方法时,整个方法都是临界区;当修饰 this 时,只有 { } 内的代码段是临界区。使用 synchronized 关键字在一定程度上会影响性能,因为在同一时间内最多只能有一个线程执行临界区的代码,不能发挥并行的优点。因此,可以根据实际情况,使用 synchronized 修饰对象的引用而非方法,调整临界区的大小,只同步访问修改共享资源的代码,其他竞争共享资源而又耗时的代码段不进行同步。
修饰 this 与修饰其他对象的引用本质也是相同的,它们都是对象锁,只是锁的对象不同。修饰 this 关键字,锁定的是当前对象。当一个线程获得一个锁时,其他线程如果要访问该锁作用的临界区时,则挂起等待直至锁被释放。
public class SynchronizedClass { public void methodA() { synchronized (this) { System.out.println("In methodA. " + new Date()); try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } } } public void methodB() { synchronized (this) { System.out.println("In methodB. " + new Date()); try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class TaskA implements Runnable { private SynchronizedClass synchronizedObject; public TaskA(SynchronizedClass synchronizedObject) { this.synchronizedObject = synchronizedObject; } @Override public void run() { synchronizedObject.methodA(); } } public class TaskB implements Runnable { private SynchronizedClass synchronizedObject; public TaskB(SynchronizedClass synchronizedObject) { this.synchronizedObject = synchronizedObject; } @Override public void run() { synchronizedObject.methodB(); } } public class Main { public static void main(String[] args) { SynchronizedClass synchronizedObject = new SynchronizedClass(); Thread threadA = new Thread(new TaskA(synchronizedObject)); Thread threadB = new Thread(new TaskB(synchronizedObject)); threadA.start(); threadB.start(); } }
当有一个线程 A 正在执行 methodA 中 synchronized 修饰的代码段时,即线程 A 获得了 synchronizedObject 对象的锁。如果此时有线程 B 要执行 methodB 中 synchronized 修饰的代码段,同样需要获得 synchronizedObject 的锁才能进入 methodB 方法中的临界区,所以线程 B 需要等待线程 A 释放 synchronizedObject 对象锁后才能执行 methodB 方法中的临界区。观察运行结果:
In methodA. Sun Oct 09 14:59:43 CST 2016 In methodB. Sun Oct 09 14:59:46 CST 2016
需要注意的是,传入 TaskA 和 TaskB 的参数必须是同一个 SynchronizedClass 实例。如果传的是两个不同的 SynchronizedClass 实例,则线程 A 和线程 B 获得的是两个不同的锁,不会形成同步。
修改 Main 方法的内容:
public class Main { public static void main(String[] args) { SynchronizedClass synchronizedObjectA = new SynchronizedClass(); SynchronizedClass synchronizedObjectB = new SynchronizedClass(); Thread threadA = new Thread(new TaskA(synchronizedObjectA)); Thread threadB = new Thread(new TaskB(synchronizedObjectB)); threadA.start(); threadB.start(); } }
现在传给 TaskA 和 TaskB 是两个不同的 SynchronizedClass 实例,观察运行结果:
In methodA. Sun Oct 09 15:13:44 CST 2016 In methodB. Sun Oct 09 15:13:44 CST 2016
前面提到,synchronized 修饰 this 关键字的作用与修饰普通方法的差别只在于临界区的范围不一样。synchronized 修饰普通方法,锁定的也是当前对象。
public class SynchronizedClass { public void methodA() { synchronized (this) { // ... } } public synchronized void methodB() { // ... } }
上述代码,对于同一个 SynchronizedClass 实例。如果有线程 A 正在执行 methodA 方法中 synchronized 修饰的代码段。如果此时有其他线程要执行 methodB,则必须挂起等待直到线程 A 释放锁后才能进入 methodB。
synchronized 还可以修饰静态方法,这样则是持有类锁。假设一个类中有两个静态方法 methodA 和 methodB 被 synchronized 修饰,当有线程 A 正在执行 methodA 时,即线程 A 持有了类锁,此时如果有线程 B 要执行 methodB,那么线程 B 需要挂起等待直至线程 A 释放锁。
import java.util.Date; public class SynchronizedClass { public synchronized static void methodA() { System.out.println("In methodA. " + new Date()); try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized static void methodB() { System.out.println("In methodB. " + new Date()); try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { Thread threadA = new Thread(new Runnable() { public void run() { SynchronizedClass.methodA(); } }); Thread threadB = new Thread(new Runnable() { public void run() { SynchronizedClass.methodB(); } }); threadA.start(); threadB.start(); } }
观察结果:
In methodB. Sun Oct 09 15:44:18 CST 2016 In methodA. Sun Oct 09 15:44:21 CST 2016
对象锁和类锁是两种不同的锁。
public class SynchronizedClass { public synchronized static void methodA() { // ... } public synchronized void methodB() { // ... } }
当有线程 A 执行 methodA 方法时,不会影响其他线程执行 methodB 方法。
关键字 synchronized 拥有锁重入功能,即当一个线程持有一个锁时,在方法中再次请求该锁时,是可以再次得到该锁的。
public class SynchronizedClass { public synchronized void methodA() { System.out.println("In methodA. "); methodB(); } public synchronized void methodB() { System.out.println("In methodB. "); } public static void main(String[] args) { final SynchronizedClass synchronizedObject = new SynchronizedClass(); Thread thread = new Thread(new Runnable() { public void run() { synchronizedObject.methodA(); } }); thread.start(); } }