Java synchronized实现线程互斥(附带实例)
多线程的并发操作可以有效地提高程序的效率,但在使用多线程访问同一个资源时,需要特别关注线程安全的问题。
一个共享数据是可以由多个线程一起操作的,但是当一个共享数据在被一个线程操作的过程中,操作并未执行完毕,如果此时另一个线程也参与操作该共享数据,就会导致共享数据存在安全问题。
下面通过一个具体的实例来演示线程安全问题。模拟场景如下:一对夫妻使用相同的银行账户去银行取钱,一人拿着存折,另一人拿着银行卡。假如账户中有 2000 元,丈夫取 1500 元,妻子在同一时间也取 1500 元。如果取钱这段程序没有同步进行,也就是说,这两个操作可以同时进行,假设丈夫执行取钱操作时,程序刚刚判断了余额充足,还没有把钱从余额中扣除,妻子也执行取钱操作,此时妻子也可以取出 1500 元,即两个人从一个账户中取出 3000 元,这就涉及线程安全。
【实例 1 】线程安全问题。

图 1 取钱操作的过程和结果
线程互斥是指不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止发生访问冲突,在有限的时间内只允许其中之一独自使用共享资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
Java 中有两种实现线程互斥的方法:一是通过同步(Synchronized)代码块或同步方法实现,二是通过锁(Lock)实现。本节介绍第一种实现方式。
同步代码块的语法格式如下:
关键字 synchronized 使用的锁保存在 Java 的对象头中。JDK 提供了多种对象锁实现机制,这些机制的实现原理较为复杂,超出了本文的探讨范围,这里侧重于介绍如何基于关键字synchronized 实现线程同步。
在使用时,需要确认多线程访问的是同一个实例的同步方法才能实现同步效果。当使用关键字 synchronized 修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
同步方法的语法格式如下:
下面对实例 1 的代码进行重构。首先在 MyThread 类中添加一个新的 Object 类型的属性,命名为 lock。然后在 MyThread 类的构造器中添加一个新的参数,接收外部传入的 lock 对象并为 lock 属性赋值。接下来在 MyThread 类的 run() 方法中添加同步代码块,使用 lock 作为对象锁,限定同一时间仅能有一个线程执行取钱操作。最后修改 Account2 类的 withdraw() 方法,创建一个 byte[] 类型的对象 lockObject,并将其作为参数传入 MyThread 类的构造器中。这里需要注意的是,lockObject 可以是任意类,使用 byte[] 类型主要是因为底层创建对象的步骤更少。
【实例】同步代码块的应用。

