线程间通信
注意:
- 必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须持有同步方法(或块)的monitor的所有权,否则将会抛出异常。
- 同步代码的monitor必须与执行wait notify方法的对象一致,简单地说就是用哪个对象的monitor进行同步,就只能用哪个对象进行wait和notify操作。
1、单生产者单消费者模式
顾名思义,就是一个线程消费,一个线程生产。我们先来看看等待/通知机制下的生产者消费者模式:我们假设这样一个场景,我们是卖北京烤鸭店铺,我们现在只有一条生产线也只有一条消费线,也就是说只能生产线程生产完了,再通知消费线程才能去卖,如果消费线程没烤鸭了,就必须通知生产线程去生产,此时消费线程进入等待状态。在这样的场景下,我们不仅要保证共享数据(烤鸭数量)的线程安全,而且还要保证烤鸭数量在消费之前必须有烤鸭。下面我们通过java代码来实现:
package com.zejian.test;
public class KaoYaResource {
private String name;
private int count = 1;//烤鸭的初始数量
private boolean flag = false;//判断是否有需要线程等待的标志
/**
* 生产烤鸭
*/
public synchronized void product(String name){
if(flag){
//此时有烤鸭,等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace()
;
}
}
this.name=name+count;//设置烤鸭的名称
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;//有烤鸭后改变标志
notifyAll();//通知消费线程可以消费了
}
/**
* 消费烤鸭
*/
public synchronized void consume(){
if(flag){//如果没有烤鸭就等待
try{this.wait();}catch(InterruptedException e){}
}
System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);//消费烤鸭1
flag = false;
notifyAll();//通知生产者生产烤鸭
}
}
在这个类中我们有两个synchronized的同步方法,一个是生产烤鸭的,一个是消费烤鸭的,之所以需要同步是因为我们操作了共享数据count,同时为了保证生产烤鸭后才能消费也就是生产一只烤鸭后才能消费一只烤鸭,我们使用了等待/通知机制,wait()和notify()。当第一次运行生产现场时调用生产的方法,此时有一只烤鸭,即flag=false,无需等待,因此我们设置可消费的烤鸭名称然后改变flag=true,同时通知消费线程可以消费烤鸭了,即使此时生产线程再次抢到执行权,因为flag=true,所以生产线程会进入等待阻塞状态,消费线程被唤醒后就进入消费方法,消费完成后,又改变标志flag=false,通知生产线程可以生产烤鸭了………以此循环。
package com.zejian.test;
/**
* @author zejian
* @time 2016年3月12日 下午10:29:12
* @decrition 单生产者单消费者模式
*/
public class Single_Producer_Consumer {
public static void main(String[] args)
{
KaoYaResource r = new KaoYaResource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
//生产者线程
Thread t0 = new Thread(pro);
//消费者线程
Thread t2 = new Thread(con);
//启动线程
t0.start();
t2.start();
}
}
/**
* @author zejian
* @time 2016年3月12日 下午11:02:22
* @decrition 生产者线程
*/
class Producer implements Runnable
{
private KaoYaResource r;
Producer(KaoYaResource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.product("北京烤鸭");
}
}
}
/**
* @author zejian
* @time 2016年3月12日 下午11:02:05
* @decrition 消费者线程
*/
class Consumer implements Runnable
{
private KaoYaResource r;
Consumer(KaoYaResource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.consume();
}
}
}
在这个类中我们创建两个线程,一个是消费者线程,一个是生产者线程,我们分别开启这两个线程用于不断的生产消费,运行结果如下:
很显然的情况就是生产一只烤鸭然后就消费一只烤鸭。运行情况完全正常,嗯,这就是单生产者单消费者模式。上面使用的是synchronized关键字的方式实现的。
2、多生产者多消费者模式
分析完了单生产者单消费者模式,我们再来聊聊多生产者多消费者模式,也就是多条生产线程配合多条消费线程。既然这样的话我们先把上面的代码Single\_Producer\_Consumer.java类修改成新类,大部分代码不变,仅新增2条线程去跑,一条t1的生产 共享资源类KaoYaResource不作更改,代码如下
package com.zejian.test;
/**
* @author zejian
* @time 2016年3月13日 上午10:35:05
* @decrition 多生产者多消费者模式
*/
public class Mutil_Producer_Consumer {
public static void main(String[] args)
{
KaoYaResource r = new KaoYaResource();
Mutil_Producer pro = new Mutil_Producer(r);
Mutil_Consumer con = new Mutil_Consumer(r);
//生产者线程
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
//消费者线程
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
//启动线程
t0.start();
t1.start();
t2.start();
t3.start();
}
}
/**
* @author zejian
* @time 2016年3月12日 下午11:02:22
* @decrition 生产者线程
*/
class Mutil_Producer implements Runnable
{
private KaoYaResource r;
Mutil_Producer(KaoYaResource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.product("北京烤鸭");
}
}
}
/**
* @author zejian
* @time 2016年3月12日 下午11:02:05
* @decrition 消费者线程
*/
class Mutil_Consumer implements Runnable
{
private KaoYaResource r;
Mutil_Consumer(KaoYaResource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.consume();
}
}
}
就多了两条线程,我们运行代码看看,结果如下:
共享数据count的获取方法都进行synchronized关键字同步了呀!那怎么还会出现数据混乱的现象啊?
分析:确实,我们对共享数据也采用了同步措施,而且也应用了等待/通知机制,但是这样的措施只在单生产者单消费者的情况下才能正确应用,但从运行结果来看,我们之前的单生产者单消费者安全处理措施就不太适合多生产者多消费者的情况了。那么问题出在哪里?可以明确的告诉大家,肯定是在资源共享类,下面我们就来分析问题是如何出现,又该如何解决?直接上图!
解决后的资源代码如下只将if改为了while:
package com.zejian.test;
/**
* @author zejian
* @time 2016年3月12日 下午10:44:25
* @decrition 烤鸭资源
*/
public class KaoYaResource {
private String name;
private int count = 1;//烤鸭的初始数量
private boolean flag = false;//判断是否有需要线程等待的标志
/**
* 生产烤鸭
*/
public synchronized void product(String name){
while(flag){
//此时有烤鸭,等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name=name+count;//设置烤鸭的名称
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;//有烤鸭后改变标志
notifyAll();//通知消费线程可以消费了
}
/**
* 消费烤鸭
*/
public synchronized void consume(){
while(!flag){//如果没有烤鸭就等待
try{this.wait();}catch(InterruptedException e){}
}
System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);//消费烤鸭1
flag = false;
notifyAll();//通知生产者生产烤鸭
}
}
运行代码,结果如下:
到此,多消费者多生产者模式也完成,不过上面用的是synchronied关键字实现的,而锁对象的解决方法也一样将之前单消费者单生产者的资源类中的if判断改为while判断即可代码就不贴了哈。
3.线程死锁
现在我们再来讨论一下线程死锁问题,从上面的分析,我们知道锁是个非常有用的工具,运用的场景非常多,因为它使用起来非常简单,而且易于理解。但同时它也会带来一些不必要的麻烦,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。我们先通过一个例子来分析,这个例子会引起死锁,使得线程t1和线程t2互相等待对方释放锁。
package com.zejian.test;
/**
* @author zejian
* @time 2016年3月13日 下午2:45:52
* @decrition 死锁示例
*/
public class DeadLockDemo {
private static String A="A";
private static String B="B";
public static void main(String[] args) {
DeadLockDemo deadLock=new DeadLockDemo();
while(true){
deadLock.deadLock();
}
}
private void deadLock(){
Thread t1=new Thread(new Runnable(){
@SuppressWarnings("static-access")
@Override
public void run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized(B){
System.out.println("1");
}
}
});
Thread t2 =new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
//启动线程
t1.start();
t2.start();
}
}
同步嵌套是产生死锁的常见情景,从上面的代码中我们可以看出,当t1线程拿到锁A后,睡眠2秒,此时线程t2刚好拿到了B锁,接着要获取A锁,但是此时A锁正好被t1线程持有,因此只能等待t1线程释放锁A,但遗憾的是在t1线程内又要求获取到B锁,而B锁此时又被t2线程持有,到此结果就是t1线程拿到了锁A同时在等待t2线程释放锁B,而t2线程获取到了锁B也同时在等待t1线程释放锁A,彼此等待也就造成了线程死锁问题。虽然我们现实中一般不会向上面那么写出那样的代码,但是有些更为复杂的场景中,我们可能会遇到这样的问题,比如t1拿了锁之后,因为一些异常情况没有释放锁(死循环),也可能t1拿到一个数据库锁,释放锁的时候抛出了异常,没有释放等等,所以我们应该在写代码的时候多考虑死锁的情况,这样才能有效预防死锁程序的出现。下面我们介绍一下避免死锁的几个常见方法:
- 1.避免一个线程同时获取多个锁。
- 2.避免在一个资源内占用多个 资源,尽量保证每个锁只占用一个资源。
- 3.尝试使用定时锁,使用tryLock(timeout)来代替使用内部锁机制。
- 4.对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
- 5.避免同步嵌套的发生
转载:
https://blog.csdn.net/javazejian/article/details/50878665
还没有评论,来说两句吧...