黑马毕向东Java课程笔记(day12-1):多线程(第二部分)——线程间通信既相应机制(等待唤醒、停止线程、守护线程、join()方法、开发多线程代码写法、线程池)

本文深入探讨Java多线程的通信与管理,包括线程间等待唤醒机制、生产者消费者模型、停止线程、守护线程、join()方法的运用、线程优先级与yield()方法,以及开发多线程代码的最佳实践和线程池的原理及优势。通过示例代码详细阐述各种机制的实现与应用。
摘要由CSDN通过智能技术生成

1、线程间的通信示例代码
  什么是线程间通信?其实就是多个线程在操作同一个资源,但是操作的动作不同。(见视频12-1开始处解释)
  我们先看第一个例子,不同步容易出现错误

/*
线程间通信
 */
package pack;

//创建一个资源类,用于存放共享操作资源
class Res
{
	String name;
	String sex;
}

//输入类实现Runnable
class Input implements Runnable
{
	//可以在这里创建一个Res对象,用于调用name与sex,但是下面的Output类也需要创建Res对象,输入输出操作的是不同对象,这是2个资源!
	//我们想要只有一个Res对象,因此我们在Input与Output内只创建一个Res类的引用,通过构造方法为Res引用赋予对象。
	//然后在main中只创建一个Res的对象,在Input与Output创建对象时传入其参数即可!(特别注意这种方法的应用!很多地方会用到!)
	private Res r;
	Input(Res r)
	{
		this.r = r;
	}
	//输入的run()方法为name,sex赋值
	public void run()
	{
		int x = 0;//设置x来控制赋不同的值
		while(true)//设置循环使其不断赋值
		{
			if(x == 0)
			{
				r.name="mike";
				r.sex="man";
			}
			else
			{
				r.name="丽丽";
				r.sex = "女女女女女";
			}
			x = (x+1)%2;//用这个公式来切换x的值,一直在0/1之间切换,这样每一个循环就会输出不一样的name与sex值
		}
	}
}

//输出类实现Runnable
class Output implements Runnable
{
	private Res r;
	Output(Res r)
	{
		this.r = r;
	}
	//Output方法用于输出
	public void run()
	{
		while(true)//循环打印
		{
			System.out.println(r.name+"...."+r.sex);
		}
	}
}

public class ThreadDemo
{
	public static void main(String[] args) {
		Res r = new Res();//创建唯一的Res对象s,将其赋予Input类与output类
		
		Input in = new Input(r);
		Output out = new Output(r);
		
		//这两个线程使用的是同一个Res对象,因此其操作的是同一批
		Thread t1 = new Thread(in);//创建一个线程,将输入类的对象赋予它,用于操作输入类的run()方法
		Thread t2 = new Thread(out);//创建另一个线程,将输出类的对象赋予它,用于操作输出类的run()方法

		t1.start();
		t2.start();
		
	}
}
/*
结果:出现如下错误(见视频12-1,14.14开始的分析)
丽丽....man
mike....女女女女女
name和sex是共享数据,而且被多条语句所操作(同步2条件),那么他们在被2个线程操作的时候,不加同步容易出错
(在一个线程中被赋予姓名,但是没有赋予性别,另一个线程就输出了!)
*/

  我们将上面代码设置同步,修改后如下:

package pack;

class Res
{
	String name;
	String sex;
}

class Input implements Runnable
{
	private Res r;
	Input(Res r)
	{
		this.r = r;
	}
	public void run()
	{//不要在函数出设置同步函数或者在while外面设置同步代码块,这样变成单线程,因为某一个线程进来会一直在while里面循环,解不开锁出不去,其他线程无法进行。
		int x = 0;
		while(true)
		{//必须把多线程写在while里面
//同步的前提是有多个同步的线程,且多个线程使用同一个锁。这里2个线程操作对共享资源的操作代码不一样,因此只有Res的对象r才是2个类中唯一相同的对象
		//因此这里的对象锁用r(当然使用各类的字节码文件对象Input.class,Output.class,ThreadDemo.class,Res.class都可以)
			synchronized(r)
			{
				if(x == 0)
				{
					r.name="mike";
					r.sex="man";
				}
				else
				{
					r.name="丽丽";
					r.sex = "女女女女女";
				}
				x = (x+1)%2;
			}
		}
	}
}