图 2 使用线程加锁机制后的取钱操作的过程和结果
一个共享数据是可以由多个线程一起操作的,但是当一个共享数据在被一个线程操作的过程中,操作并未执行完毕,如果此时另一个线程也参与操作该共享数据,就会导致共享数据存在安全问题。
下面通过一个具体的实例来演示线程安全问题。模拟场景如下:一对夫妻使用相同的银行账户去银行取钱,一人拿着存折,另一人拿着银行卡。假如账户中有 2000 元,丈夫取 1500 元,妻子在同一时间也取 1500 元。如果取钱这段程序没有同步进行,也就是说,这两个操作可以同时进行,假设丈夫执行取钱操作时,程序刚刚判断了余额充足,还没有把钱从余额中扣除,妻子也执行取钱操作,此时妻子也可以取出 1500 元,即两个人从一个账户中取出 3000 元,这就涉及线程安全。
【实例 1 】线程安全问题。
public class Example { public static void main(String[] args) { Account account = new Account(); account.withdraw(); } } class Account { // 账户余额 public double balance = 2000; /** * 取钱测试方法 * 在该方法中,模拟丈夫和妻子同时取1500元 */ public void withdraw() { MyThread t1 = new MyThread("丈夫", 1500); MyThread t2 = new MyThread("妻子", 1500); t1.start(); t2.start(); } } /** * 封装取钱操作的自定义线程类 */ class MyThread extends Thread { private double money; private String name; public MyThread(String name, double money) { this.name = name; this.money = money; } public void run() { // MyThread是Account类的内部类,故可直接访问balance属性 if (balance > money) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } balance = balance - money; System.out.println(name + "取钱成功!"); } else { System.out.println(name + "取钱失败!"); } } }运行结果为:
妻子取钱成功!
丈夫取钱成功!

图 1 取钱操作的过程和结果
Java线程互斥
在 Java 中,可以通过线程互斥来解决线程安全问题。线程互斥是指不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止发生访问冲突,在有限的时间内只允许其中之一独自使用共享资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
Java 中有两种实现线程互斥的方法:一是通过同步(Synchronized)代码块或同步方法实现,二是通过锁(Lock)实现。本节介绍第一种实现方式。
同步代码块(同步监听器)
同步代码块是使用关键字 synchronized 修饰的语句块。使用关键字 synchronized 修饰的语句块会自动被加上内置锁,从而实现同步。同步代码块的语法格式如下:
synchronized(对象锁){ // 需要同步访问控制的代码 }利用关键字 synchronized 就可以为代码加上锁,在该关键字后面的圆括号中所放入的就是对象锁。对象锁可以是一个任意的类的对象,但是所有线程必须共用一个锁。
关键字 synchronized 使用的锁保存在 Java 的对象头中。JDK 提供了多种对象锁实现机制,这些机制的实现原理较为复杂,超出了本文的探讨范围,这里侧重于介绍如何基于关键字synchronized 实现线程同步。
同步方法
与同步代码块不同,同步方法将子线程要允许的代码放到一个方法中,在该方法的名称前面加上关键字 synchronized 即可,这里默认的锁为 this,即当前对象。在使用时,需要确认多线程访问的是同一个实例的同步方法才能实现同步效果。当使用关键字 synchronized 修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
同步方法的语法格式如下:
访问修饰符 synchronized 返回类型 方法名() { // 需要同步访问控制的代码 }或者:
synchronized 访问修饰符 返回类型 方法名() { // 方法体 }关键字 synchronized 也可以修饰静态方法,此时如果调用该静态方法,就会锁住当前类的 Class 对象。
下面对实例 1 的代码进行重构。首先在 MyThread 类中添加一个新的 Object 类型的属性,命名为 lock。然后在 MyThread 类的构造器中添加一个新的参数,接收外部传入的 lock 对象并为 lock 属性赋值。接下来在 MyThread 类的 run() 方法中添加同步代码块,使用 lock 作为对象锁,限定同一时间仅能有一个线程执行取钱操作。最后修改 Account2 类的 withdraw() 方法,创建一个 byte[] 类型的对象 lockObject,并将其作为参数传入 MyThread 类的构造器中。这里需要注意的是,lockObject 可以是任意类,使用 byte[] 类型主要是因为底层创建对象的步骤更少。
【实例】同步代码块的应用。
public class Example { public static void main(String[] args) { Account2 account = new Account2(); account.withdraw(); } } class Account2 { // 账户余额 public double balance = 2000; /** * 取钱测试方法 * 在该方法中,模拟丈夫和妻子同时取1500元 */ public void withdraw() { // 创建多线程共用的锁对象 byte[] lockObject = new byte[0]; // 两个线程使用的是同一个锁对象 MyThread t1 = new MyThread("丈夫", 1500, lockObject); MyThread t2 = new MyThread("妻子", 1500, lockObject); t1.start(); t2.start(); } } /** * 封装取钱操作的自定义线程类 */ class MyThread extends Thread { private double money; private String name; // 保存当前线程使用的锁对象 private Object lock; public MyThread(String name, double money, Object lock) { this.name = name; this.money = money; this.lock = lock; } public void run() { // 使用同步锁 synchronized (lock) { if (balance > money) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } balance = balance - money; System.out.println(name + "取钱成功!"); } else { System.out.println(name + "取钱失败!"); } } } }运行结果为:
丈夫取钱成功!
妻子取钱失败!

图 2 使用线程加锁机制后的取钱操作的过程和结果