深入学习理解(7):java:高效的解决死锁问题的线程通讯方式:Semaphore 和 BlockingQueue

经典原始问题:生产者和消费者的问题,其实在实际项目中很容易遇到这样的无奈的问题,但是面对这样的问题的时候我们首先想到的就是多线程批处理,通过notify()…的处理,但只这样的处理只能给我们贴上对java多线程不熟悉的标签。比较赞的办法是用Semaphore 或者 BlockingQueue来实现生产者消费者模型。下面我们就来看看吧!
Semaphore
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()和release()获取和释放访问许可。
Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。
以一个停车场运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。

在java中,还可以设置该信号量是否采用公平模式,如果以公平方式执行,则线程将会按到达的顺序(FIFO)执行,如果是非公平,则可以后请求的有可能排在队列的头部。
JDK中定义如下:

     Semaphore(int permits, boolean fair)
     //创建具有给定的许可数和给定的公平设置的Semaphore。

Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java并发库Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。

Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合

package concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
	Semaphore semaphore=new Semaphore(1);
	List<String>list=new ArrayList<>();
public static void main(String[] args) {
	SemaphoreDemo s=new SemaphoreDemo();
	//创建线程管理
	ExecutorService executorService=Executors.newCachedThreadPool();
	executorService.execute(s.new ThreadTestA());//启动线程
	executorService.execute(s.new ThreadTestB());//
	executorService.shutdown();//结束服务
}
private class ThreadTestA implements Runnable{
	@Override
	public  void  run() {
		try {
			System.out.println("这个是线程A获取许可证之前");
			semaphore.acquire();//获得许可证
			System.out.println("这个是线程A获取许可证之后");
			list.add("test");
			Thread.sleep(1000);//模拟延迟
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		finally{
			semaphore.release();//吊销
			System.out.println("这个是线程A释放许可证之后");
			}	
	}
}
private class ThreadTestB implements Runnable{
	@Override
	public void run() {
		try {
			System.out.println("这个是线程B获取许可证之前");
			semaphore.acquire();
			System.out.println("这个是线程B获取许可证之后");
			if(list.size()!=0)
			System.out.println(list.get(0));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		finally{
			semaphore.release();
			System.out.println("这个是线程B释放许可证之后");
		}
	}
}

}

运行结果可能会出现几种情况
1

这个是线程A获取许可证之前
这个是线程A获取许可证之后
这个是线程B获取许可证之前
这个是线程A释放许可证之后
这个是线程B获取许可证之后
test
这个是线程B释放许可证之后

2

这个是线程A获取许可证之前
这个是线程A获取许可证之后
这个是线程B获取许可证之前
这个是线程B获取许可证之后
test
这个是线程B释放许可证之后
这个是线程A释放许可证之后

3

这个是线程B获取许可证之前
这个是线程B获取许可证之后
这个是线程B释放许可证之后
这个是线程A获取许可证之前
这个是线程A获取许可证之后
这个是线程A释放许可证之后

BlockingQueue
首先是一个接口。有很多子接口和实现类:

BlockingQueue也是java.util.concurrent下的主要用来控制线程同步的工具。

BlockingQueue有四个具体的实现类,根据不同需求,选择不同的实现类
1、ArrayBlockingQueue:一个由数组支持的有界阻塞队列,规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的。

2、LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。

3、PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。

4、SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。

LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。
下面我们通过一个例子来说明:
Bread

package concurrent;

public class Bread {
private String name;
private String color;
public String getName() {
	return name;
}
public void setName(String name) {
	this.name = name;
}
public String getColor() {
	return color;
}
public void setColor(String color) {
	this.color = color;
}
}

生产者机器 BreadMachine

package concurrent;

import java.awt.Color;
import java.util.Random;
import java.util.concurrent.BlockingQueue;

public class BreadMachine implements Runnable{
	 private BlockingQueue<Bread> queue;
     public BreadMachine(BlockingQueue<Bread> queue){
   	  this.queue=queue;
     }
		@Override
		public void run() {
			System.out.println("开始生产面包");
			Bread bread=new Bread();
			bread.setColor(Color.BLACK+"");
			Random random=new Random();
			bread.setName(random.nextInt(100)+"面包");
			try {
				queue.put(bread);
				System.out.println(bread.getName()+"已经生产完毕!去吃吧");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
   	
}

消费者People

package concurrent;

import java.util.concurrent.BlockingQueue;

public class People implements Runnable{

	BlockingQueue<Bread> bread;
	public People(BlockingQueue<Bread> bread ) {
		this.bread=bread;
	}
	@Override
	public void run() {
		try {
			System.out.println("我需要面包");
		System.out.println("这个二货吃了"+bread.take().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

测试程序

package concurrent;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueenDemo {
    static BlockingQueue<Bread> blockingQueue=new ArrayBlockingQueue<>(5);
    public static void main(String[] args) {
		BreadMachine breadMachine=new BreadMachine(blockingQueue);
		People people=new People(blockingQueue);
		for(int i=0;i<5;i++){
			new Thread(people, "people").start();
			new Thread(breadMachine, "breadMachine").start();
			
		}
	}
    
}

运行效果;

开始生产面包
开始生产面包
我需要面包
我需要面包
开始生产面包
我需要面包
开始生产面包
我需要面包
我需要面包
开始生产面包
29面包已经生产完毕!去吃吧
23面包已经生产完毕!去吃吧
这个二货吃了29面包
66面包已经生产完毕!去吃吧
31面包已经生产完毕!去吃吧
36面包已经生产完毕!去吃吧
这个二货吃了23面包
这个二货吃了66面包
这个二货吃了31面包
这个二货吃了36面包

由于队列的大小限定成了5,所以最多只有两个产品被加入到队列当中,而且消费者取到产品的顺序也是按照生产的先后顺序,原因就是LinkedBlockingQueue和ArrayBlockingQueue都是按照FIFO的顺序存取元素的。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您编写一个Java程序,使用ReentrantLock锁来解决中国人和美国人吃饭死锁案例。下面是程序的伪代码: ```java import java.util.concurrent.locks.ReentrantLock; public class DeadlockSolution { private static ReentrantLock chinaLock = new ReentrantLock(); private static ReentrantLock usaLock = new ReentrantLock(); public static void main(String[] args) { Thread chinaThread = new Thread(new ChinaPerson()); Thread usaThread = new Thread(new USAPerson()); chinaThread.start(); usaThread.start(); } static class ChinaPerson implements Runnable { @Override public void run() { while (true) { chinaLock.lock(); try { System.out.println("中国人正在吃饭..."); Thread.sleep(1000); usaLock.lock(); try { System.out.println("中国人拿到美国人的餐具,可以继续吃饭了!"); Thread.sleep(1000); } finally { usaLock.unlock(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { chinaLock.unlock(); } } } } static class USAPerson implements Runnable { @Override public void run() { while (true) { usaLock.lock(); try { System.out.println("美国人正在吃饭..."); Thread.sleep(1000); chinaLock.lock(); try { System.out.println("美国人拿到中国人的餐具,可以继续吃饭了!"); Thread.sleep(1000); } finally { chinaLock.unlock(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { usaLock.unlock(); } } } } } ``` 在这个程序中,我们使用了两个ReentrantLock锁:`chinaLock`和`usaLock`。当中国人线程进入run()方法时,首先获取`chinaLock`锁,然后尝试获取`usaLock`锁,如果获取成功,则表示中国人可以继续吃饭,否则就等待。当美国人线程进入run()方法时,也是类似的操作,先获取`usaLock`锁,然后尝试获取`chinaLock`锁,如果获取成功,则表示美国人可以继续吃饭,否则就等待。 这样,即使中国人和美国人同时进入run()方法,也不会因为死锁而阻塞,因为它们都会先尝试获取一个锁,然后再去获取另一个锁,从而避免了死锁的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值