class Output implements Runnable
{
	private Res r;
	Output(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			synchronized(r)
			{//虽然2个线程的代码在2个run()方法中(代码不同),但是他们在操作同一个资源,那么这些操作共享资源的代码应该使用同一个对象锁同步起来。
				System.out.println(r.name+"...."+r.sex);
			}
		}
	}
}

public class ThreadDemo
{
	public static void main(String[] args) {
		Res r = new Res();
		
		Input in = new Input(r);
		Output out = new Output(r);
		
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);

		t1.start();
		t2.start();
		
	}
}

2、线程间通信——等待唤醒机制
  先看例子1

/*
 由于Input线程可能持有CPU执行权就一直赋值,等到Output线程持有CPU执行权就一直输出,再切换到Input线程,
 这样就造成一大片一大片地输出,而我们想实现赋值一个就输出一个的功能。
 我们利用等待唤醒机制,具体操作如下代码(解释见视频12-3开始处解释!)
 
 过程:首先,flag为false,那么Output线程等待,即使其获得CPU执行权也没办法运行。如果Input线程获取CPU执行权,它会执行一次赋值,
 赋值结束后,将flag赋值为true,并执行notify()唤醒Output线程。这个时候如果Input线程还有CPU执行权,运行发现flag为true
 会冻结Input线程,即使其获得CPU执行权也没办法运行。如果Output线程获取CPU执行权,它会执行一次输出,输出结束后,将flag赋值为false,
 并执行notify()唤醒Input线程。这个时候如果Output线程还有CPU执行权,运行发现flag为false, 会冻结Output线程,即使其获得CPU执行权也没办法运行。
 就这样循环往复,使得每一个线程只能执行一次,另一个线程如果不执行它就没办法再执行!
 
 wait()的时候,线程在哪里呢?线程运行的时候,内存中会建立一个线程池,等待线程都存在线程池当中。当我们notify的时候,
 就会唤醒线程池中的线程。如果线程池中有很多线程,一notify,通常会唤醒第一个等待的,因为之前是按顺序往线程池存储线程,
 因此唤醒一般也按顺序唤醒。
 */
package pack;

class Res
{
	String name;
	String sex;
	boolean flag = false;//设置一个标准,赋值为false。设置在Res类是为了方便调用
}

class Input implements Runnable
{
	private Res r;
	Input(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{
			synchronized(r)
			{
				//首先,如果flag为真的话,这个时候Input的线程进入冻结状态,线程放弃运行资格,即使有CPU的执行权,该线程也不执行
				//这个时候Input线程没办法赋值,只有等OutPut把上一次赋值的结果输出完,并用notify()唤醒,Input线程才会复苏
				if(r.flag)//我们用线程锁r来调用wait(),这样另一个线程代码的notify()才会对应该wait(),才可以将其唤醒
					try {r.wait();}catch(Exception e) {}//wait()会抛出异常,因此需要捕捉处理;
				if(x == 0)
				{
					r.name="mike";
					r.sex="man";
				}
				else
				{
					r.name="丽丽";
					r.sex = "女女女女女";
				}
				x = (x+1)%2;
				r.flag = true;//这个Input线程运行完就将flag赋值true,使得这个Input线程冻结,另一个Output线程打开
				r.notify();//同样用r调用notify(),这样这个notify()才能唤醒同一个对象锁下另一个线程代码的wait()方法
			}
		}
	}
}

class Output implements Runnable
{
	private Res r;
	Output(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			synchronized(r)
			{
				if(!r.flag)
					try {r.wait();}catch(Exception e) {}
				System.out.println(r.name+"...."+r.sex);
				r.flag = false;//这个线程运行完就将flag赋值false,使得这个Output线程冻结,另一个Input线程打开
				r.notify();
			}
		}
	}
}

