线程通信
- 应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,到仓库中再次放入产品为止.
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 }
线程池
合理利用线程池能够带来三个好处。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
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方法等待执行结果。线程池实现原理类结构图如下:
由上图可知,ExecutorService是Java中对线程池定义的一个接口,它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)处理死锁的方法
- 忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;
- 检测死锁并恢复;
- 资源进行动态分配;
- 破除上面的四种死锁条件之一。