线程同步和线程通信
Synchronized锁和Lock锁机制解决线程同步问题,及wait和notify实现线程间通信的简要介绍
Author: Msuenb
Date: 2023-02-15
线程同步
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
通过一个经典案例,演示线程的安全问题: 模拟火车站的卖票过程。假设本趟列车有100张票,开启三个窗口售票。
同一资源和线程同步问题
现在需要三个窗口卖的是全部的100张票,而不是每个窗口各自卖一百张票。所以要实现资源的同一。
在Java中局部变量和实例变量都不是共享变量,静态变量对于不同的实例对象是共享的,因此可以用静态变量存储剩余票数。
示例代码:
public class ThreadSync01 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.start();
t2.start();
t3.start();
}
}
class TicketSaleThread extends Thread{
private static int total = 100;
public void run(){
while(total>0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖出一张票,剩余:" + --total);
}
}
}
运行结果:
Thread-0卖出一张票,剩余:99
Thread-2卖出一张票,剩余:98
Thread-1卖出一张票,剩余:97
Thread-2卖出一张票,剩余:96
Thread-1卖出一张票,剩余:94
Thread-0卖出一张票,剩余:95
...
Thread-0卖出一张票,剩余:4
Thread-0卖出一张票,剩余:2
Thread-1卖出一张票,剩余:2
Thread-2卖出一张票,剩余:1
Thread-2卖出一张票,剩余:0
Thread-1卖出一张票,剩余:-1
售票结束...
Thread-0卖出一张票,剩余:-2
售票结束...
售票结束...
结果:有重复票和负数票问题。导致这样情况的原因如下
![](https://s1.ax1x.com/2023/02/14/pSTwiCR.md.png)
Synchronized同步机制
要解决上述多线程并发访问一个资源的安全性问题,也就是解决重复售票与超额售票问题,Java中提供了同步机制(synchronized)
来解决。
![](https://s1.ax1x.com/2023/02/14/pSTBetH.md.png)
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
同步解决线程安全的原理:
-
同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,这个锁称为同步锁。
-
当一个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。
同步代码块和同步方法:
-
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。
public synchronized void method(){ 可能会产生线程安全问题的代码 }
-
同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁对象){ 需要同步操作的代码 }
同步锁对象的选择:
同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。
对于同步代码块来说,同步锁对象是由程序员手动指定的,但是对于同步方法来说,同步锁对象只能是默认的,
- 静态方法:当前类的Class对象
- 非静态方法:this
同步代码的范围选择:
-
锁的范围太小:不能解决安全问题
-
锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。
释放同步锁:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导 致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。
注意:以下操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程的执行- 线程执行同步代码块时,其他线程调用了该线程的
suspend()
(避免使用)方法将该线程 挂起,该线程不会释放锁
Synchronized使用演示
-
静态方法加锁
public class ThreadSync02 { public static void main(String[] args) { TicketSaleThread t1 = new TicketSaleThread(); TicketSaleThread t2 = new TicketSaleThread(); TicketSaleThread t3 = new TicketSaleThread(); t1.start(); t2.start(); t3.start(); } } class TicketSaleThread extends Thread { private static int tickets = 100; @Override public void run() { // 直接锁这里,肯定不行,会导致,只有一个窗口卖票 while (tickets > 0) { saleTicket(); } } // 锁对象是TicketSaleThread类的Class对象 一个类的Class对象只有一个 private synchronized static void saleTicket() { if (tickets > 0) { // 不加条件 相当于条件判断没有进入锁管控 线程安全问题就没有解决 System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --tickets); } } }
-
非静态方法加锁
public class ThreadSync03 { public static void main(String[] args) { TicketSaleRunnable t = new TicketSaleRunnable(); new Thread(t, "窗口1").start(); new Thread(t, "窗口2").start(); new Thread(t, "窗口3").start(); } } class TicketSaleRunnable implements Runnable { private static int tickets = 100; @Override public void run() { // 直接锁这里,肯定不行,会导致,只有一个窗口卖票 while (tickets > 0) { saleTicket(); } } // 锁对象是 this 这里就是TicketSaleRunnable的对象 private synchronized void saleTicket() { if (tickets > 0) { // 不加条件 相当于条件判断没有进入锁管控 线程安全问题就没有解决 System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --tickets); } } }
-
同步代码块
public class ThreadSync04 { public static void main(String[] args) { // 2、创建资源对象 Ticket ticket = new Ticket(); // 3、启动多个线程操作资源类的对象 Thread t1 = new Thread() { @Override public void run() { //不能给run()直接加锁,因为t1,t2,t3的三个run方法分别属于三个Thread类对象, // run方法是非静态方法,那么锁对象默认选this,那么锁对象根本不是同一个 while (true) { // 通过异常停止 synchronized (ticket) { ticket.sale(); } } } }; Thread t2 = new Thread() { @Override public void run() { while (true) { synchronized (ticket) { ticket.sale(); } } } }; Thread t3 = new Thread() { @Override public void run() { while (true) { synchronized (ticket) { ticket.sale(); } } } }; t1.start(); t2.start(); t3.start(); } } // 1、编写资源类 class Ticket { private static int tickets = 100; public void sale() { if (tickets > 0) System.out.println(Thread.currentThread().getName() + "卖出了一张票,还剩:" + --tickets); else throw new RuntimeException("票已卖完..."); } public static int getTickets() { return tickets; } }
Lock锁
从JDK 5.0开始,Java提供了更强大的线程同步机制,通过显式定义Lock锁对象来实现同步。
- Lock是
java.util.concurrent.locks
包的接口,它提供了比 synchronized 更加广泛的锁定操作,Lock接口有三个实现类:ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock,即重入锁、读锁和写锁,一般用ReentrantLock为其实例化。 - Lock锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。Lock需要显式地创建、加锁和释放。
Lock一般结构:
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
} finally {
lock.unlock();
}
}
}
Lock使用演示:
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
TicketSaleLock ticketSaleLock = new TicketSaleLock();
new Thread(ticketSaleLock).start();
new Thread(ticketSaleLock).start();
new Thread(ticketSaleLock).start();
}
}
class TicketSaleLock implements Runnable {
private static int tickets = 100;
private final ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
reentrantLock.lock(); // 加锁
try {
if (tickets > 0) // 不加条件 相当于条件判断没有进入锁管控 线程安全问题就没有解决
System.out.println(Thread.currentThread() +
"卖出了一张票,还剩:" + --tickets);
else break;
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock(); // 解锁
}
}
}
}
synchronized 与 Lock 的对比:
- Lock是显式锁(手动创建、开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)
线程通信
多个线程处理同一个资源,但线程的任务却不同。而多个线程并发执行时, 默认情况下CPU是随机切换线程的,当需要多个线程来共同完成一件任务,并且希望它们有规律的执行, 则多线程之间需要一些通信机制协调它们的工作。
等待唤醒机制
这是多个线程间的一种协作机制。就是在一个线程满足某个条件时,就进入等待状态wait()/wait(time)
, 等待其他线程执行完指定代码过后再将其唤醒notify()
;或可以指定wait的时间,等时间到了自动唤醒;有多个线程进行等待时,可以使用 notifyAll()
来唤醒所有的等待线程。wait/notify
就是线程间的一种协作机制。
- wait:线程不再活动和参与调度,进入 wait set 中。它要等别的线程
notify
或者等待时间到,才能从wait set 中释放出来,重新进入到就绪队列中 - notify:选取所通知对象的 wait set 中的一个线程释放;
- notifyAll:释放所通知对象的 wait set 上的全部线程。
注意: 被通知线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁。
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
- 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
wait和notify使用细节:
- wait方法与notify方法必须要由同一个锁对象调用。
- wait方法与notify方法是属于Object类的方法的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。
生产者与消费者问题
等待唤醒机制可以解决经典的“生产者与消费者”的问题。
生产者与消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
生产者与消费者问题中其实隐含了两个问题:
-
线程安全问题:因为生产者与消费者共享数据缓冲区,不过这个问题可以使用同步解决。
-
线程的协调工作问题:
可以通过
wait/notify
机制解决。- 让生产者线程在缓冲区满时 wait,等待消费者线程消耗了缓冲区数据之后 notify;
- 让消费者线程在缓冲区空时 wait,等待生产者进程往缓冲区添加数据之后 notify
单生产者与单消费者
案例:有家餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务员从这个工作台取出快餐给顾客。现在有1个厨师和1个服务员。
public class CommunicateTest {
public static void main(String[] args) {
// 创建资源对象
Workbench wb = new Workbench();
// 启动厨师线程
new Thread("厨师") {
@Override
public void run() {
while (true) {
try {
wb.put();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}.start();
// 启动服务员线程
new Thread("服务员") {
@Override
public void run() {
while (true) {
try {
wb.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}.start();
}
}
// 定义资源类
class Workbench {
private static final int MAX_VALUE = 10;
private int num;
public synchronized void put() throws InterruptedException {
if (num >= MAX_VALUE) {
this.wait();
}
Thread.sleep(500);
num++;
System.out.println(Thread.currentThread().getName() +
"制作了一份快餐,现在工作台上有:" + num + " 份快餐");
this.notify();
}
public synchronized void take() throws InterruptedException {
if (num <= 0) {
this.wait();
}
Thread.sleep(250);
num--;
System.out.println(Thread.currentThread().getName() +
"取走了一份快餐,现在工作台上有:" + num + " 份快餐");
this.notify();
}
}
多生产者与多消费者
案例:有家餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务员从这个工作台取出快餐给顾客。现在有多个厨师和多个服务员。
public class CommunicateTest {
public static void main(String[] args) {
WindowBoard wb = new WindowBoard();
Cook c1 = new Cook("厨师1", wb);
Cook c2 = new Cook("厨师2", wb);
Waiter w1 = new Waiter("服务员1", wb);
Waiter w2 = new Waiter("服务员2", wb);
c1.start();
c2.start();
w1.start();
w2.start();
}
}
class WindowBoard {
private static final int MAX_VALUE = 10;
private int num;
public synchronized void put() throws InterruptedException {
while (num >= MAX_VALUE) { // 不能是if
this.wait();
}
Thread.sleep(500);
num++;
System.out.println(Thread.currentThread().getName() +
"制作了一份快餐,现在工作台上有:" + num + " 份快餐");
this.notifyAll();
}
public synchronized void take() throws InterruptedException {
while (num <= 0) { // 不能是if
this.wait();
}
Thread.sleep(250);
num--;
System.out.println(Thread.currentThread().getName() +
"取走了一份快餐,现在工作台上有:" + num + " 份快餐");
this.notifyAll();
}
}
class Cook extends Thread {
private String name;
private WindowBoard wb;
public Cook(String name, WindowBoard wb) {
super(name);
this.wb = wb;
}
@Override
public void run() {
while (true) {
try {
wb.put();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Waiter extends Thread {
private String name;
private WindowBoard wb;
public Waiter(String name, WindowBoard wb) {
super(name);
this.wb = wb;
}
@Override
public void run() {
while (true) {
try {
wb.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}