Java基础14--多线程

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()方法,临时暂停线程使用。

 

你可能感兴趣的:(java,多线程,基础,线程间通信)