【重难点】【操作系统 02】线程有哪几种状态、线程调度方法、保持线程同步的方法、死锁、手写线程安全的生产者与消费者模型
文章目录
一、线程有哪几种状态
我们以 Java 中的线程为例
- 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它会保持这个状态直到程序 start() 这个线程
- 就绪状态:当线程对象调用了 start() 方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待 JVM 里线程调度器的调度
- 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态
- 阻塞状态:如果一个线程执行了 sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。进入阻塞状态的方有三种:
- 等待状态:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态
- 同步阻塞:线程获取 synchronized 同步锁失败(因为同步锁被其它线程占用)
- 其它阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当 sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态
- 死亡状态:一个运行状态的线程完成任务或者 其它终止条件发生时,该线程就切换到终止状态
二、线程调度方法
与进程调度一样
三、保持线程同步的方法
对于多线程访问共享资源出现数据混乱的问题,需要进行线程同步。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源
互斥锁
互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,对于被锁定的这个代码块,所有的线程只能串行处理,不能并行处理
读写锁
读写锁是互斥锁的升级版,所有线程的读操作都是并行的,只有写操作是串行的。程序中的读操作越多,读写锁性能就越高,相反,若程序中全是写操作,那么读写锁会退化成互斥锁
条件变量
条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。常常和互斥锁一起用在生产者和消费者模型中。举个例子,当任务队列为空时,消费者无法消费,使用条件变量把消费者线程阻塞住,等待生产者生产出任务后,再唤醒一个或多个被条件变量阻塞的消费者线程;反过来也可以控制生产者生产的上限,当任务队列达到一个上限值时用条件变量阻塞住生产者线程,直到消费者把任务消费后再唤醒被条件变量阻塞的生产者线程
信号量
信号量可以实现线程同步,也可以实现进程同步,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量主要是为了阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要和互斥锁一起使用。信号量也可以用来实现生产者消费者模型,在用条件变量实现生产者消费者模型时需要自己做条件判断,而使用信号量就不需要。举个例子,初始化生产者的信号量为 5,消费者的信号量为 0,因为消费者信号量为 0,所以会被阻塞。生产者进行一次生产后会将自己的信号量减 1,将消费者信号量加 1,这时消费者解除阻塞,进行消费后再将自己的信号量减 1,将生产者信号量加 1
自旋锁
自旋锁与互斥锁类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多成本
屏障
屏障让每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行
四、死锁
1.概念
多个线程竞争有限数量的资源,自己持有某种其它线程需要的资源不释放,而且又在等待其它线程持有的资源释放,一直在保持这种状态,就称为死锁。简单的说,就是两个或多个进程处于无期限的阻塞、相互等待的状态
2.产生的条件
- 互斥:一个资源每次只能被一个线程使用
- 不可抢占:线程已获得的资源,在未使用完之前,不能强行剥夺
- 占有并等待:一个进程因请求资源而阻塞时,对已获得的资源不释放
- 环形等待:若干进程之间形成一种首尾相接的循环等待资源关系
这四个条件时死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁
3.如何解决死锁
理解了死锁产生的条件,就可以最大可能地避免、预防和解除死锁。具体做法就是在在系统设计、进程调度等方面注意不要让死锁产生的四个必要条件成立,确定合理的资源分配算法,避免进程永久占用系统资源
4.手写一个死锁案例
package com.sisyphus.lock;
public class DeadLock {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
Thread thread1 = new Thread(new FirstThread(object1, object2));
Thread thread2 = new Thread(new SecondThread(object1, object2));
thread1.start();
thread2.start();
}
}
class FirstThread implements Runnable {
Object object1;
Object object2;
public FirstThread(Object object1, Object object2) {
this.object1 = object1;
this.object2 = object2;
}
@Override
public void run() {
synchronized (object1) {
System.out.println("线程 1 获得 锁 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
System.out.println("线程 1 获得 锁 2");
}
}
}
}
class SecondThread implements Runnable {
Object object1;
Object object2;
public SecondThread(Object object1, Object object2) {
this.object1 = object1;
this.object2 = object2;
}
@Override
public void run() {
synchronized (object2) {
System.out.println("线程 2 获得 锁 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
System.out.println("线程 2 获得 锁 1");
}
}
}
}
五、手写线程安全的生产者与消费者模型
1.Synchronized 实现
这是最简单也是最基础的实现方式,缓冲区 count 满和空时都调用 wait() 等待,当生产者生产或消费者消费后唤醒所有线程
package com.sisyphus.product;
public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
//3 个生产者
new Thread(demo.new Producer()).start();
new Thread(demo.new Producer()).start();
new Thread(demo.new Producer()).start();
//2 个消费者
new Thread(demo.new Consumer()).start();
new Thread(demo.new Consumer()).start();
}
private static Integer count = 0;
private static final Integer FULL = 9;
private static final String LOCK = "lock";
class Producer implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK) {
while (count.equals(FULL)) {
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + " 生产者生产了 1 个产品,目前共有 " + count + " 个产品");
LOCK.notifyAll();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK) {
while (count == 0) {
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println(Thread.currentThread().getName() + " 消费者消费了 1 个产品,目前共有 " + count + " 个产品");
LOCK.notifyAll();
}
}
}
}
}
2.ReentrantLock 实现
java.util.concurrent.lock 中的 Lock 通过对 lock() 和 unlock() 实现了对锁的显示控制,而 Synchronized 只能实现隐式控制。Reentrant 实现了 Lock 接口,是一个可重入的互斥锁。互斥锁是指同一时间只能被一个线程持有,可重入是指 ReentrantLock 可以被单个线程多次获取,每获取一次之后就要释放一次,否则其它线程无法获取该 ReentrantLock
与 Synchronized 的等待唤醒机制相比,Condition 具有更多的灵活性以及精确性,这是因为使用 Synchronized 的实现方式想要唤醒线程只能选择 notify() 随机唤醒一个线程,或者使用 notifyAll() 唤醒所有线程。而 Condition 则可以通过多个 Condition 实例对象建立更加精细的线程控制
package com.sisyphus.product;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
//3 个生产者
new Thread(demo.new Producer()).start();
new Thread(demo.new Producer()).start();
new Thread(demo.new Producer()).start();
//2 个消费者
new Thread(demo.new Consumer()).start();
new Thread(demo.new Consumer()).start();
}
private static Integer count = 0;
private static final Integer FULL = 9;
//创建一个锁对象
private final Lock lock = new ReentrantLock();
//创建两个条件变量,一个为缓冲区非满,一个为缓冲区非空
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
class Producer implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取锁
lock.lock();
try {
while (count.equals(FULL)) {
try {
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + " 生产者生产了 1 个产品,目前共有 " + count + " 个产品");
//唤醒消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
while (count == 0) {
try {
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println(Thread.currentThread().getName() + " 消费者消费了 1 个产品,目前共有 " + count + " 个产品");
//唤醒生产者
notFull.signal();
} finally {
lock.unlock();
}
}
}
}
}
3.BlockingQueue 实现
使用 BlockingQueue 的 take() 和 put() 实现生产者消费者,这里的生产者和生产者之间、消费者和消费者之间不存在同步,所以会出现连续生产和连续消费的现象
package com.sisyphus.product;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueueDemo demo = new BlockingQueueDemo();
//3 个生产者
new Thread(demo.new Producer()).start();
new Thread(demo.new Producer()).start();
new Thread(demo.new Producer()).start();
//2 个消费者
new Thread(demo.new Consumer()).start();
new Thread(demo.new Consumer()).start();
}
private static Integer count = 0;
//创建一个阻塞队列
final BlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(9);
class Producer implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
blockingQueue.put(1);
count++;
System.out.println(Thread.currentThread().getName() + " 生产者生产了 1 个产品,目前共有 " + count + " 个产品");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
blockingQueue.take();
count--;
System.out.println(Thread.currentThread().getName() + " 消费者消费了 1 个产品,目前共有 " + count + " 个产品");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}