多线程(二)
一、线程间通信
1.定义
线程间通信就是多个线程操作同一资源,但是操作的动作不同。
2.等待唤醒机制
等待唤醒机制,是由wait()
,notify()
或notifyAll()
等方法组成。对于有些资源的操作,需要一个线程完成一步,进入等待状态,将CPU执行权交由另一个线程,让它完成下一步的操作,如此交替进行。这个过程中,一个线程需要在完成一步操作后,先通知(notify()
)另一个线程运行,再等待(wait()
),进入冻结状态,以此类推。等待中的线程,都储存在系统线程池中,等待这被notify()
唤醒。
以下代码,通过等待唤醒机制,实现了生产一个披萨,消费一个披萨:
package com.heisejiuhuche;
public class ProductionConsumptionModel {
public static void main(String[] args) {
Pizza pizza = new Pizza();
new Thread(new Producer(pizza)).start();
new Thread(new Consumer(pizza)).start();
}
}
class Pizza {
private String pizza;
private int count = 1;
//包子存在与否的旗标,false代表没有pizza,true代表有
private boolean flag = false;
public synchronized void producePizza(String pizza) {
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.pizza = pizza + "---" + count++;
System.out.println(Thread.currentThread().getName() + "-生产-----"
+ this.pizza);
flag = true;
notify();
}
public synchronized void consumePizza() {
//如果没有pizza,则执行生产包子的代码
if (!flag) {
try {
//如果有pizza,则线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "-消费--------------" + this.pizza);
//如果没有pizza,生产完之后,将flag设为true
flag = false;
//线程进入冻结状态之前,通知另一线程开始启动,消费pizza
notify();
}
}
class Producer implements Runnable {
private Pizza pizza;
Producer(Pizza pizza) {
this.pizza = pizza;
}
public void run() {
while (true) {
pizza.producePizza("pizza");
}
}
}
class Consumer implements Runnable {
private Pizza pizza;
Consumer(Pizza pizza) {
this.pizza = pizza;
}
public void run() {
while (true) {
pizza.consumePizza();
}
}
}
程序运行部分结果如下:
Thread-1-消费--------------pizza---20609
Thread-0-生产-----pizza---20610
Thread-1-消费--------------pizza---20610
Thread-0-生产-----pizza---20611
Thread-1-消费--------------pizza---20611
3.Object类中的wait等方法wait()
等多线程同步等待唤醒机制中的方法,被定义在Object
类中是因为:
首先,在等待唤醒机制中,无论是等待操作,还是唤醒操作,都必须标识出等待的这个线程和被唤醒的这个线程锁持有的锁;表现为代码是:锁.wait()
;锁.notify()
;而这个锁,由synchronized
关键字格式可知,可以是任意对象;那么,可以被任意对象调用的方法,一定是定义在了Object
类当中。wait()
,notify()
,notifyAll()
这些方法都被定义在了Object
类中,因为这些方法是要使用在多线程同步的等待唤醒机制当中,必须具备能被任意对象调用的特性。所以,这些方法要被定义在Object
类中。
4、生产者消费者模型
在实际生产时,会有多个线程负责生产,多个线程负责消费;那么在上述代码中启动新线程,来模拟多线程生产消费的情况。
示例代码:
package com.heisejiuhuche;
public class ProductionConsumptionModel {
public static void main(String[] args) {
Pizza pizza = new Pizza();
//两个线程负责生产,两个线程负责消费
new Thread(new Producer(pizza)).start();
new Thread(new Producer(pizza)).start();
new Thread(new Consumer(pizza)).start();
new Thread(new Consumer(pizza)).start();
}
}
用这样的方式,运行会出现如下结果:
Thread-0-生产-----pizza---198
Thread-1-生产-----pizza---199
Thread-2-消费--------------pizza---199
生产了两个披萨,但只消费了一个。现在0,1线程负责生产,2,3线程负责消费,原因推断:
1)当0线程生产完一个披萨,进入冻结;
2)1线程判断有披萨,进入冻结;
3)2线程消费一个披萨,唤醒0线程,进入冻结;
4)3线程判断没披萨,进入冻结;
5)现在出于运行状态的只有0线程,0线程生产一个披萨,唤醒1线程(1线程是线程池中第一个线程),进入冻结;
6)1线程又生产了一个披萨
这导致了生产两个,只消费一个的问题。这个问题的发生是因为,第5
步0
线程唤醒1
线程的时候,由于1
线程的等待代码在if
语句中,1
线程醒了之后,不需要再判断flag
的值所导致。如果1
线程被唤醒,还要继续判断flag的值,就不会产生这个情况。因此,要将if判断,改为while
循环,让线程被唤醒之后,再次判断flag
的值。
示例代码:
while (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
每次被唤醒,都要判断flag的值。代码运行结果如下:
Thread-0-生产-----pizza---1
Thread-2-消费--------------pizza---1
Thread-0-生产-----pizza---2
Thread-3-消费--------------pizza---2
程序出现了无响应,因为使用while
循环,可能会出现所有线程全部进入冻结状态的情况。要解决这个问题,必须用到另一个方法notifyAll();
唤醒所有线程。由于用了while
循环,所有线程被唤醒之后第一件事是判断flag
的值,所以不会再出现多生产或多消费问题。至此,程序运行正常。
示例代码:
public synchronized void consumePizza() {
while(!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "-消费--------------" + this.pizza);
//如果没有pizza,生产完之后,将flag设为true
flag = false;
//线程进入冻结状态之前,唤醒所有其他线程
notifyAll();
}
}
程序运行部分结果:
Thread-2-消费--------------pizza---198
Thread-0-生产-----pizza---199
Thread-3-消费--------------pizza---199
Thread-1-生产-----pizza---200
Thread-3-消费--------------pizza---200
二、jdk5新特性
1.概述
jdk5开始,提供了多线程同步的升级解决方案。将synchronized
关键字,替换成Lock接口
;将Object
对象,替换为Condition对象
;将wai()
,notify()
,notifyAll()
方法,替换为await()
,signal()
,signalAll()
方法。一个锁,可以对应多个Condition对象
。这个特性的出现,可以让多线程在唤醒其他线程时,不必唤醒本方的线程,只唤醒对方线程。例如在生产者消费者模型中,使用Lock
和Condition
类,可以实现只唤醒消费者线程,或只唤醒生产者线程。
2.Lock接口和Condition接口
1)Lock接口已知实现类中,有ReentrantLock类。这个子类可以用来实例化,创建ReentrantLock对象
ReentrantLock lock = new ReentrantLock();
2)Condition接口的实例可以通过newCondition()方法获得
Condition conditon = Lock.newCondition();
3)一个Lock对象可以对应多个Condition对象
Condition condition1 = Lock.newCondition();
Condition condition2 = Lock.newCondition();
3.新特性应用
将此新特性应用在消费者生产者模型中,实现只唤醒对方线程。
修改之后的Pizza类代码如下:
class Pizza {
private String pizza;
private int count = 1;
private boolean flag = false;
//获取Lock和Condition对象
private final ReentrantLock lock = new ReentrantLock();
//分别指定生产者和消费者的Condition对象
private final Condition conditionPro = lock.newCondition();
private final Condition conditionCon = lock.newCondition();
public void producePizza(String pizza) {
//上锁
lock.lock();
try {
while (flag) {
//如果有披萨,线程冻结
conditionPro.await();
}
this.pizza = pizza + "---" + count++;
System.out.println(Thread.currentThread().getName() + "-生产-----"
+ this.pizza);
flag = true;
//只唤醒消费者线程中的一个
conditionCon.signal();
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
//这是一定要执行的代码,解锁
lock.unlock();
}
}
public void consumePizza() {
lock.lock();
try {
while(!flag) {
conditionCon.await();
}
System.out.println(Thread.currentThread().getName()
+ "-消费--------------" + this.pizza);
flag = false;
conditionPro.signal();
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
分别创建的conditionPro
和conditionCon
对象,用于实现只唤醒对方线程,代码更优。
三、停止线程
1.线程停止原理stop()
方法已经过时,停止的唯一标准就是run()
方法结束。开启多线程运行,运行代码通常都是循环结构,只要控制住循环,就可以让run()
方法结束,就可以让线程结束。
注意:
当线程处于冻结状态,无法读取控制循环的标记,线程就不会结束。
2.interrupt()方法
将处于冻结状态的线程,强制恢复到运行状态。interrupt()
方法是在清除线程的冻结状态。
示例代码:
package com.heisejiuhuche;
public class InterruptTest {
public static void main(String[] args) {
int x = 0;
Interrupt inter = new Interrupt();
Thread t1 = new Thread(inter);
Thread t2 = new Thread(inter);
t1.start();
t2.start();
while(true) {
System.out.println(Thread.currentThread().getName() + "run....");
if(x++ == 60) {
//强制t1 t2恢复运行状态,抛出异常
t1.interrupt();
t2.interrupt();
break;
}
}
System.out.println("over");
}
}
class Interrupt implements Runnable {
//循环控制变量
private boolean flag = true;
public synchronized void run() {
while(flag) {
try {
//让t1 t2进入冻结状态
this.wait();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "Interrupt Exception.....");
//处理完异常,改变flag的值,下次判断时,结束循环
changeFlag();
}
System.out.println(Thread.currentThread().getName() + " Interrupt run.....");
}
}
public void changeFlag() {
flag = false;
}
}
如果不调用t1
和t2
线程的interrupt()
方法,程序会无响应,因为两个线程都处于冻结状态,无法继续运行。
上述程序运行结果:
mainrun....
over
Thread-1Interrupt Exception.....
Thread-1 Interrupt run.....
Thread-0Interrupt Exception.....
Thread-0 Interrupt run.....
四、Thread类其他方法
1.setDaemon()方法
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。该方法必须在启动线程前调用。守护线程可以理解为后台线程。后台线程开启后,会和前台线程(一般线程)一起抢夺CPU资源;当所有前台线程结束运行后,后台线程自动结束。可以理解为,后台线程依赖前台线程的运行。
示例代码:
package com.heisejiuhuche;
public class InterruptTest {
public static void main(String[] args) {
int x = 0;
Interrupt inter = new Interrupt();
Thread t1 = new Thread(inter);
Thread t2 = new Thread(inter);
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
}
在启动两个线程前,将两个线程设置为守护线程,其他代码不变;那么这两个线程依赖主线程运行;虽然这两个线程都处于冻结状态,但是当主线程运行完毕,这两个守护进程随之结束。
2.join()方法
调用join()
方法的线程,在申请CPU执行权。之前拥有CPU执行权的线程,将转入冻结状态,等调用join()
方法的线程执行完毕,再转回运行状态。
示例代码:
package com.heisejiuhuche;
public class JoinTest {
public static void main(String[] args) {
Join j = new Join();
Thread t1 = new Thread(j);
Thread t2 = new Thread(j);
t1.start();
try {
//主线程将CPU执行权交给t1线程,自己转入冻结
//等待t1线程执行完毕,主线程再运行
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "***" + x);
}
}
}
class Join implements Runnable {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "---" + x);
}
}
}
程序在启动t1
线程之后,主线程先等待t1
线程打印完100
个数;主线程再继续和t2
线程交替打印100
个数。
3.yield()方法
调用yield()
方法的线程,会临时释放执行权,可以达到线程均衡运行的效果。
示例代码:
package com.heisejiuhuche;
public class YieldTest {
public static void main(String[] args) {
Yield j = new Yield();
Thread t1 = new Thread(j);
Thread t2 = new Thread(j);
t1.start();
t2.start();
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "***" + x);
}
}
}
class Yield implements Runnable {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "---" + x);
Thread.yield();
}
}
}
程序运行部分结果:
Thread-1---51
main***79
Thread-0---46
main***80
Thread-1---52
main***81
Thread-0---47
三个线程均衡执行。
五、多线程开发应用
多线程应用在程序中的运算需要同时进行的时候,可以提高程序运行的效率。例如,main()
方法中有三个循环需要执行,如果是单线程,第二个循环要等待第一个循环执行完才能执行,第三个循环要等第二个循环执行完,如此一来,程序运行效率低下。此时,就可以运用多线程,让三个循环同时运行。
示例代码:
package com.heisejiuhuche;
public class ThreadApplycation {
public static void main(String[] args) {
//主线程执行
for(int x = 0; x < 100; x ++) {
System.out.println("Main thread running ...");
}
//匿名线程执行
new Thread() {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println("Anonymous thread running ...");
}
}
}.start();
//线程r执行
Runnable r = new Runnable() {
public void run() {
for(int x = 0; x < 100; x ++) {
System.out.println("r thread running ...");
}
}
};
new Thread(r).start();
}
}
让主线程,匿名线程和r
线程,同时开始运算。