public class ThreadDemo
{
	public static void main(String[] args) {
		Res r = new Res();
		
		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....man
丽丽....女女女女女
而且不会有一大片相同的内容打印,说明是赋值一次就打印一次,2个线程之间相互唤醒!
*/

//notifyAll();唤醒所有线程

/*
wait:public final void wait() throws InterruptedException:使用的时候必须要标识处wait()所属线程的锁!比如此处r.wait()
notify();
notifyAll();

都使用在同步中,因为要对持有监视器(锁/对象锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁(监视器)。

为什么这些操作线程的方法要定义Object类中呢?
因为这些方法在操作同步中线程时,都必须要标识它们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。
不可以对不同锁中的线程进行唤醒,也就是说,等待和唤醒必须是同一个锁。
而锁可以是任意对象,而锁可以调用wait()\nitify()\notifyAll(),所以可以被任意对象调用的方法定义Object类中。
*/

  我们对上面的代码进行优化:我们

/*
 我们将代码几句缩短为一句,并将同步的设置调整到Res类下的set()与out()方法。
 同时wait()、notify()方法与flag的操作移动到set()/out()方法,这样,代码模块化变得更加整洁易看!
 */
package pack;

class Res
{
	//我们在设置变量的时候一帮要把变量私有化并向外提供设置与获取变量的方法
	private String name;
	private String sex;
	private boolean flag = false;
	
	//我们发现将变量私有化并提供给同步代码块使用的话,会较为繁琐,我们干脆将同步代码块部分设置为同步函数,并放在Res中,给run()方法调用。
	//若将同步设置在函数处,wait方法等也必须移动到函数内部。在Res中,对象锁变为this。
	
	//提供外部设置的方法,我们只要在外部调用set方法,再调用out打印方法即可
	public synchronized void set(String name,String sex)
	{
		if(flag)
			try {this.wait();}catch(Exception e) {}
		this.name = name;
		this.sex = sex;
		flag = true;
		this.notify();
	}
	//提供外部打印的方法
	public synchronized void out()
	{
		if(!flag)
			try {this.wait();}catch(Exception e) {}
		System.out.println(name+"........"+sex);
		flag = false;
		this.notify();
	}
}

class Input implements Runnable
{
	private Res r;
	Input(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{//我们将多线程操作的数据——name,sex.flag封装到同步函数里面就可以,其他的不需要封装到同步函数中
			if(x == 0)
				r.set("mike","man");
			else
				r.set("丽丽","女女女女女");
			x = (x+1)%2;
		}
	}
}


class Output implements Runnable
{
	private Res r;
	Output(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
			r.out();				
	}
}

public class ThreadDemo
{
	public static void main(String[] args) {
		Res r = new Res();
		//将所有的调用用缩写的形式
		new Thread(new Input(r)).start();
		new Thread(new Output(r)).start();
		
	}
}

3、线程间通信——生产者消费者例子
  这个例子与2中的例子类似,但是这个例子的写法才是开发中常用的写法(有多个生产者消费者)。

/*
 生产者消费者的例子,我们实现生产一个消费一个的功能。
 */
package pack;

class Resource
{
	private String name;//产品编号
	private int count;//产品编号
	private boolean flag = false;
	//  t1    t2
	public synchronized void set(String name)
	{
		while(flag)
			try {this.wait();}catch(Exception e) {}
		
		this.name = name+"---"+count++;//在set()方法中设置产品名称为产品+编号值。我们将计数器设置在Resource中,不设置在Producer中,省事
		System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);//输出一下线程名称和当前生产的产品名称
		
		flag = true;
		this.notifyAll();
	}
	//  t3   t4  
	public synchronized void out()
	{
		while(!flag)
			try {this.wait();}catch(Exception e) {}
		
		System.out.println(Thread.currentThread().getName()+"...消费者..........."+this.name);
		
		flag = false;
		this.notifyAll();
	}
}

class Producer implements Runnable
{
	private Resource res;
	Producer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		int x = 0;
		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 ThreadDemo
{
	public static void main(String[] args) {
		Resource r = new Resource();

		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);

		//我们设置有2个线程进行生产,2个线程进行消费
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);

		t1.start();
		t2.start();
		t3.start();
		t4.start();
		//当出现多个生产者消费者的时候,必须要while循环判断flag,必须要notifyAll唤醒全部
		
	}
}
/*
在有2个线程的时候,生产一个消费一个不断循环,结果如下:
...
Thread-0...生产者...--商品-----13299
Thread-1...消费者...........--商品-----13299
...
 */

