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();
}
}
在上述代码中,建立了数据共享机制,通过将同步锁用于代码块之上,巧妙地解决了线程安全问题,确保了数据的一致性和完整性。
ICP备案:
公安联网备案: