Java 实现多线程【同步】的三种方式
多线程之间对同一共享资源进行操作,容易出现线程安全问题,解决方案就是把共享资源加锁,从而实现线程同步,使任意时刻只能有一个线程操作共享资源。Java 有 3 种方式可以实现线程同步,为了更清晰的描述方案,我以两个窗口卖火车票为例进行介绍 3 种线程同步的方案。本篇博客目的在于总结 Java 多线程同步的知识点,以便在平时工作中用到的时候,可以快速上手。
方案一、采用同步代码块
同步代码块格式:
//需要确保多个线程使用的是同一个锁对象
synchronized (锁对象) {
多条语句操作共享数据的代码
}
代码演示:
public class Ticket implements Runnable {
//火车票的总数量
private int ticket = 50;
//锁对象
private Object obj = new Object();
@Override
public void run() {
while (true) {
//同步代码块:多个线程必须使用同一个锁对象
synchronized (obj) {
if (ticket <= 0) {
break;
} else {
try {
Thread.sleep(100);
} catch (Exception ex) {
ex.printStackTrace();
}
ticket = ticket - 1;
System.out.println(Thread.currentThread().getName() +
"正在卖票,还剩下 " + ticket + " 张票");
}
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
/*
不能采用这种方式,因为这样相当于每个线程使用不同的对象,没有共享资源
Ticket ticket1 = new Ticket();
Ticket ticket2 = new Ticket();
Thread t1 = new Thread(ticket1);
Thread t2 = new Thread(ticket2);*/
//实例化一个对象,让所有线程都使用这一个对象
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
}
}
同步代码块:这种实现方案允许一个类中存在多个锁对象。
如果想让多个线程即使访问多个不同的代码块,也要统一排队等待的话,可以让多个代码块使用同一个锁对象。
如果想让多个线程访问不同的代码块互不影响,但是访问同一个代码块需要排队等待的话,可以让多个代码块分别使用不同的锁对象。
方案二、采用同步方法
同步方法的格式:
//同步方法的锁对象是其所在类的实例化对象本身 this
修饰符 synchronized 返回值类型 方法名 (方法参数) {
方法体
}
//同步静态方法的锁对象是其所在的类的 类名.Class
修饰符 static synchronized 返回值类型 方法名 (方法参数) {
方法体
}
同步方法的代码演示:
public class Ticket implements Runnable {
private static int ticketCount = 50;
@Override
public void run() {
while (true) {
//这里先休眠 100 毫秒,为了让多个线程都有机会抢夺共享资源
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使用同步方法
boolean result = synchronizedMthod();
if (result) {
break;
}
}
}
//同步方法的锁对象就是 this 本身
private synchronized boolean synchronizedMthod() {
if (ticketCount <= 0) {
return true;
} else {
ticketCount = ticketCount - 1;
System.out.println(Thread.currentThread().getName() +
"正在卖票,还剩下 " + ticketCount + " 张票");
return false;
}
}
}
public class TicketDemo {
public static void main(String[] args) {
//实例化一个对象,让所有线程都使用这一个对象
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket,"窗口一");
Thread t2 = new Thread(ticket,"窗口二");
t1.start();
t2.start();
}
}
同步静态方法的代码演示:
//为了证明同步静态方法的锁对象是其所在的类的 类名.Class
//这里针对两个窗口线程,分别采用不同的同步方式来证明
//窗口一线程,采用同步静态方法
//窗口二线程,采用同步代码块,但是使用的是当前类的 类名.Class 作为锁对象
//最终可以发现【窗口一线程】和【窗口二线程】能够实现线程同步
public class Ticket implements Runnable {
private static int ticketCount = 50;
@Override
public void run() {
while (true) {
//窗口一线程,使用同步静态方法
if ("窗口一".equals(Thread.currentThread().getName())) {
//同步方法
boolean result = synchronizedMthod();
if (result) {
break;
}
}
//窗口二线程,使用同步代码块,但是锁对象是当前类的 类名.Class
if ("窗口二".equals(Thread.currentThread().getName())) {
//同步代码块
synchronized (Ticket.class) {
if (ticketCount <= 0) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() +
"正在卖票,还剩下 " + ticketCount + " 张票");
}
}
}
}
}
//同步静态方法
private static synchronized boolean synchronizedMthod() {
if (ticketCount <= 0) {
return true;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() +
"正在卖票,还剩下 " + ticketCount + " 张票");
return false;
}
}
}
public class TicketDemo {
public static void main(String[] args) {
//实例化一个对象,让所有线程都使用这一个对象
Ticket mr = new Ticket();
Thread t1 = new Thread(mr, "窗口一");
Thread t2 = new Thread(mr, "窗口二");
t1.start();
t2.start();
}
}
同步方法:这种方案会导致同一个实例对象中的所有的同步方法的锁对象都是 this ,因此多个线程即使访问该实例对象中不同的同步方法时,也必须统一排队等待。
同步静态方法:这种方案导致同一个类中所有的同步静态方法的锁对象都是当前的 类名.Class ,因此多个线程即使访问该类中不同的同步静态方法时,也必须统一排队等待。
方案三、采用 Lock 锁对象实例
JDK5以后提供了一个新的锁对象 Lock,但是 Lock 是接口不能直接实例化,因此必须采用它的实现类 ReentrantLock 来实现线程同步。ReentrantLock 有两个方法:
方法名 | 说明 |
---|---|
void lock() | 对多线程要访问的共享资源代码加锁 |
void unlock() | 对多线程要访问的共享资源代码解锁 |
代码演示:
public class Ticket implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//这里先休眠 100 毫秒,为了让多个线程都有机会抢夺共享资源
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock(); //加锁
if (ticket <= 0) {
break;
} else {
ticket--;
System.out.println(Thread.currentThread().getName() +
"正在卖票,还剩下 " + ticket + " 张票");
}
lock.unlock(); //解锁
}
}
}
public class TicketDemo {
public static void main(String[] args) {
//实例化一个对象,让所有线程都使用这一个对象
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket,"窗口一");
Thread t2 = new Thread(ticket,"窗口二");
t1.start();
t2.start();
}
}
这种方案,跟同步代码块一样,一个类中可以存在多个锁对象。只不过需要自己手动进行加锁和解锁。
到此为止,三种线程同步的方案已经介绍完毕,每种方案各有优缺点,大家可以根据实际需要,选择使用不同的方案。