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,此时这两个对象锁却还没有被释放,导致死锁。
image.png

生产者消费者问题(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;
	}
}

输出如下:
image.png

  • 信号灯法
    即设置一个标志位当作唤起线程和等待线程的信号灯(标志)。
    具体代码待续。。。。