线程通信,死锁

线程通信

  • 应用场景:生产者和消费者问题
  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,到仓库中再次放入产品为止.

1、借助于Object类的wait()、notify()和notifyAll()实现通信

线程执行wait()后,就放弃了运行资格,处于冻结状态;

​ 线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
​ notifyall(), 唤醒线程池中所有线程。

注: (1) wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
(2) wait(),notify(),notifyall(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。

单个消费者生产者例子如下:

 class Resource{  //生产者和消费者都要操作的资源  
    private String name;  
    private int count=1;  
    private boolean flag=false;  
    public synchronized void set(String name){  
        if(flag)  
            try{wait();}catch(Exception e){}  
        this.name=name+"---"+count++;  
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);  
        flag=true;  
        this.notify();  
    }  
    public synchronized void out(){  
        if(!flag)  
            try{wait();}catch(Exception e){}  
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  
        flag=false;  
        this.notify();  
    }  
}  
class Producer implements Runnable{  
    private Resource res;  
    Producer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.set("商品");  
        }  
    }  
}  
class Consumer implements Runnable{  
    private Resource res;  
    Consumer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.out();  
        }  
    }  
}  
public class ProducerConsumerDemo{  
    public static void main(String[] args){  
        Resource r=new Resource();  
        Producer pro=new Producer(r);  
        Consumer con=new Consumer(r);  
        Thread t1=new Thread(pro);  
        Thread t2=new Thread(con);  
        t1.start();  
        t2.start();  
    }  
}//运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。

但是如果有多个生产者和多个消费者,上面的代码是有问题,比如2个生产者,2个消费者,运行结果就可能出现生产的1个商品生产了一次而被消费了2次,或者连续生产2个商品而只有1个被消费,这是因为此时共有4个线程在操作Resource对象r,
而notify()唤醒的是线程池中第1个wait()的线程,所以生产者执行notify()时,唤醒的线程有可能是另1个生产者线程,这个生产者线程从wait()中醒来后不会再判断flag,而是直接向下运行打印出一个新的商品,这样就出现了连续生产2个商品。
为了避免这种情况,修改代码如下:

class Resource{  
        private String name;  
        private int count=1;  
        private boolean flag=false;  
        public synchronized void set(String name){  
            while(flag) /*原先是if,现在改成while,这样生产者线程从冻结状态醒来时,还会再判断flag.*/  
                try{wait();}catch(Exception e){}  
            this.name=name+"---"+count++;  
            System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);  
            flag=true;  
            this.notifyAll();/*原先是notity(), 现在改成notifyAll(),这样生产者线程生产完一个商品后可以将等待中的消费者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/  
        }  
        public synchronized void out(){  
            while(!flag) /*原先是if,现在改成while,这样消费者线程从冻结状态醒来时,还会再判断flag.*/  
                try{wait();}catch(Exception e){}  
            System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  
            flag=false;  
            this.notifyAll(); /*原先是notity(), 现在改成notifyAll(),这样消费者线程消费完一个商品后可以将等待中的生产者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/  
        }  
    }  
    public class ProducerConsumerDemo{  
        public static void main(String[] args){  
            Resource r=new Resource();  
            Producer pro=new Producer(r);  
            Consumer con=new Consumer(r);  
            Thread t1=new Thread(pro);  
            Thread t2=new Thread(con);  
            Thread t3=new Thread(pro);  
            Thread t4=new Thread(con);  
            t1.start();  
            t2.start();  
            t3.start();  
            t4.start();  
        }  
    }  

2、使用Condition控制线程通信

jdk1.5中,提供了多线程的升级解决方案为:

​ (1)将同步synchronized替换为显式的Lock操作;

​ (2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;

​ (3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。

 class Resource{  
        private String name;  
        private int count=1;  
        private boolean flag=false;  
        private Lock lock = new ReentrantLock();/*Lock是一个接口,ReentrantLock是该接口的一个直接子类。*/  
        private Condition condition_pro=lock.newCondition(); /*创建代表生产者方面的Condition对象*/  
        private Condition condition_con=lock.newCondition(); /*使用同一个锁,创建代表消费者方面的Condition对象*/  
          