/*
 当有4个线程时,结果是:
 Thread-0...生产者...--商品-----4091
Thread-1...生产者...--商品-----4092
Thread-3...消费者...........--商品-----4092
我们发现多线程生产和多线程消费时,出现还没有生产就消费或者生产完不消费的情况(见视频13.50处开始的解释)

注意,notify方法在线程池中唤醒是按之前wait()的顺序唤醒!!!

对于生产者t1、t2与消费者t3、t4,首先进来flag为false,t1获取CPU执行权,打印“生产者1”,并将flag置true,并放弃运行资格等待(t1等待)。
注意,wait()的时候线程t1会将synchronized的锁释放,这样t2才能获取锁进来!等到t1重新被唤醒,它才会重新获取锁,执行wait()下面的语句!。

接下来假设t2获取CPU执行权,flag为true,(t2等待)。接下来t3进来,打印“消费者1”,并将flag置false,notify唤醒t1(t1唤醒),并放弃运行资格等待(t3等待)
接下来假设t4获取CPU执行权,flag为false,(t4等待)。接下来t1获取CPU执行权,打印“生产者2”,,并将flag置true,按线程池唤醒顺序,唤醒t2(t2唤醒),
并放弃运行资格等待(t1等待)。t2在被唤醒后,会从wait()处开始执行,那么就不需要再判断flag,直接执行下面的代码,打印“生产者3”,前一个还没有消费就被覆盖!
t2再把t3唤醒,也只会打印t2生成的,不会打印t1生成的。

原因:因为t2醒的时候并没有去看标记flag,而t1已经生成一个并将flag置为true,t2没有判断便直接生产,把前一个生产产品覆盖,便出错。
如果t2判断标记便会再次wait()不会打印,我们要每一次醒来都判断标记。
解决:我们将if换为while,if只判断一次,而t1唤醒t2后(t1等待),while会再回去判断flag是不是为true,发现是true,会再次将t2wait(),
如果flag不是true,才会继续执行t2线程下面的代码。 
如果t2也wait,那么4个线程全部在等待。
那么我们干脆全部唤醒,由于会循环while判断flag,那么就不会出错!
 */

  针对上面这个程序,我们在notifyAll()的时候,不仅将对方的线程唤醒了,而且本方的线程也被唤醒。如果我们只希望唤醒对方的线程而不唤醒本方线程,我们利用JDK5的新特性Lock接口,Condition接口(2个都在java.util.concurrent.locks下),请看如下代码:(我们以后尽量使用这种Lock的新特性上锁,而不用synchronized)

/*
 生产者消费者的例子,我们实现只唤醒对方而不唤醒本方的功能。

注意区分锁(lock)与等待唤醒(wait,signal)

JDK1.5 中提供了多线程升级解决方案,将同步Synchronized替换成显式的Lock操作。
将Object中的wait,notify,notifyAll,替换了Condition对象,该对象可以Lock锁 进行获取。
现在一个锁可以对应多个Condition对象,那么就对应多个await()等,而之前一个synchronized对象(this,字节码文件对象)只能对应一个wait()等

该示例中,实现了本方只唤醒对方操作。

Lock:替代了Synchronized
	lock 
	unlock
	newCondition():用于创建Condition接口的对象

Condition:替代了Object wait notify notifyAll
	await();
	signal();
	signalAll();

 */
package pack;


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

class Resource
{
	private String name;
	private int count;
	private boolean flag = false;
	
	//首先,我们想要使用Lock接口里面的方法,必须先创建实现该Lock接口的子类对象(多态)调用子类重写Lock接口的方法。
	//由于Lock接口捕捉java.lang包中,我们需要导入相关的包(快捷键ctrl+shift+o)
	//Reentrant:可重入的;可再入的;可重新进入;可重入程序
	private Lock lock = new ReentrantLock();//并且为了避免该对象被外界调用,必须私有化
			
	//相关的操作(wait、notify等)都在Condition接口中,而相应的这些操作都必须对应指定的锁(他们放在锁中使用)
	//lock中有一个Condition newCondition()方法:返回绑定到此 Lock 实例的新 Condition 实例。
	//因此我们想要创建Condition接口的方法,必须通过lock对象调用newCondition方法来实现
	
	//这里说明。lock有一个新特性,就是一个lock锁上可以创建多个Condition对象,而之前synchronized一个锁只能有一个对象!
	//那么我们此处创建2个Condition对象,set与out所对应的线程各一个Condition对象,
	//这两个对象的signal()只能唤醒本对象对应的await()
//	private Condition condition = lock.newCondition();
	private Condition condition_pro = lock.newCondition();//接口无法创建实例(对象),因此这里也相当于多态,lock.newCondition()创建一个实现Condition接口的子类的对象
	private Condition condition_con = lock.newCondition();
	
