Java多线程3:同步机制,死锁、生产者消费者问题及其解决办法等
死锁
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
产生死锁的四个必要条件
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。
- 产生死锁
假设存在公司和工人,工人:先给钱再干活。公司:先干活再给钱。于是死锁就产生了。
代码示例如下:
public class DieLock {
public static Integer flag1=0;
public static Integer flag2=1;
public static void main(String[] args) {
// TODO 自动生成的方法存根
Company company = new Company("电子厂");
Emp emp = new Emp("杨大侠");
new Thread(company).start();
new Thread(emp).start();
}
}
class Company implements Runnable{
public String name;
public Company(String name) {
this.name=name;
}
public void run() {
synchronized (DieLock.flag1) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("开始获得flag2锁");
synchronized (DieLock.flag2) {
System.out.println(name+"先工作再给钱,获得flag2对象锁");
}
}
}
}
class Emp implements Runnable{
public String name;
public Emp(String name) {
this.name=name;
}
public void run() {
synchronized (DieLock.flag2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("开始获得flag1锁");
synchronized (DieLock.flag1) {
System.out.println(name+"先给钱再工作,获得flag2对象锁");
}
}
}
}
可以看到输出结果如下,且运行一直未停止,可知上述代码运行中,当两个线程进入synchronized 代码块中时,company锁住flag1对象,emp锁住flag2对象,而在两个对象的逐渐运行到第二个synchronized代码块时,请求锁住flag2和flag1,此时这两个对象锁却还没有被释放,导致死锁。
生产者消费者问题(PV)
- PV操作是一种实现进程互斥与同步的有效方法。PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思。
- 该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。(百度百科)
- 要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有管程法、信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
问题及解决示例:
- 问题:
存在一个生产者,生产商品,存在一个消费者,消费商品。 - 解决方法1:管程法
管程法采用一个缓存仓库,暂时保存商品。仓库满,通知生产者等待生产,仓库空,通知消费者等待消费。
代码示例如下,其中需要注意两个API
object.wait();//wait()方法,来自于所有类的父类Object类,此方法作用为使调用当前方法的线程休眠,进行等待。
object.notify();notify()方法,同来自Object类,此方法作用为唤醒所有处于等待(休眠)中的线程。
管程法代码示例:
public class Pv {
public static void main(String[] args) {
// TODO 自动生成的方法存根
// List<Product> productList=new ArrayList<>(10);
// productList.add(new Product("1"));
// productList.remove(0);
// System.out.println(productList.size());
Sto sto=new Sto();
new Thread(new Pro(sto)).start();
new Thread(new Cus(sto)).start();
}
}
class Pro implements Runnable{
private Sto sto;
public Pro(Sto sto){
this.sto=sto;
}
public void run() {
// TODO 自动生成的方法存根
for (int i = 0; i < 20; i++) {
Product produce = new Product("产品"+i);
try {
sto.push(produce);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
class Cus implements Runnable{
private Sto sto;
public Cus(Sto sto){
this.sto=sto;
}
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
for (int i = 0; i <20; i++) {
try {
sto.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Sto{
private List<Product> productList=new ArrayList<>(10);
private int index=0;
public synchronized void push(Product product) throws InterruptedException {
if(index>9) {
this.wait();
}
productList.add(product);
System.out.println("向仓库中放如"+product.name);
index=index+1;
System.out.println("index "+index);
this.notify();
}
public synchronized void pop() throws InterruptedException {
if (index<=0) {
this.wait();
}
Product remove = productList.remove(index-1);
System.out.println("消费仓库中的"+remove.name);
index=index-1;
this.notify();
}
}
class Product{
public String name;
public Product(String name){
this.name=name;
}
}
输出如下:
- 信号灯法
即设置一个标志位当作唤起线程和等待线程的信号灯(标志)。
具体代码待续。。。。