        public void set(String name){  
            lock.lock();//锁住此语句与lock.unlock()之间的代码  
            try{  
                while(flag)  
                    condition_pro.await(); //生产者线程在conndition_pro对象上等待  
                this.name=name+"---"+count++;  
                System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);  
                flag=true;  
                 condition_con.signalAll();  
            }  
            finally{  
                lock.unlock(); //unlock()要放在finally块中。  
            }  
        }  
        public void out(){  
            lock.lock(); //锁住此语句与lock.unlock()之间的代码  
            try{  
                while(!flag)  
                    condition_con.await(); //消费者线程在conndition_con对象上等待  
            System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  
            flag=false;  
            condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/  
            }  
            finally{  
                lock.unlock();  
            }  
        }  
    }  

3、使用阻塞队列(BlockingQueue)控制线程通信

​ BlockingQueue是一个接口,也是Queue的子接口。**BlockingQueue具有一个特征:**当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

BlockingQueue提供如下两个支持阻塞的方法:

**(1)put(E e):**尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。

**(2)take():**尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:

**(1)**在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。

**(2)**在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。

**(3)**在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue接口包含如下5个实现类:

ArrayBlockingQueue ://基于数组实现的BlockingQueue队列。

LinkedBlockingQueue://基于链表实现的BlockingQueue队列。

PriorityBlockingQueue://它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。
 //  它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。

SynchronousQueue://同步队列。对该队列的存、取操作必须交替进行。

DelayQueue://它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),
           // DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

案列

public class BlockingQueueTest{
 4     public static void main(String[] args)throws Exception{
 5         //创建一个容量为1的BlockingQueue
 6         
 7         BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
 8         //启动3个生产者线程
 9         new Producer(b).start();
10         new Producer(b).start();
11         new Producer(b).start();
12         //启动一个消费者线程
13         new Consumer(b).start();
14         
15     }
16 }
17 class Producer extends Thread{
18     private BlockingQueue<String> b;
19     
20     public Producer(BlockingQueue<String> b){
21         this.b=b;
22         
23     }
24     public synchronized void run(){
25         String [] str=new String[]{
26             "java",
27             "struts",
28             "Spring"
29         };
30         for(int i=0;i<9999999;i++){
31             System.out.println(getName()+"生产者准备生产集合元素!");
32             try{
33             
34                 b.put(str[i%3]);
35                 sleep(1000);
36                 //尝试放入元素,如果队列已满,则线程被阻塞
37                 
38             }catch(Exception e){System.out.println(e);}
39             System.out.println(getName()+"生产完成:"+b);
40         }
41         
42     }
43 }
44 class Consumer extends Thread{
45     private BlockingQueue<String> b;
46     public Consumer(BlockingQueue<String> b){
47         this.b=b;
48     }
49     public  synchronized  void run(){
50     
51         while(true){
52             System.out.println(getName()+"消费者准备消费集合元素!");
53             try{
54                 sleep(1000);
55                 //尝试取出元素,如果队列已空,则线程被阻塞
56                 b.take();
57             }catch(Exception e){System.out.println(e);}
58             System.out.println(getName()+"消费完:"+b);
59         }
60     
61     }
62 }

线程池

合理利用线程池能够带来三个好处。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

JDK5.0起提供了线程池相关API:ExecutorService和Executors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecuto

voidexecute(Runnablecommand):执行任务/命令,没有返回值,一般用来执行Runnable

< T>Future< T>submit(Callable< T>task):执行任务,有返回值,一般又来执行Callable

voidshutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

使用Executors工厂类产生线程池

Executor线程池框架的最大优点是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,然后提交即可。而Task如何执行客户端则是透明的。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果。线程池实现原理类结构图如下:
在这里插入图片描述

由上图可知,ExecutorServiceJava中对线程池定义的一个接口,它java.util.concurrent包中。 Java API对ExecutorService接口的实现有两个,所以这两个即是Java线程池具体实现类如下:

ThreadPoolExecutor
 ScheduledThreadPoolExecutor

除此之外,ExecutorService还继承了Executor接口(注意区分Executor接口和Executors工厂类),这个接口只有一个execute()方法,最后我们看一下整个继承树:

​ 使用Executors执行多线程任务的步骤如下:

• 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池;

• 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;

• 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;

• 当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。

死锁

产生死锁的四个必要条件如下。当下边的四个条件都满足时即产生死锁,即任意一个条件不满足既不会产生死锁

(1)死锁的四个必要条件

  • 互斥条件:资源不能被共享,只能被同一个进程使用

  • 请求与保持条件:已经得到资源的进程可以申请新的资源

  • 非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺

  • 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源

    举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

(2)处理死锁的方法

  • 忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;
  • 检测死锁并恢复;
  • 资源进行动态分配;
  • 破除上面的四种死锁条件之一。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值