	public void set(String name) throws InterruptedException//此处的异常没有catch处理,抛出让调用者处理
	{
		lock.lock();//上锁
		try
		{
			while(flag)//判断标记的时候我们调用Condition接口中的等待await()方法
//我们发现await()方法抛出InterruptedException异常,如果在这里抛出异常,这段程序便会结束,到调用的地方去处理异常。而unlock没有执行,锁没有解开!注意Lock中的await()方法不会像synchronized中的wait()一样自动释放锁!必须unlock才释放锁!
//因此我们用try_finally结构,按文档内Condition接口示例代码,捕捉异常并抛出
				//当flag=true,执行await(),线程等待,下面的代码不执行;当flag=false,不执行await(),线程不等待,下面的代码执行
				//但是不管下面的代码会不会执行,锁都会解开。只有解锁了,其他生产者线程才能进来。与之前的synchronized是一样的
				condition_pro.await();//用pro调用等待,生产者等待
			this.name = name+"---"+count++;
			System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
			flag = true;
			condition_con.signal();//用con唤醒,那么生产者线程不唤醒,只唤醒消费者线程
		}
		finally
		{
			lock.unlock();//释放锁
		}
	}
	 
	public void out() throws InterruptedException
	{
		lock.lock();//上锁
		try
		{
			while(!flag)
				condition_con.await();//用con调用等待,消费者
			System.out.println(Thread.currentThread().getName()+"...消费者..........."+this.name);
			flag = false;
			condition_pro.signal();//用pro唤醒,那么消费者线程不唤醒,只唤醒生产者线程
		}
		finally
		{
			lock.unlock();
		}
	}
}

class Producer implements Runnable
{
	private Resource res;
	Producer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
			//我们这里将set方法抛出的异常处理一下,但是不做任何操作。没有被interrupt()方法中断就不会抛出异常!
			try
			{
				res.set("+商品+");
			}
			catch (InterruptedException e)
			{
			}
	}
}

class Consumer implements Runnable
{
	private Resource res;
	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
			try
			{
				res.out();
			}
			catch (InterruptedException e)
			{
			}				
	}
}

public class ThreadDemo
{
	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(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);

		t1.start();
		t2.start();
		t3.start();
		t4.start();
		
	}
}
/*
结果1:发现出现和之前一样的错误,线程全部在等待,既线程全部进入冻结状态。
那么我们使用signalAll(),就正常运行!但是这样与之前全部唤醒没有什么区别,我们想实现只唤醒对方而不唤醒本方的功能。
我们使用Lock接口的新特性创建属于生产者的Condition对象condition_pro与属于消费者的Condition对象condition_con,
这样就可以在不唤醒本方的前提下唤醒对方。(注意视频12-6,25.10开始处的解释)
 */

4、多线程——停止线程(这部分结合视频看,否则我自己都不知道自己在讲什么)
  对于处于冻结状态的线程,可以使用interrupt()方法使其恢复运行,再利用在处理interrupt的InterruptedException的catch语句将标志设置为false来跳出多线程run()方法的循环,以此来停止线程。如下:

/*
stop方法已经过时。但是不能清除,因为之前的代码都是用的stop()

如何停止线程?只有一种,run方法结束。线程要运行的代码都在run()中,如果run()结束,那么线程必然结束。
开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。

特殊情况:当线程处于了冻结状态,就不会读取到标记。那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态是,这时需要对冻结进行清除,强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。

Thread类提供该方法 interrupt();
 */
package pack;

//创建一个停止线程的方法
class StopThread implements Runnable
{
	private boolean flag =true;//提供一个标志flag,用于控制多线程的循环
	//
	public synchronized void run()
	{
		//解释(见视频9.45秒开始处)
		while(flag)
		{
			try
			{
				//注意!!!,在synchronized同步中,wait()的时候线程不仅会放弃执行资格冻结,而且会默认释放锁,这个时候其他线程才能进来
				//而lock中需要用unlock方法来手动释放锁!
				wait();//线程0进来,拿到锁,就wait()在这里,并释放锁;那么线程1进来,拿到锁,也wait()在这里!
			}
			catch(InterruptedException e)
			{
				System.out.println(Thread.currentThread().getName()+"....Exception");
				//我们可以不要changeFlag方法,而是直接在异常处理的时候,直接将flag变为false,这样程序一样会跳出循环停止
//我们清楚冻结就是为了让线程结束,那么直接在处理清楚异常的时候就flag=false,直接跳出run()的循环,线程结束。这样更加方便。
				flag = false;
			}
			System.out.println(Thread.currentThread().getName()+"....run");
		}
	}
	//对外提供一个可以改变flag值的方法
	public void changeFlag()
	{
		flag = false;
	}
}

