14-1,多线程-线程间通信示例
1,什么是多线程间通讯?
多线程通讯:多个线程在处理同一个资源,但是任务不同。
比如说:有一堆煤(资源),一辆卡车向里放煤(Input),一辆卡车向外取煤(output),放煤和取煤的任务不同,但操作的是同一个资源。
由于有两个任务,所以要定义两个run方法。
2,以下面代码说明。
定义name和sex变量,实现交替输出不同信息的功能。定义两个run方法,一个输入名字,另一个打印名字,因为name和sex是资源,将之封装成一个类,两个run方法分别创建一个类,代码如下:
//资源
class Resource {
String name;
String sex;
}
class Input implements Runnable {
Resource r;
//资源是一开始就有的,定义在构造器中
Input(Resource r) {
this.r = r;
}
public void run() {
int x = 0;
while(true) {
//这里会出现安全问题
synchronized(r) {
if(x == 0) {
r.name = "mike";
r.sex = "nan";
} else {
r.name = "丽丽";
r.sex = "女女女女女";
}
}
//实现x在0和1之间的变化,保证两个赋值都能执行到
x = (x + 1) % 2;
}
}
}
class Output implements Runnable {
Resource r;
Output(Resource r) {
this.r = r;
}
public void run() {
while(true) {
synchronized(r) {
//这句是把赋的值输出出来,跟赋值有关,也应该放在同步代码块中
System.out.println(r.name + "..." + r.sex);
}
}
}
}
class ResourceDemo {
public static void main(String[] args) {
//创建资源
/*
创建资源,并用Input和Output传入进去,保证两个run操作的是同一个Resource对象。
并且保证了两个线程用的是同一个锁。
*/
Resource r = new Resource();
//创建任务
Input in = new Input(r);
Output out = new Output(r);
//创建线程
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
//开启线程
t1.start();
t2.start();
}
}
说明:不加同步时可能会出现mike...女女女女女,或,丽丽...nan的情况,因为在进行完一次赋值后,切换到另一种赋值时,如赋了mike...nan,在要赋丽丽,女女女女女时,CPU在只赋了丽丽后,就切走了,这时sex还是nan,所以出现了t2输出丽丽...nan的情况,加入同步代码块就能解决。
锁的问题:如果两个线程不用同一个锁是不能解决问题的,所以在main中创建一个Resource对象,把r传入,这样两个线程就能用同一个锁了。
14-2,线程间通信,等待唤醒机制
1,上一个例子中,我们希望,输出完mike...nan后就输出丽丽...女女女女女,再输出mike...nan这样。
2,等待,唤醒机制
涉及的方法:
(1)wait():让线程处于冻结状态,被wait的线程会被存储在线程池中。
(2)notify():唤醒线程池中的一个线程(任意)。
(3)notifyAll():唤醒线程池中的所有线程。
这些方法都必须被定义在同步中,因为这些方法是用于操作线程状态的方法,必须要明确操作的是哪个锁上的线程。
3,为什么操作线程的方法wait,notify,notifyAll定义在了Object中?
因为这些方法是监视器的方法(锁),监视器其实就是锁,锁可以是任意对象,任意对象调用的方法一定定义在Object中。
4,以代码说明14-1中程序实现1中的需求。
//资源
class Resource {
String name;
String sex;
//用于判断name和sex的值是否为空
boolean flag = false;
}
class Input implements Runnable {
Resource r;
//资源是一开始就有的,定义在构造器中
Input(Resource r) {
this.r = r;
}
public void run() {
int x = 0;
while(true) {
//这里会出现安全问题
synchronized(r) {
//第一次为false,不执行
if(r.flag) {
try{
r.wait();
} catch(InterruptedException e) {}
}
if(x == 0) {
r.name = "mike";
r.sex = "nan";
} else {
r.name = "丽丽";
r.sex = "女女女女女";
}
//name和sex已经赋值,不为空,置为true,t2可以从中取值输出
r.flag = true;
//唤醒t2线程
r.notify();
}
//实现x在0和1之间的变化,保证两个赋值都能执行到
x = (x + 1) % 2;
}
}
}
class Output implements Runnable {
Resource r;
Output(Resource r) {
this.r = r;
}
public void run() {
while(true) {
synchronized(r) {
//flag已经是true,!flag是false,第一次不执行
if(!r.flag) {
try{
r.wait();
}catch(InterruptedException e) {}
}
//这句是把赋的值输出出来,跟赋值有关,也应该放在同步代码块中
System.out.println(r.name + "..." + r.sex);
//输出一次把flag置为flase,防止继续输出mike...nan,
//因为若t2继续拿着执行权,!flag为true,t2会被wait。
r.flag = false;
r.notify();
}
}
}
}
class ResourceDemo {
public static void main(String[] args) {
//创建资源
/*
创建资源,并用Input和Output传入进去,保证两个run操作的是同一个Resource对象。
并且保证了两个线程用的是同一个锁。
*/
Resource r = new Resource();
//创建任务
Input in = new Input(r);
Output out = new Output(r);
//创建线程
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
//开启线程
t1.start();
t2.start();
}
}
说明:第一次执行:r.flag为false,不被wait,进行赋值,mike,nan,再把flag置为true,若此时t1继续拿着执行权,判断if(r.flag)为true,执行r.wait,t1被冻结,同时唤醒了t2(t2不被冻结也可以被唤醒),这时只能执行t2,这时if(!r.flag)为false,不执行,执行输出mike...nan,再把flag置为false,并唤醒t1,这时就算t2拿着执行权,if(!r.flag)为true,t2会被wait,只能执行了t1,t1这时x=1,赋值为丽丽,女女女女女,这时在重复上面的步骤,实现了1中的需求。
14-3,线程间通信-等待唤醒机制-代码优化
上例的代码优化:
class Resource {
//为了保护成员变量,使其私有化,并提供public方法将其对外公开
private String name;
private String sex;
private boolean flag = false;
//同步函数解决线程安全问题
public synchronized void set(String name,String sex) {
if(flag) {
try{
this.wait();
}catch(InterruptedException e) {}
}
//下面的程序调用此函数向里传值,进行赋值操作
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out() {
if(!flag) {
try{
this.wait();
}catch(InterruptedException e) {}
}
System.out.println(name + "..." + sex);
flag = false;
notify();
}
}
class Input implements Runnable {
Resource r;
Input(Resource r) {
this.r = r;
}
public void run() {
int x = 0;
while(true) {
if(x == 0) {
//调用set方法赋值
r.set("mike","nan")
} else {
r.set("丽丽","女女女女女");
}
x = (x + 1) % 2;
}
}
}
class Output implements Runnable {
Resource r;
Output(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.out();
}
}
}
class ResourceDemo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
14-4,线程间通信-多生产者多消费者问题
代码说明:
生产者,消费者,比如说生产烤鸭,消费烤鸭。
单一的生产者和消费者用上例的模式就可以解决,若是多个生产者和多个消费者,则要用下列程序解决。
/*
if判断标记,只有一次,会导致不该运行的线程运行了,出现了数据错误的情况。
while判断标记,解决了线程获取执行权后,是否要运行。
notify只能唤醒一个线程,如果本方唤醒了本方,没有意义。而且while判断标记+notify会导致死锁。
notifyAll解决了本方线程一定会唤醒对方线程的问题。
*/
/*
问题描述:有多个烤鸭店和多个消费者,任何一个烤鸭店生产好一只烤鸭,其中一个消费者就会消费烤鸭。
*/
class Resource {
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name) {
/*
如果这里用if,t0如果挂在try上,再被唤醒将不再进行if判断,若为while,t0如果挂在try上,
再被唤醒将继续判断是否成立。
*/
while (flag) {
try {
this.wait();
} catch(InterruptedException e) {}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "..生产者.." + this.name);
flag = true;
/*
notify的情况下,若t1,t2,t3都挂了,t0有执行权,那么他可能唤醒t1,这时只有生产者没有消费者。
*/
notifyAll();
}
public synchronized void out() {
while(!flag) {
try {
this.wait();
}catch(InterruptedException e) {}
}
System.out.println(Thread.currentThread().getName() + "..消费者.." + this.name);
flag = false;
notifyAll();
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.set("烤鸭");
}
}
}
class Consumer implements Runnable {
private Sesource r;
Consumer(Resource r) {
this.r = r;
}
public void run() {
while(true) {
r.out();
}
}
}
class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new 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();
}
}
说明:
if和notify的情况:
to拿到执行权,if(flag)为假,生产烤鸭1,count为2,置flag为true,notify一次,若t0继续拿执行权,if(flag)为true,被wait,t1执行,if(flag)为true,被wait,t2执行,if(!flag)为假,消费了烤鸭1,置flag为false,唤醒t0,t1中的一个,假设t0被唤醒,t3执行,if(!flag)为true,被wait(),t2执行,if(!flag)为true,也被wait(),这时活着的线程只有t0,t0执行,此时不再判断if,生产了烤鸭2,count为3,flag为true,notify随机唤醒一个,若唤醒了t1,t1也不判断if,生产了烤鸭3,若唤醒t2,t2消费了烤鸭3,这时烤鸭2未被消费,此时烤鸭2没有消费者,就出现了生产出的烤鸭无人消费的问题。
结果打印如:
生产烤鸭1
...消费烤鸭1
生产烤鸭2 //烤鸭2没有被消费
生产烤鸭3
...消费烤鸭3
while和notify的情况:
继续上面的说法,此时t1,t2,t3被wait,只有t0活着,flag为false,t0被唤醒后,需要判断while(flag),为假,生产烤鸭4,置flag为true,t0唤醒t1,t0继续执行,while(flag)为true,t0被wait,t1被唤醒后也判断while(flag)为true,被wait,这时4个线程都被wait了,造成死锁。
While和notifyAll解决了问题,但效率降低了,为此,JDK1.5提供了解决方案。
14-5,多生产者多消费者问题-JDK1.5性特性-Lock
同步代码块对于锁的操作时隐式的。
JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock接口在java.util.concurrent.locks包中,用Lock需要导入java.util.concurrent.locks.*;
标准写法:
//Lock是接口,ReentrantLock是Lock的子类,实现了Lock
Lock lock = new ReentrantLock();
public void show() {
lock.lock();
try {
//代码中可能会抛出异常,用try处理,抛出异常后会导致程序跳转,这样就不能释放锁了
//释放锁是必须执行的,所以放在finally内。
code...
}finally {
//释放锁
lock.unlock();
}
}
14-6,JDK1.5新特性-Condition
1,Lock接口:替代了同步代码或者同步函数,将同步的隐式锁操作变成显示所操作,同时更为灵活,可以在一个锁上加多组监视器。
lock()方法获取锁。
unlock()方法释放锁,通常定义在finally代码块中。
2,Condition接口:替代了Object中的wait(),notify(),notifyAll()方法,将这些监视器方法单独进行了封装,变成Condition监视器对象,可以与任意锁进行组合。
await():-->wait();
signal();-->notify();
signalAll();-->notifyAll();
3,实现机制对比:
旧方法:
class Object {
wait();
notify();
notifyAll();
}
class Demo extends Object {}
...
Demo d = new Demo();
synchronized(d){
d.wait();
}
一个锁只能实现一组wait,notify,notifyAll方法。
JDK1.5新特性:
interface Condition {
await();
signal();
signalAll();
}
Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
可以创建多个Condition对象,实现多组Condition中的await,signal,signalAll。
4,14-4中的代码可以写为:
//创建一个锁对象
Lock lock = new ReentrantLock();
//通过已有的锁获取该锁上的监视器对象
Condition con = lock.newCondition();
public voidset(String name) {
//获取锁
lock.lock();
try {
while(true) {
try {
//调用监视器的await方法
con.awati();
}catch(InterrputedException e) {}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"生产者5.0"+this.name);
flag = true;
//调用监视器的signalAll方法
con.signalAll();
} finally {
//释放锁
lock.unlock();
}
}
14-7,解决方案
1,在14-4的程序中的Resource类改为。
import java.util.concurrent.locks.*;
class Resource {
private String name;
private int count = 1;
private boolean flag = false;
//创建锁对象
Lock lock = new ReentrantLock();
//通过已经有的锁获取两组监视器,一组监视生产者,一组监视消费者
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
public void set(String name) {
lock.lock();
try {
while(true) {
try {
producer_con.await();
}catch(InterruputedException e) {}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName());
flag = true;
//用消费者监视器唤醒一个消费者线程
consumer_con.signal();
}finally {
lock.unlock();
}
}
public void out() {
lock.lock();
try{
while(!flag) {
try {
consumer_con.swait();
}catch(InterruptedException e) {}
}
flag = false;
//用生产者监视器唤醒一个生产者线程
producer_con.signal();
}finally {
lock.unlock();
}
}
}
2,图示
以前的锁:三个方法能操作线程池中的所有线程,但只有一组监听器。
现在的锁:三个方法分别操作两个监听器中的对象。
14-8,JDK1.5解决方法—范例
这是JDK API文档中Condition接口中给出的范例:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
14-9,多线程-wait和sleep的区别
1,wait和sleep的区别?
(1)wait可以指定时间也可以不指定。
sleep必须执行时间。
(2)在同步中时,对CPU的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。
2,示例:
class Demo {
void show() {
synchronized(this) {
wait();//t0,t1,t2都挂在这里
}
}
void method() {
synchronized(this) { //t4拿执行权
notifyAll();//唤醒全部,t0,t1,t2
}//t4结束
}
}
这时t0,t1,t2都已经进入到同步内,t0,t1,t2都有执行资格,但t4释放锁后,只有一个线程拿到锁,所以还能保证同步性。
14-10,停止线程的方式-定义标记
1,停止线程
(1)stop方法(已过时,不可用)
(2)run方法结束。
如何控制线程的任务结束呢?
任务中一般都会有循环结构,只要控制住循环就可以结束任务,控制循环结束通常以定义标记的形式完成。如:
class StopThread implements Runnable {
private boolean flag = true;
public void run() {
while(flag) {
System.out.println(Thread.currentThread().getName()+"....");
}
}
public void setFlag() {
flag = false;
}
}
class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 1;
for(;;) {
//定义循环结束条件
if(++num == 50) {
/*
如果num=50,则把flag置为false,并退出无限循环。
将flag置为false,则run方法中的while(flag)为假,不再执行。
实现了用标记结束run方法。
*/
st.setFlag();
break;
}
System.out.println("main..." + num);
}
System.out.println("over");
}
}
14-11,停止线程的方式-Interrupt
1,如果线程处于了冻结状态,无法读取标记,该如何结束呢?
可以使用Interrupt方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格。Interrupt方法为强制动作,会发生InterruptedException异常,记得要处理。
例如:
class StopThread implements Runnable {
private boolean flag = true;
public synchronized void run() {
while(flag) {
//产生InterruptException异常,用try-catch处理
try {
//t1,t2进入后会被wait,用后面的interrupt方法中断wait,
//是t1,t2可以继续执行输出,并把标记改为false,是线程结束。
wait();
}catch(InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"..."+e);
flag = false;
}
System.out.println(Thread.currentThread().getName() + ".....");
}
}
public void setFlag() {
flag = false;
}
}
class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 1;
for(;;) {
if(++num == 50) {
//interrupt方法中断t1,t2的wait状态。
t1.interrupt();
t2.interrupt();
break;
}
System.out.println("main ... " + num);
}
System.out.println("over");
}
}
14-12,守护线程-setDeamon
1,守护线程setDeamon:将该线程标记为守护或用户线程,当正在运行的线程都是守护线程时,Java虚拟机退出。
可将守护线程理解为后台线程,后台线程依附于前台线程,前台线程结束后,后台线程也自动结束。
2,如上例中,把t2.interrupt()注释掉,这样只有t1继续执行并且t1线程结束,t2结束不了,则整个进程结束不了,若在t2.start()上方加上一句:t2.setDeamon(),则t2变为后台线程,当主线程与t1线程都结束时,t2也会随之结束。
setDeamon方法必须在启动线程前调用。
14-13,其他方法-join等
1,join方法:等待该线程结束。
如:
t1.start();
t1.join(); //从main得到执行权,等到t1执行完,t2和main在执行。
t2.start();
若把t1.join()放在t2.start()下面,则main不执行,t2和t1随机执行,main只等t1结束后就开始执行,跟t2没有关系。
什么时候用join?
在临时加入一个线程运算时可以使用join方法。
2,优先级
Thread类中有toString()方法,返回线程名字,优先级和线程组。
线程的优先级是指线程被CPU执行的机率,值越高,机率越大,范围是1-10。
Thread中有三个字段:
staticint MAX_PRIORITY;值为10
staticint MIN_PRIORITY;值为1
staticint NORM_PRIORITY;值为5
如:
将t1的优先级设置为10可以这么写:
t1.setPriority(Thread.MAX_PRIORITY);
3,线程组:把线程进行组的划分。
若要对一组线程进行某种统一的操作,可将这组线程加入线程组(ThreadGroup)。
4,yield()方法,临时暂停线程使用。