Java线程同步的多种实现方法(非常详细)
线程安全问题主要是指多个线程同时访问和操作同一共享资源时,可能导致数据的不一致性、程序的不可预测性,以及错误的结果。
【实例 1】火车站票务系统模拟多窗口并行售票流程。
为了模拟火车站的票务销售过程,我们将构建一个场景,其中本次列车的座位总数为 5 个(火车票的最大销售数量为 5 张)。在此框架下,我们将模拟车站的售票窗口功能,特别是多个售票窗口同时运行的情况,以确保票务分配高效且准确。
重要提示,在模拟过程中,需要严格遵循票务的唯一性和准确性原则,以避免任何形式的错票或重票现象发生,代码如下:
在实例 1 的基础上,尝试使用静态变量实现数据共享。
【实例 2】在实例 1 的基础上进行数据共享,代码如下:
在实例 2 的基础上,尝试使用同一个对象的实例变量共享。
【实例 3】在实例 2 的基础上,实现对同一对象实例变量的共享使用,代码如下:

图 1 Java线程同步机制
关键字 synchronized 可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。语法结构如下:
关键字 synchronized 还可以直接修饰方法,表示同一时刻只有一个线程能够进入这个方法,其他线程在外面等待。语法结构如下:
对同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为 this 或类名.class),但对同步方法来说,同步锁对象只能是默认的静态方法[当前类的Class对象(类名.class)]和非静态方法(this)。
【实例 4】在基于例 3 的情境下,建立数据共享机制,以有效应对并解决线程安全问题,代码如下:
② 在非静态方法上添加同步锁。
【实例 5】在基于例 3 的情境下,建立数据共享机制,以有效应对并解决线程安全问题,代码如下:
【实例 6】在基于例 3 的情境下,实现数据共享机制,以有效应对并解决线程安全问题,代码如下:
当窗口 1 的线程开始进行操作时,窗口 2 和窗口 3 的线程需要在外部等待。直到窗口 1 的操作完成,窗口 1、窗口 2 与窗口 3 的线程才有机会进入代码执行阶段。
换言之,在某个线程对共享资源进行修改时,其他线程必须等待该修改完成并实现同步后,方可去竞争 CPU 资源,完成各自的操作。这样的机制确保了数据的同步性,从而解决了线程不安全的问题。
为确保每个线程都能正确执行原子操作,Java 引入了线程同步机制。特别值得注意的是,无论任何时候,都只能有一个线程持有同步锁。获得同步锁的线程将进入代码块执行阶段,而其他线程处于阻塞状态,等待同步锁的释放。
同步锁使加锁和释放锁方法化:
【实例 7】在基于例 3 的情境下,建立数据共享机制,以有效应对并解决线程安全问题,代码如下:
【实例 1】火车站票务系统模拟多窗口并行售票流程。
为了模拟火车站的票务销售过程,我们将构建一个场景,其中本次列车的座位总数为 5 个(火车票的最大销售数量为 5 张)。在此框架下,我们将模拟车站的售票窗口功能,特别是多个售票窗口同时运行的情况,以确保票务分配高效且准确。
重要提示,在模拟过程中,需要严格遵循票务的唯一性和准确性原则,以避免任何形式的错票或重票现象发生,代码如下:
public class WindowDemo extends Thread { private int ticket = 5; @Override public void run() { while (ticket > 0) { System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SafeTicketDemo { public static void main(String[] args) { WindowDemo w1 = new WindowDemo(); WindowDemo w2 = new WindowDemo(); WindowDemo w3 = new WindowDemo(); // 给线程定义名称 w1.setName("窗口 1"); w2.setName("窗口 2"); w3.setName("窗口 3"); // 启动线程 w1.start(); w2.start(); w3.start(); } }程序运行结果为:
窗口2卖出一张票,票号:5 窗口2卖出一张票,票号:4 窗口2卖出一张票,票号:3 窗口2卖出一张票,票号:2 窗口2卖出一张票,票号:1 窗口3卖出一张票,票号:5 窗口3卖出一张票,票号:4 窗口3卖出一张票,票号:3 窗口3卖出一张票,票号:2 窗口3卖出一张票,票号:1 窗口1卖出一张票,票号:5 窗口1卖出一张票,票号:4 窗口1卖出一张票,票号:3 窗口1卖出一张票,票号:2 窗口1卖出一张票,票号:1在上述代码中,局部变量不可共享,若发现售出 15 张票的情况,则需要明确局部变量在每次方法调用时均保持独立状态。因此,每个线程的 run() 方法中的 ticket 变量也各自独立,不存在数据共享问题。同样,不同实例对象的实例变量各自独立,不相互共享数据。
在实例 1 的基础上,尝试使用静态变量实现数据共享。
【实例 2】在实例 1 的基础上进行数据共享,代码如下:
public class WindowDemo02 extends Thread { private static int ticket = 5; public void run() { while (ticket > 0) { try { Thread.sleep(10); // 加入这个命令,使问题暴露得更明显 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SafeTicketDemo02 { public static void main(String[] args) { WindowDemo02 w1 = new WindowDemo02(); WindowDemo02 w2 = new WindowDemo02(); WindowDemo02 w3 = new WindowDemo02(); // 给线程定义名称 w1.setName("窗 1"); w2.setName("窗 2"); w3.setName("窗 3"); // 启动线程 w1.start(); w2.start(); w3.start(); } }程序运行结果为:
窗口1卖出一张票,票号:5 窗口2卖出一张票,票号:5 窗口3卖出一张票,票号:5 窗口3卖出一张票,票号:2 窗口1卖出一张票,票号:2 窗口2卖出一张票,票号:2 窗口3卖出一张票,票号:-1在上述代码中,我们发现已经售出近 5 张票,采用静态变量方式实现数据共享。然而,这一方法导致重复票或负数票问题,暴露了线程安全隐患。
在实例 2 的基础上,尝试使用同一个对象的实例变量共享。
【实例 3】在实例 2 的基础上,实现对同一对象实例变量的共享使用,代码如下:
public class WindowDemo03 implements Runnable { private int ticket = 5; public void run() { while (ticket > 0) { try { Thread.sleep(10); // 加入这个命令,使问题暴露得更明显 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SafeTicketDemo03 { public static void main(String[] args) { // 创建线程任务对象 WindowDemo03 tr = new WindowDemo03(); // 定义线程对象 Thread t1 = new Thread(tr, "窗 1"); Thread t2 = new Thread(tr, "窗 2"); Thread t3 = new Thread(tr, "窗 3"); // 启动线程 t1.start(); t2.start(); t3.start(); } }程序运行结果为:
窗口1卖出一张票,票号:5 窗口2卖出一张票,票号:5 窗口3卖出一张票,票号:5 窗口1卖出一张票,票号:2 窗口3卖出一张票,票号:1 窗口2卖出一张票,票号:1 窗口1卖出一张票,票号:-1在上述代码中,我们发现卖出近 5 张票,但有重复票或负数票问题,依然存在线程安全问题,下一节将讲述如何解决线程安全问题。
Java线程同步机制的实现
要解决上节提到的多线程并发访问一个资源的安全性问题,也就是解决重复票与不存在票(负数票)的问题,Java 提供了线程同步机制,如下图所示。
图 1 Java线程同步机制
关键字 synchronized 可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。语法结构如下:
synchronized(同步锁){ // 需要同步操作的代码 }
关键字 synchronized 还可以直接修饰方法,表示同一时刻只有一个线程能够进入这个方法,其他线程在外面等待。语法结构如下:
public synchronized void method() { // 可能产生线程安全问题的代码 }同步锁对象可以是任意类型,但必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。
对同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为 this 或类名.class),但对同步方法来说,同步锁对象只能是默认的静态方法[当前类的Class对象(类名.class)]和非静态方法(this)。
1) 同步方法
① 在静态方法上添加同步锁。【实例 4】在基于例 3 的情境下,建立数据共享机制,以有效应对并解决线程安全问题,代码如下:
public class WindowDemo04 extends Thread { private static int ticket = 5; public void run() { // 直接锁这里,肯定不行,会导致只有一个窗口卖票 while (ticket > 0) { saleOneTicket(); } } public synchronized static void saleOneTicket() { // 锁对象是 WindowDemo04 类的 Class 对象,而一个类的 Class 对象在内存中肯定只有一个 if (ticket > 0) { // 不加条件,相当于条件判断没有进入锁管控,线程安全问题没有解决 System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SafeTicketDemo04 { public static void main(String[] args) { // 创建线程对象 WindowDemo04 t1 = new WindowDemo04(); WindowDemo04 t2 = new WindowDemo04(); WindowDemo04 t3 = new WindowDemo04(); // 设置线程名称 t1.setName("窗 1"); t2.setName("窗 2"); t3.setName("窗 3"); // 启动线程 t1.start(); t2.start(); t3.start(); } }程序运行结果为:
窗口1卖出一张票,票号:5 窗口2卖出一张票,票号:4 窗口2卖出一张票,票号:3 窗口2卖出一张票,票号:2 窗口2卖出一张票,票号:1在上述代码中,建立了数据共享机制,通过将 synchronized 同步锁用于静态方法之上,巧妙地解决了线程安全问题,确保了数据的一致性和完整性。
② 在非静态方法上添加同步锁。
【实例 5】在基于例 3 的情境下,建立数据共享机制,以有效应对并解决线程安全问题,代码如下:
public class WindowDemo05 implements Runnable { private static int ticket = 5; public void run() { // 直接锁这里,肯定不行,会导致只有一个窗口卖票 while (ticket > 0) { saleOneTicket(); } } public synchronized void saleOneTicket() { // 锁对象是 this,这里就是 WindowDemo05 对象,因为上面三个线程使用同一个 // WindowDemo05 对象,所以可以 if (ticket > 0) { // 不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决 System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SafeTicketDemo05 { public static void main(String[] args) { // 创建线程任务对象 WindowDemo05 tr = new WindowDemo05(); // 创建线程类 Thread t1 = new Thread(tr, "窗 1"); Thread t2 = new Thread(tr, "窗 2"); Thread t3 = new Thread(tr, "窗 3"); // 启动线程 t1.start(); t2.start(); t3.start(); } }在上述代码中,建立了数据共享机制,通过将 synchronized 同步锁用于非静态方法之上,巧妙地解决了线程安全问题,确保了数据的一致性和完整性。
2) 同步代码块
将 synchronized 同步锁应用在代码块上,解决了线程安全问题,确保了数据的一致性和完整性。【实例 6】在基于例 3 的情境下,实现数据共享机制,以有效应对并解决线程安全问题,代码如下:
public class WindowDemo06 { private static int ticket = 5; public void sale() { // 也可以直接给这个方法加锁,锁对象是 this,这里就是 WindowDemo06 对象 if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket); ticket--; } else { throw new RuntimeException("没有票了"); } } public int getTicket() { return ticket; } } public class SafeTicketDemo06 { public static void main(String[] args) { // 创建资源对象 WindowDemo06 ticket = new WindowDemo06(); // 启动多个线程操作资源类的对象 Thread t1 = new Thread("窗 1") { @Override public void run() { // 不能给 run() 直接加锁,因为 t1、t2、t3 的三个 run 方法分别属于三个 // Thread 类对象 // run 方法是非静态方法,那么锁对象默认选 this,那么锁对象根本不是同一个 while (true) { synchronized (ticket) { ticket.sale(); } } } }; Thread t2 = new Thread("窗 2") { @Override public void run() { while (true) { synchronized (ticket) { ticket.sale(); } } } }; Thread t3 = new Thread(new Runnable() { @Override public void run() { while (true) { synchronized (ticket) { ticket.sale(); } } } }, "窗口3"); t1.start(); t2.start(); t3.start(); } }在上述代码中,建立了数据共享机制,通过将 synchronized 同步锁用于代码块之上,巧妙地解决了线程安全问题,确保了数据的一致性和完整性。
当窗口 1 的线程开始进行操作时,窗口 2 和窗口 3 的线程需要在外部等待。直到窗口 1 的操作完成,窗口 1、窗口 2 与窗口 3 的线程才有机会进入代码执行阶段。
换言之,在某个线程对共享资源进行修改时,其他线程必须等待该修改完成并实现同步后,方可去竞争 CPU 资源,完成各自的操作。这样的机制确保了数据的同步性,从而解决了线程不安全的问题。
为确保每个线程都能正确执行原子操作,Java 引入了线程同步机制。特别值得注意的是,无论任何时候,都只能有一个线程持有同步锁。获得同步锁的线程将进入代码块执行阶段,而其他线程处于阻塞状态,等待同步锁的释放。
3) 同步锁(Lock)
java.util.concurrent.locks.Lock 机制提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作,同步代码块/同步方法具有的功能同步锁都有,同步锁的功能更强大,更体现面向对象。同步锁使加锁和释放锁方法化:
- public void lock();加同步锁;
- public void unlock();释放同步锁。
【实例 7】在基于例 3 的情境下,建立数据共享机制,以有效应对并解决线程安全问题,代码如下:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TicketDemo implements Runnable { static int ticket = 6; // 定义一个变量一共卖 10 张票 // 创建 lock 对象 static Lock l = new ReentrantLock(); @Override public void run() { while (true) { method(); } } public static void method() { l.lock(); if (ticket > 0) { try { // Thread 线程对象,调用 sleep 方法,只要睡觉,就会失去 CPU 的执行权,睡醒之后 // 继续获得 CPU 的执行权,参数是毫秒值 Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在买" + ticket--); } l.unlock(); } } public class TicketDemoTest { public static void main(String[] args) { // 创建线程任务 TicketDemo ticketDemo = new TicketDemo(); // 创建三个窗口 Thread thread1 = new Thread(ticketDemo); Thread thread2 = new Thread(ticketDemo); Thread thread3 = new Thread(ticketDemo); // 同时卖票 thread1.start(); thread2.start(); thread3.start(); } }在上述代码中,建立了数据共享机制,通过将同步锁用于代码块之上,巧妙地解决了线程安全问题,确保了数据的一致性和完整性。