public class ThreadDemo
{
	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 = 0;
		while(true)
		{
			if(num++ == 600)//当num加到600,主线程再跳出while循环
			{
			//当num加到600的时候,我们改变flag的值,使得多线程跳出循环,不再打印
				//这里说明我们想让线程结束,控制线程中的循环结束即可
//				st.changeFlag();
				
				//注意,interrupt中断线程并不是停止线程,而stop()才是停止线程(视频12-7,11.30)
				//interrupt中断状态相当于强制将线程从wait()/sleep()的冻结状态强制恢复到运行状态。
				//也就是说,interrupt在清除线程的冻结状态!
//当主函数运行完,并且flag改变后,我们手动清除2个线程的冻结状态,这样,个线程就会发现flag已经变化,就会跳出循环停止打印
				t1.interrupt();
				t2.interrupt();
				
				break;//主线程跳出循环
			}
			//Thread.currentThread().getName()同样可以获主线程名
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}
/*
第一次结果:发现在主程序循环600次后,整个程序停止;

第二次加同步,加wait()
第二次结果:发现主程序循环600次后,整个程序没有停止,此时主线程执行完毕,因为其他2个分线程获取CPU执行权之后,进入冻结状态,
那么它便不会读取到flag变化的标记,那么它便不会停止while(flag)循环。也就是我们改变了分线程运行的标记,但是由于分线程进入wait(),
没有发现flag的变化,2个线程并没有结束。

第三次:加上interrupt后,结果是:
main.......600
over
Thread-0....Exception
Thread-0....run
Thread-1....Exception
Thread-1....run
主函数执行完后,interrupt唤醒wait(),并会抛出一个InterruptException异常。打印异常信息,再打印一次线程内容,由于flag被设置为false,线程就跳出循环,对线程0和线程1都是一样的。
(wait()方法解释:InterruptedException - 如果在当前线程等待通知之前或者正在等待通知时,任何线程中断了当前线程。在抛出此异常时,当前线程的中断
也就是说,wait()方法被interrupt方法中断的时候,会抛出这种异常,没被中断就不会抛出,但是有可能抛出这种异常,就必须处理!)
 */

5、多线程——守护线程
  守护线程setDaemon()的使用如下例子:

/*
守护线程:public final void setDaemon(boolean on)
当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。 
 */
package pack;

class StopThread implements Runnable
{
	private boolean flag =true;
	public void run()
	{
		while(flag)
		{
			System.out.println(Thread.currentThread().getName()+"....run");
		}
	}

}

public class ThreadDemo
{
	public static void main(String[] args) {
		StopThread st = new StopThread();
		
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		
		//如果我们不设置flag=false,不interrupt,也不setDaemon,那么线程打印完main后,2个多线程会wait(),线程冻结不动。
		//如果标志为守护线程,那么线程打印完main后,线程会停止!(解释:12-8,1.40开始处解释)
		//可以理解为“后台线程”,其他的没有守护的都是前台线程。守护线程的开始,运行与普通线程没有区别,就是当全部非守护线程结束,守护线程会自动结束!
		//我们将多线程改为无限循环打出,并且不设置wait(),发现当主线程(非守护)打印完后,其他分线程也会结束!
		t1.setDaemon(true);//标志为真就是设置为守护线程,守护线程在线程开始前设置
		t2.setDaemon(true);
		
		t1.start();
		t2.start();
		
		int num = 0;
		while(true)
		{
			if(num++ == 600)
			{//设置主线程打印600次退出		
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}
/*
 会有如下结果:
 main.......600
over
Thread-0....run
Thread-1....run

因为主线程结束是判断到},如果打印完over后,CPU被线程0与线程1抢了,会继续打印线程0,1的内容。
等到CPU回到主线程。运行到}的时候,主线程才结束,分线程0,1也随之结束
 */

6、多线程——join()方法
  我们这一节介绍join()方法:join可以用来在一个运行中的线程A临时加入线程B执行。具体见如下例子:

/*
join:当A线程执行到了B线程的.join()方法时,A就会等待,等B线程都执行完,A才会执行。
需要注意,A何时恢复执行权只与B有关,而与此时执行的其他线程无关。
如本程序,main何时恢复只与t1线程何时运行完有关,而与t2线程何时运行完无关。

join可以用来在一个运行中的线程A临时加入线程B执行。

 */
package pack;

//创建一个Demo用来保存多线程运行代码run()
class Demo implements Runnable
{
	public void run()
	{//多线程会循环打印本类对象700次再停止
		for(int x=0; x<700; x++)
		{
			System.out.println(Thread.currentThread().getName()+"....."+x);
//			Thread.yield();
		}
	}
}

public class ThreadDemo
{
	public static void main(String[] args) throws InterruptedException//将异常抛给虚拟机,由于我们没有中断线程,那么不会抛出异常
	{
		Demo d = new Demo();
		
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		
		t1.start();
		//0线程开始执行,随后0线程的join()方法加入,这个时候会等0线程都执行完再执行1线程和主线程
//		t1.join();
		t2.start();
		t1.join();//移动这一句到t2线程开启之后
		//为了方便观察主函数也得输出一些东西
		for(int x=0; x<800; x++)
		{
			System.out.println("main....."+x);
		}
		System.out.println("over");
		
	}
}
/*
 第一次结果:0线程,1线程与main线程交替打印。
 
在t1.start();后面加入t1.join()
 第二次结果:0线程全部打印完毕,再交替打印1线程与主线程 
 解释:主线程向下走,遇到t1.start();t1.join();,t1开启,且t1申请加入到主线程的运行中来,也就是t1要CPU的执行权,
 那么t1从主线程中获得CPU执行权(此时主线程处于冻结状态),这个时候t2.start();还没有执行,只有t1活着,等到t1执行完毕,主线程恢复到运行状态,
 t2.start();与下面主线程的打印代码才执行,随后t2线程才开启,t2与主线程才交替打印。

将t1.join移动到t2.start();后面
第三次结果:0线程与1线程交替打印,等到0线程执行完,主线程才开始执行,接下来主线程与1线程交替执行
解释:主线程执行下来,先开启t1与t2两个线程,如果接下来CPU还在主线程手里,主线程执行:t1.join();,也就是t1线程插入主线程中
这样主线程就冻结(释放执行权),t1线程与t2线程还活着,只有等到t1(0线程)执行完毕,主线程才会恢复执行权开始执行t2.start();之后的代码。
(t2结束不结束与主线程无关)接下来就是主线程与t2线程(1线程)交替执行
 */

7、多线程——优先级yield方法
  下面介绍toString()、yield()、与setPriority()方法

/*
这一节我们再介绍其他几个方法:
toString():重写Object类的toString()方法,有线程对象自己特有的字符串表现形式:返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
setPriority():设置线程优先级
yield():暂停当前正在执行的线程对象,并执行其他线程。(少用,面试可能问到)
 */
package pack;

class Demo implements Runnable
{
	public void run()
	{
		for(int x=0; x<70; x++)
		{
			System.out.println(Thread.currentThread().toString()+"....."+x);//打印一下toString()的内容
			//假设0线程打印一次,yield()就将0线程释放;假接下来1线程打印一次,yield()再将1线程释放。
			//稍微减缓一下线程执行的频率。如果不释放,某一个线程会连续输出很多次(CPU执行权一直在这个线程手里)
			Thread.yield();
		}
	}
}

public class ThreadDemo
{
	public static void main(String[] args) throws InterruptedException
	{
		Demo d = new Demo();
		
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		
		t1.start();
		//将t1的优先级设置为10,可以直接写10,但是阅读性不好!用常量MAX_PRIORITY表示
		//数据是固定的,定义为常量;数据是共享的,定义为静态。
//		t1.setPriority(Thread.MAX_PRIORITY);//这样t1获得CPU资源的概率越高!
		t2.start();

		for(int x=0; x<80; x++)
		{
//			System.out.println("main....."+x);
		}
		System.out.println("over");
		
	}
}
/*
第一次:不加join()和yield()方法,直接打印toString()的内容:交替打印main,0和1线程:如下格式
Thread[Thread-1,5,main].....65
Thread[Thread-1,5,main].....66
线程名称:Thread-1;优先级:5;线程组:main;

线程组:
一般情况下,哪一个线程开启你,你就属于哪个线程组,main线程开启线程0,1,所以他们属于main线程组。
如果你想属于别的组,你就new一个ThreadGroup类的对象,产生一个新的线程组,把你要封装到这个新组中的对象存储进去就可以。(这一部分很少用)

优先级:优先级代表抢夺CPU资源的频率,优先级越高获得CPU执行权的频率越高。(1-10一共10级,10级的优先级是最高的)
其中,10级用MAX_PRIORITY 表示,5级用NORM_PRIORITY 表示,1级用MIN_PRIORITY 表示
我们可以用setPriority()方法来设置优先级,所有线程的默认优先级为5。

第二次:将主线程打印注释,并在run()方法里面设置yield()
结果:线程0打印一次,线程1打印一次。交替打印直到循环结束。
 */

8、开发时如何写线程——代码示例(视频12-10,10分钟开始处的解释)

 /*

 */
package pack;

public class ThreadDemo
{
	public static void main(String[] args) throws InterruptedException
	{
		//如果一个类中有多个循环,而写在之前的循环运行太久,那么其他循环就很难运行到!
		//当某些线程需要同时被执行时,就需要用单独的线程进行封装,以用多线程执行。
		//如下面3部分就有3个线程可以同时执行,这样一封装程序就更加高效!
		
		//用一个类继承Thread()类创建多线程的第一类方法。用匿名内部类的形式来写,创建Thread类的对象并重写run()方法
		//本来是有一个类来继承Thread(),再创建该类的对象,这里匿名内部类简化了这个继承Thread的类,而是直接以Thread()类的形式表现。(具体见匿名内部类)
		new Thread()
		{
			public void run()
			{
				for(int x=0; x<100; x++)
				{
					System.out.println(Thread.currentThread().getName()+"....."+x);
				}
			}
		}.start();//在这里直接调用start方法开启这个线程
		
		//用一个类实现Runnable接口的创建线程的第二种方法。同样用匿名内部类的形式,但是需要创建一个Runnable的对象。
		Runnable r = new Runnable()//这里相当于多态,因为本来要实现Runnable接口的子类名字匿名,所以用父接口的名字替代,同时在这里重写父接口的方法。正常情况下接口是没办法创建对象的,因为接口都是抽象方法,创建对象调用抽象方法没有意义!!!这里是多态的应用!
		{
			public void run()
			{
				for(int x=0; x<100; x++)
				{
					System.out.println(Thread.currentThread().getName()+"....."+x);
				}
			}
		};
		new Thread(r).start();
		
		//普通循环
		for(int x=0; x<100; x++)
		{
			System.out.println(Thread.currentThread().getName()+"....."+x);
		}
		
		//new Test1().start();
		
	}
}
/*也可以将这三个部分单独封装出去,这样就看起来更加模块化!不过开发一般使用的是匿名内部类的形式。
class Test1 extends Thread
{
	public void run()
	{
		for(int x=0; x<100; x++)
		{
			System.out.println(Thread.currentThread().getName()+"....."+x);
		}
	}
}
*/

9、就业班多线程补充
补充1:为什么run方法不能抛出异常
  因为run方法来自于父类Thread,而Thread的run方法没有抛出异常,我们知道子类抛出的异常小于等于父类抛出的异常,父类没有抛出异常,那么子类也不能抛出异常。既run方法不能抛出异常。

补充2:线程池
  我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
  那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。
  线程池底层原理,JDK1.5之后,java就自带了线程池,不需要我们自己创建
在这里插入图片描述
  线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
在这里插入图片描述
  合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;
  2. 提高响应速度,当任务到达时,任务可以不需要的等到线程创建就能立即执行;
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
    线程池的实现代码
ThreadTest类
package lkj.demo1;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
    线程池:JDK1.5之后提供的
    java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
    Executors类中的静态方法:
        static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
        参数:
            int nThreads:创建线程池中包含的线程数量
        返回值:
            ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
    java.util.concurrent.ExecutorService:线程池接口
        用来从线程池中获取线程,调用start方法,执行线程任务
            submit(Runnable task) 提交一个 Runnable 任务用于执行
        关闭/销毁线程池的方法
            void shutdown()
    线程池的使用步骤:
        1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
        3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
 */
public class ThreadTest
{
    public static void main(String[] args)
    {
        //1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        //Executors类的newFixedThreadPool方法使用接口ExecutorService接收(多态)
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        //注意ExecutorService接口的submit方法既可以接收Runnable的子类对象并放入线程池的Thread中创建线程,又会start隐式开启线程
        es.submit(new RunnableImpl());
        //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
        es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程执行

        //4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
        es.shutdown();

//        es.submit(new RunnableImpl());//抛异常,线程池都没有了,就不能获取线程了
    }
}
---------------------
RunnaImpl类
package lkj.demo1;
/*
    2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
 */
public class RunnableImpl implements Runnable
{
    @Override
    public void run()
    {
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值