上一篇文章Java进阶——多线程之线程间的通信、同步、等待唤醒机制小结末尾我们留两个问题,这篇文章我们将好好分析出现那个问题的原因和解决之道。
如何停止线程?只有一种方法,就是通过标记,让线程中的run方法自然结束。
当涉及到同步操作时,使用标记可能不能使得线程停止,因为此时可能线程前面已经进入冻结态,那么需要主动调用interrupt方法强制让其停止。
守护线程可以理解成是“后台”线程(与我们常见的前台线程唯一区别就是会随着它“守护”的线程自动结束),必须在启动线程前调用通过线程对象调用setDaemon方法可以把该线程设置为当前运行的线程的守护线程(比如说在main线程里创建了子线程t1,并在main线程里t1.setDaemon(true),则t1是main的守护线程,当main线程运行结束之后t1线程也随之自动结束),当虚拟机此时运行的全是守护线程时,JVM会自动呢退出。所谓“守护”其实更多的像是依赖关系,守护线程依赖于其守护的线程,如果其守护的线程依然运行,那么守护线程与普通线程没啥区别,公平抢夺CPU执行权,但是其守护的线程一旦结束,它也不会独活,它也会自动结束。
Join方法是申请加入到运行中来,相当于是主动抢夺CPU执行权,假如在主线程中创建了一个线程t2并启动,再执行t2.join()方法,当执行到这句时,此时main线程拥有CPU执行权,但是执行了join方法之后相当于是t2 要向main抢夺CPU执行权,main线程进入冻结状态,t2获得CPU执行权,直到t2执行完毕之后,main线程才会重新进入运行态。换言之,当线程t 执行到了线程t3的join方法时,线程t就会等待直到t3 运行结束,t才会重新进入运行态
暂停当前执行的线程,并执行其他线程,执行Thread.yield()方法的线程就会主动放弃当前的CPU执行权,让给其他线程争夺,可以稍微减缓某一个长时间占用CPU执行权。
生产者-消费者模式是一个十分经典的多线程并发协作的模式,所谓生产者-消费者问题,实际上主要是包含了两类线程:一类是生产者线程用于生产数据;另一类是消费者线程用于消费数据。目的是为了解耦生产者和消费者,通常会采用共享的数据区域(就像是一个仓库),生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。通常这个共享数据区域应该满足的线程间并发协作的条件:
而在实现生产者消费者模式时,目前可以采用三种方式:
public class ProduceConsumeModel {
public static void main(String[] args) {
Product res = new Product();
new Thread(new ProduceThread(res)).start();
new Thread(new ConsumeThread(res)).start();
new Thread(new ProduceThread(res)).start();
new Thread(new ConsumeThread(res)).start();
}
}
class Product {
private String name;
private int number = 1;
private boolean flag = false;
public synchronized void produce(String name) {
if (this.flag) {
try {
// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "--" + number++;
System.out.println(Thread.currentThread().getName() + "...生产:"
+ this.name);
flag = true;
this.notify();
}
public synchronized void consume() {
if (!this.flag) {
try {
// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "******消费:"
+ this.name);
flag = false;
this.notify();
}
}
// 生产者
class ProduceThread implements Runnable {
private Product res;// 要操作的资源对象
public ProduceThread(Product res) {
this.res = res;
}
public void run() {
while (true && !Thread.interrupted()) {
res.produce("产品");
}
}
}
// 消费者
class ConsumeThread implements Runnable {
private Product res;// 要操作的资源对象
public ConsumeThread(Product res) {
this.res = res;
}
public void run() {
while (true && !Thread.interrupted()) {
res.consume();
}
}
}
多个线程运行之后的执行结果可能为
从上图部分运行结果我们可以看到,由于同步是可以嵌套的,在多个线程的生产-消费者模型执行时会导致线程不安全。具体原因是什么呢?我们跟着代码逻辑逐步分析下:现在我们创建了t1、t2两个生产者线程和t3、t4两个消费者线程,都是在主线程中依次启动(互相争夺CPU执行权),假设t1 首先得到CPU执行权,执行produce方法,首先判断flag为false,直接给产品赋值,flag设置为true,最后调用notify方法,假如此时t1依然占有CPU执行权,则再次执行produce方法,flag为true 则执行wait方法,主动放弃CPU执行权进入冻结状态(此时produce方法还未执行完毕,虚拟机会记录当前运行的位置),这时候t2、t3、t4抢夺CPU执行权,假设t2抢到了,执行produce方法,flag依然为true,t2也进入冻结状态,这时候t3、t4抢夺CPU执行权,假设t3抢到CPU执行权执行consume 方法,正常消费之后,flag置为false,还调用了notify方法把t1唤醒(t1 仅仅是具备争取CPU执行权的资格)此时CPU执行权还是在t3这,再次执行consume方法,t3 放弃CPU执行权进入冻结态,此时t4、t1抢夺CPU执行权,假如t4抢到了也执行connsume方法,t4也放弃了CPU执行权,t1重新获取CPU执行权,继续之前冻结前的语句往下执行(而不是从produce方法的第一句语句开始执行),给产品赋值,flag设置为true,最后调用notify方法把t2 唤醒,此时t1还是拥有CPU执行权再次从头开始执行produce方法,再次进冻结态,此时t2(因为t3、t4依然处于冻结态)继续之前冻结前的语句往下执行,t2 又生产了一个导致前一个产品被覆盖,t2 接着唤醒t3,t3消费了t2生产的产品,从而导致生产了两次只消费一次的现象,其他现象分析大致相同。原因分析完毕之后,只需要改动两处即可:唤醒所有线程和循环进行条件判断。
package thread;
public class ProduceConsumeModel {
public static void main(String[] args) {
Product res = new Product();
new Thread(new ProduceThread(res)).start();
new Thread(new ConsumeThread(res)).start();
new Thread(new ProduceThread(res)).start();
new Thread(new ConsumeThread(res)).start();
}
}
class Product {
private String name;
private int number = 1;
private boolean flag = false;
public synchronized void produce(String name) {
while (this.flag) {
try {
// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "--" + number++;
System.out.println(Thread.currentThread().getName() + "...生产:"
+ this.name);
flag = true;
this.notifyAll();//这里必须唤醒所有的线程,否则可能会导致所有线程都进入冻结态
}
public synchronized void consume() {
while (!this.flag) {
try {
// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "******消费:"
+ this.name);
flag = false;
this.notifyAll();
}
}
// 生产者
class ProduceThread implements Runnable {
private Product res;// 要操作的资源对象
public ProduceThread(Product res) {
this.res = res;
}
public void run() {
while (true && !Thread.interrupted()) {
res.produce("产品");
}
}
}
// 消费者
class ConsumeThread implements Runnable {
private Product res;// 要操作的资源对象
public ConsumeThread(Product res) {
this.res = res;
}
public void run() {
while (true && !Thread.interrupted()) {
res.consume();
}
}
}
在JDK 1.5之前我们实现生产者-消费者模型只能依靠wait-notify等待唤醒机制,很明显有些弊端:每次都得唤醒等待池中的所有线程,而且超过2个线程还能导致所有线程都进入冻结状态,导致系统全部被挂起(并非死锁),幸亏JDK1.5之后引入了一套更高级些的Lock-Condition机制,Lock-Condition机制一般的使用步骤:
final Lock lock=new ReetrantLock();//1、创建Lock对象
final Condition condition=lock.newCondition();//2、获取Condition对象
lock.lock();//3、获取锁
try{
while(...){
condition.await();//4、当前线程等待
}
...
}finally{
lock.unlock();//5、释放锁
}
JDK1.5之后引入了一套更高级些的Lock-Condition机制为我们多线程同步提供了更优秀的解决方案,其中Lock是一个接口类,比较常见的实现类有ReetrantLock、ReadWriteLock,主要提供了与同步synchronized类似的操作,可以看成使用Lock替换了synchronized,而且一个Lock可以持有多个Condition对象。
方法 | 说明 |
---|---|
void lock() | 获取锁,如果锁不可用,则让当前线程一直处于休眠状态,直到获取锁,相当于是开始进入synchronized 代码块或者synchronized 方法,把这个共享资源显式锁住了 |
Condition newCondition() | 返回绑定到该Lock对象上的Condition实例 |
Void unlock() | 释放锁 |
Condition条件变量或条件队列也是一个接口,将Object的wait、notify、notifyAll方法分解为Condition的操作,以便结合任意Lock对象实现组合使用,同时为每个对象提供多个wait-set,可以看成用于替换Object 部分的wait、notify、notifyAll方法。
方法 | 说明 |
---|---|
void await() | 使得当前线程处于等待状态,通过对应的Condition对象进行等待,只能通过同一个Condition对象signa唤醒 |
void signal() | 唤醒Condition所属Lock的的线程 |
void signalAll() | 唤醒所有等待所属线程 |
package lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProduceConsumeModel2 {
public static void main(String[] args) {
Product res = new Product();
new Thread(new ProduceThread2(res)).start();
new Thread(new ConsumeThread2(res)).start();
new Thread(new ProduceThread2(res)).start();
new Thread(new ConsumeThread2(res)).start();
}
}
class Product {
private String name;
private int number = 1;
private boolean flag = false;
private final Lock lock = new ReentrantLock();
private final Condition produceCondition = lock.newCondition();
private final Condition consumeCondition = lock.newCondition();
public void produce(String name) {
lock.lock();
try {
while (this.flag) {
// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
// /this.wait();
produceCondition.await();
}
this.name = name + "--" + number++;
System.out.println(Thread.currentThread().getName() + "...生产:"
+ this.name);
flag = true;
// /this.notifyAll();
consumeCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (!this.flag) {
// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
// /this.wait();
consumeCondition.await();
}
System.out.println(Thread.currentThread().getName() + "******消费:"
+ this.name);
flag = false;
// /this.notifyAll();
produceCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
// 生产者
class ProduceThread2 implements Runnable {
private Product res;// 要操作的资源对象
public ProduceThread2(Product res) {
this.res = res;
}
public void run() {
while (true && !Thread.interrupted()) {
res.produce("产品");
}
}
}
// 消费者
class ConsumeThread2 implements Runnable {
private Product res;// 要操作的资源对象
public ConsumeThread2(Product res) {
this.res = res;
}
public void run() {
while (true && !Thread.interrupted()) {
res.consume();
}
}
}