关闭

黑马程序员----超实用的多线程

标签: java
209人阅读 评论(0) 收藏 举报

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ------


多线程

1.什么叫线程?

如果我们想要了解什么叫线程,那么首先我们应该了解什么叫进程。

进程

所谓进程,就是一个正在执行的程序(应用程序的一个实例),每一个进程执行都有一个执行顺序,该顺序就是一个执行路径,或者叫控制单元。

线程

所谓线程就是进程中一个独立的控制单元,线程控制着进程的执行

到这里我们就知道,其实多线程,指的就是在一个进程中,有多个控制单元,或者说多条执行路径,使得程序的各个部分好像在同时运行似的。

JVM

我们来看看JVM启动的java程序,当一个java程序启动时,就意味着开启了一个进程,而这个进程中首先有一个主线程,也就是执行main方法的线程,而同时我们知道,当程序中产生垃圾时,不需要主线程来处理垃圾回收的问题,而是由垃圾处理机制来处理,我们可想而知,所谓的垃圾处理机制肯定运行在另一个线程中,因此一个java程序严格来说就是多线程程序


2.多线程的作用

假如没有多线程,我们思考一下,当程序在执行一段程序时,只要这段程序没有执行完,那么就不可能执行到其他程序段,即便这段程序的重要性不如其他程序,那么可想而知这种方式的效率低,且安全性不好,如果有问题出现,那么也不能第一时间得到解决。另外,我们知道有的语句是阻塞式的,当执行到这些语句时,如果没有多线程,那么整个程序将停在那里等待,这种方式是我们不能接受的,你能想象当你使用电脑时,如果想聊天,那么视频和音乐都要停止吗?
而多线程的出现解决了这一系列问题,它能够使得程序的每个部分看起来都是在“同时运行的”,当然了,在单核CPU上,这只是看起来,因为其实并不是同时运行,而是线程A运行一下,线程B运行一下。。。。本质就是CPU在不同的线程间做着快速的切换,由于速度非常快,因此在我们看起来就是同时运行的效果。当然了,在当今的多核CPU上,确实是可以实现同时运行的。
多于程序设计,多线程有着另一个好处,那就是将不同的工作放到不同的线程中,这是符合单一职责原则的,也就是每个线程只负责一个工作,程序结构更清晰。


3.线程的创建

线程的创建有两种方式:继承和实现

继承:

通过查阅文档我们发现,如果想要创建一个线程,我们可以通过继承于Thread类,实现其run方法来自定义线程类,再调用其start方法启动线程。
1.继承于Thread类(使其具有线程的基本特性)。
2.实现run方法(run方法中是线程的运行代码)。
3.new一个本类对象(这个对象就是自定义的线程对象)。
4.调用start方法(启动线程,使得线程去运行run方法)。
/*
创建一个线程的第一种方法:
1.继承自Thread类
2.重写run方法(run方法时Thread类实现的Runnable接口的方法)
*/
package com.helong.threaddemo;
class MyThread extends Thread
{
	public void run()
	{
		for(int i=0;i<100000;i++)
			System.out.println("mythread run");
	}
}
class ThreadDemo 
{
	public static void main(String[] args) 
	{
		MyThread mt = new MyThread();
		//mt.run();
		mt.start();//使得线程开始执行,jvm调用mt的run方法,也只有这种方法才能主线程mt线程交替执行
		for(int i=0;i<100000;i++)
			System.out.println("mainthread run");
	}
}
运行图:


实现:

继承方式会导致该子类不能继承于别的类,这是继承的局限性。因此通常我们使用实现方式来创建线程。
1.实现Runnable接口(此时子类并不是一个线程对象,可以理解为一个需要被线程执行的代码块)。
2.定义run方法:将需要执行的代码存放在run中(线程将要运行的代码)。
3.创建子类对象
4.将该对象传递给Thread类的构造函数:通知该线程对象,需要执行的代码在这个实参中(将创建的线程与运行代码相关联)。
5.调用Thread对象的start方法(启动线程,执行运行代码)。
/*
继承方式:
简单卖票程序:
多个窗口同时卖票
每卖出一张,票号加1
假设只有100张
*/
package com.helong.sellticket;
class TicketWindow extends Thread
{
	private static int tickets=1;//没有static,会打印30张票
	TicketWindow(String name)
	{
		super(name);
	}
	public void run()
	{
		while(tickets!=11)
		{
			System.out.println(this.getName()+"sell ticket :"+tickets++);
		}
	}
}
class SellTicket 
{
	public static void main(String[] args) 
	{
		TicketWindow tw1 = new TicketWindow("窗口1");
		TicketWindow tw2 = new TicketWindow("窗口2");
		TicketWindow tw3 = new TicketWindow("窗口3");
		tw1.start();
		//tw1.start();
		tw2.start();
		tw3.start();
	}
}
运行图:


/*
继承方式问题:
票数必须为static,否则有几个线程对象,就打印几份票
而static由于生命周期过长,通常不建议使用
实现方式:
这种方式即避免了继承方式的单继承局限性
不存放打印重复票的问题(因为本身TickWindow只有一个对象)
*/
/*
未使用同步代码块时出现安全问题的打印:
Thread-1sell ticket :1
Thread-0sell ticket :2
Thread-2sell ticket :1
Thread-1sell ticket :3
Thread-2sell ticket :4
Thread-0sell ticket :5
Thread-1sell ticket :6
Thread-2sell ticket :7
Thread-0sell ticket :8
Thread-1sell ticket :9
Thread-0sell ticket :10
Thread-2sell ticket :11
Thread-1sell ticket :12
*/
/*
使用同步代码块后的打印:

*/
package com.helong.sellticket2;

class TickWindow implements Runnable
{
	private int tickets=1;
	Object obj = new Object();
	//并没有静态化tickets,不需要,因为自始至终多个线程运行的是同一段代码,操作的是同一个成员tickets
	public void run()
	{
		while(true)			
		{
			//解决这种安全问题的方法,叫做同步代码块,将操作了共享数据的代码块使用synchronized(对象)包起来
			synchronized(obj)
			{
				if(tickets<1001)
				{
					try
					{
						//Thread.currentThread().sleep(1000);
						Thread.sleep(100);
						//有可能看上去不成功,那可能是因为睡眠时间不够,或者票数不够,一个线程还没执行完毕
						//票就没了
						//这种安全性问题出现的几率不大,但是出现了就很严重,因此多线程的安全问题一定要注意
					}
					catch (InterruptedException e)
					{
					}
					System.out.println(Thread.currentThread().getName()+"sell ticket :"+tickets++);
				}
			}
		}
	}
}

class SellTicket2 
{
	public static void main(String[] args) 
	{
		TickWindow tw = new TickWindow();

		Thread t1 = new Thread(tw);
		//将线程本身与要执行的代码的对象联系起来,联系的基础是实现了Runnable接口
		//只是实现了该接口,都能在一个线程中独立运行
		Thread t2 = new Thread(tw);
		Thread t3 = new Thread(tw);

		t1.start();
		t2.start();
		t3.start();
	}
}
运行图:


继承与实现的区别:

1.存放线程代码位置不同,继承存放于Thread子类的run中,实现存放在Runnable子类run中
2.卖票例子(票数本身必须只有一份)
继承-》四个线程,每个线程运行自己的run方法(也就是卖票),我们看到每个线程都是卖出了自己的一份票,我们知道这是不正常的,因为一号票只有一张,但是在此处却被卖出了四份。
实现-》四个线程,每个线程都是运行Runnable子类的run方法,也就是说自始至终自由一份run在运行,只有一份票,肯定不会重票
3.继承方式会导致该子类不能继承于别的类,这是继承的局限性,而实现方式避免了这种局限性

4.线程在运行中的几种状态

1.被创建:new
2.运行:start ---有运行资格,也有执行权
3.冻结(睡眠):sleep(int)或者wait() ---没有运行资格
如果是sleep进入冻结,当时间一到就自动运行,如果是wait进入,需要使用notify唤醒线程
4.消亡:stop或者当run方法结束
5.临时状态(阻塞状态) ---有运行资格,但是没有运行权
当一个线程在运行状态或者从冻结状态被唤醒时,可能会进入临时状态,因为虽然线程有了运行资格,但是没有执行权(cpu还没执行到该线程)


5.线程常用方法

静态方法currentThread获取当前线程对象,作用相当于this(this只适用于继承方式的多线程中),在实现方法实现多线程中:由于线程运行代码本身不在线程的方法中,因此无法使用this来获取当前线程,此时只能用Thread.currentThread()来获取。

 setName设置线程名称

 getName获取线程名称

小知识点:一个线程不能多次start。

6.线程的安全性问题

由于多线程操作的是共享数据,当一个线程在执行操作数据的几条语句时,可能在中间停止了,这时另一个线程也来执行这几条语句,这个时候就会造成数据错误
class Ticket implements Runnable
{
	private  int tick = 1000;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			
			if(tick>0)
			{
				try{Thread.sleep(10);}catch(Exception e){}
				System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
			}
		}
	}
}


class  TicketDemo2
{
	public static void main(String[] args) 
	{

		Ticket t = new Ticket();

		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

运行图:

我们看到,结果打印出了0,-1,-2这样的票号,这肯定是错误的,其实原因很简单,如图,当线程1判断成功准备打印票号1时,它睡眠了,这时线程3执行,也判断成功了,准备打印时也睡眠了,线程2和线程0同样的结果,都是判断成功了,但是没打印,此时回到线程1,它打印票号1,运行结束,线程3执行,此时票号已经为0了,按理说是不合理的,但是由于之前就判断成功了,所以此处没有再判断一次,结果就打印出了非法的票号0,线程2和线程0也是一样的道理,结果打印出了票号-1和-2

其实解决这个问题的原则也很简单:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行

java对于解决多线程安全性问题提供了专业的解决办法:同步代码块和同步函数

同步代码块:

操作了共享数据的语句放到一个同步代码块中,如果有线程在这里面执行,那么别的线程是不能执行这个代码块的,必须等到前一个线程从代码块中出来,这样也就不会出现数据错误的情况
格式:synchronized(对象){操作了共享数据的代码块}
对象相当于一个锁,当线程执行到此时判断一下是否开锁,如果是开锁状态,那就持有锁,这样别的线程就不能进入了
前提:1.必须是多线程 ,2.必须是多个线程使用一个锁

好处:解决了多线程的安全性问题

弊端:每次执行到此都要进行锁的判断等,消耗资源

class Ticket implements Runnable
{
	private  int tick = 100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			synchronized(obj)
			{
			
				if(tick>0)
				{
					try{Thread.sleep(10);}catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
				}
			}
		}
	}
}


class  TicketDemo2
{
	public static void main(String[] args) 
	{

		Ticket t = new Ticket();

		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
运行图:



可以看到,当买到1号票时,程序就会停止卖票,这就是使用同步代码块解决多线程的安全性问题。

同步函数:

同步函数synchronized修饰一个函数,使得函数具有同步的能力

此时锁对象是this,因此如果使用的是继承方式的话,那就代表当前线程,也就是说该锁不唯一,因此此方法只适用于实现方式

如果同步函数被static修饰,它随着类的加载而进入内存,静态进内存时,还没有本类对象,但是有本类对应的字节码文件对象,因此静态同步函数使用的锁就是类名.class

/*
验证:同步函数中使用的锁是this
前提:实现方式
*/
package com.helong.threaddemo3;
class Ticket implements Runnable
{
	private static int tickets=1;
	private Object obj = new Object();
	boolean flag = true;
	public void run()
	{
		if(flag)
		{
			flag=false;
			while(true)
			{
				//synchronized(this)
					//如果此时为obj,那么达不到同步的效果,因为两个线程持有的锁不同,换成this即可
				synchronized(Ticket.class)
					//当同步函数为静态时,它使用的锁是本类对应的字节码文件对象也就是Ticket.class
				{
					if(tickets<1001)
					{
						try{Thread.sleep(10);}catch(Exception e){}
						System.out.println("code ..... "+tickets++);
					}
				}
			}
		}
		else
		{
			while(true)
				sale();
		}
	}
	private static synchronized void sale()//将此同步函数设为静态,那么tickets也应该为静态,但此时它使用的锁是Ticket.class对象
	{
		if(tickets<1001)
		{
			try{Thread.sleep(10);}catch(Exception e){}
			System.out.println("function ..... "+tickets++);
		}
	}
}
class ThreadDemo3 
{
	public static void main(String[] args) 
	{
		Ticket t = new Ticket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);

		t1.start();
		try{Thread.sleep(10);}catch(Exception e){}
		t2.start();
	}
}
运行图:



7.死锁

举例:运行同步内容需要持有两把锁A,B,此时有线程A和线程B,线程A先申请锁A,线程B先申请锁B,此时如果线程A持有锁A,线程B持有锁B,那么就进入了死锁
/*
需求:实现死锁现象
形成死锁的原因:两个或以上的线程,两个或以上的锁,
	此处使用线程A,线程B,锁A,锁B,当线程A持有锁A,
	线程B持有锁B,而执行需要同时持有两种锁时,这时
	线程A有锁A,请求锁B,线程B有锁B,请求锁A,进入
	死锁现象。
思路:
1.run方法中分两路对应两个线程,设置两个锁
2.每一路请求锁的顺序不同
*/
package com.helong.deadlocktest;
class DeadLock implements Runnable
{
	private int tickets=1;
	private Object obj1 = new Object();
	private Object obj2 = new Object();//两把锁
	boolean flag = true;
	public void run()
	{
		if(flag)
		{
			flag=false;
			while(true)
			{
				synchronized(obj1)//请求obj1锁
				{
					//很可能线程A在请求到obj1时,线程B已经持有了obj2了,此时就是死锁
					synchronized(obj2)//请求obj2锁
					{
						if(tickets<1001)
						{
							try{Thread.sleep(10);}catch(Exception e){}
							System.out.println(Thread.currentThread().getName()+"卖出票:"+tickets++);
						}
					}
				}
			}
		}
		else
		{
			while(true)
			{
				synchronized(obj2)
				{
					synchronized(obj1)
					{
						if(tickets<1001)
						{
							try{Thread.sleep(10);}catch(Exception e){}
							System.out.println(Thread.currentThread().getName()+"卖出票:"+tickets++);
						}
					}
				}
			}
		}
	}
}
class DeadLockTest 
{
	public static void main(String[] args) 
	{
		DeadLock dl = new DeadLock();
		Thread t1 = new Thread(dl);
		Thread t2 = new Thread(dl);

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

运行图:

可以看到,在某一时刻,票没有卖完,但是所有线程都进入了等待的状态,这就是死锁。


8.线程间通信

线程间通信:不同的线程操作同一片资源,但是操作动作不同(例如:存,取)

安全问题:例如资源包含两个数据name,sex,两个线程,一个负责存,一个负责取,当存线程存入"mike","man"时,此时取线程来打印name,sex,没问题,但是如果当存线程存入"lili"之后还没来得及存入"woman",结果取线程就打印了,那输出的就是"lili","man",这就有了安全性问题,因为这组数据时相关的,要么全部存好再取,要么全部取出再存,应该讲这一片资源视为共享数据组,对它们的操作都应该是同步的

/*
线程间通信:就是说不同的线程操作同一个资源,但是操作动作不同(存,取)
*/
package com.helong.threadmessagetest1;

class Resource
{
	private String name;
	private String sex;
	boolean flag=false;//用于表示此时是否存在可用数据组
	private static Resource r = new Resource();
	private Resource(){}
	public static Resource getInstance()
	{
		return r;
	}
	public String getName()
	{
		return name;
	}
	public void setName(String name)
	{
		this.name=name;
	}
	public String getSex()
	{
		return sex;
	}
	public void setSex(String sex)
	{
		this.sex=sex;
	}
}
//解决方法只需两个线程中的一个对数据进行操作
//结束前不允许其他线程来操作
class Input implements Runnable//负责存入数据
{
	static Thread thread;
	public void run()
	{
		thread=Thread.currentThread();
		int x=1;
		Resource r = Resource.getInstance();
		while(true)
		{
	
				if(x==1)//模拟根据不同的情况,存入的每组数据都是不同的,此处设置两组
				{
					r.setName("mike");
				<span style="white-space:pre">	</span>r.setSex("man");
				}
				else
				{
					r.setName("肥园");
					r.setSex("女");
				}

			x=(x+1)%2;//通过x控制该run方法反复设置两组数据到Resource中
		}
	}
}
class Output implements Runnable//负责打印数据
{
	static Thread thread;
	public void run()
	{
		thread=Thread.currentThread();
		Resource r = Resource.getInstance();
		while(true)
		{

			System.out.println(r.getName()+"......"+r.getSex());

		}
	}
}
class ThreadMessageTest1
{
	public static void main(String[] args) 
	{
		Thread t1 = new Thread(new Input());
		Thread t2 = new Thread(new Output());
		t1.start();
		t2.start();
	}
}
运行图:


我们可以看到,此时的数据就是错误的,打印出了“肥园......man”和“mike......女”这样明显错误的数据,当然,这个只是共享数据问题,我们可以使用同步代码块来解决。

/*
线程间通信:就是说不同的线程操作同一个资源,但是操作动作不同(存,取)
*/
package com.helong.threadmessagetest1;

class Resource
{
	private String name;
	private String sex;
	boolean flag=false;//用于表示此时是否存在可用数据组
	private static Resource r = new Resource();
	private Resource(){}
	public static Resource getInstance()
	{
		return r;
	}
	public String getName()
	{
		return name;
	}
	public void setName(String name)
	{
		this.name=name;
	}
	public String getSex()
	{
		return sex;
	}
	public void setSex(String sex)
	{
		this.sex=sex;
	}
}
//解决方法只需两个线程中的一个对数据进行操作
//结束前不允许其他线程来操作
class Input implements Runnable//负责存入数据
{
	static Thread thread;
	public void run()
	{
		thread=Thread.currentThread();
		int x=1;
		Resource r = Resource.getInstance();
		while(true)
		{
			synchronized(r)//Resource为单例模式设计的,因此r肯定只有一个,可以作为锁
			{

					if(x==1)//模拟根据不同的情况,存入的每组数据都是不同的,此处设置两组
					{
						r.setName("mike");
						r.setSex("man");
					}
					else
					{
						r.setName("肥园");
						r.setSex("女");
					}

			}
			x=(x+1)%2;//通过x控制该run方法反复设置两组数据到Resource中
		}
	}
}
class Output implements Runnable//负责打印数据
{
	static Thread thread;
	public void run()
	{
		thread=Thread.currentThread();
		Resource r = Resource.getInstance();
		while(true)
		{
			synchronized(r)
			{

				System.out.println(r.getName()+"......"+r.getSex());

			}
		}
	}
}
class ThreadMessageTest1
{
	public static void main(String[] args) 
	{
		Thread t1 = new Thread(new Input());
		Thread t2 = new Thread(new Output());
		t1.start();
		t2.start();
	}
}
运行图:


我们看到,虽然数据是没问题了,但是由于每个线程运行的时间都是一小段,因此每个线程都会打印一片数据,之后再轮到下一个线程运行,能不能使每个线程只打印一条数据呢?

/*
线程间通信:就是说不同的线程操作同一个资源,但是操作动作不同(存,取)

控制台:存多次,打印一片
原因:每个线程执行都是一段时间,那么在打印线程中打印一段时间肯定是同样的数据,
	因为这一段时间内,存入线程根本没执行
实现,存一个,打印一个的效果
*/
package com.helong.threadmessagetest;

class Resource
{
	private String name;
	private String sex;
	boolean flag=false;//用于表示此时是否存在可用数据组
	private static Resource r = new Resource();
	private Resource(){}
	public static Resource getInstance()
	{
		return r;
	}
	public String getName()
	{
		return name;
	}
	public void setName(String name)
	{
		this.name=name;
	}
	public String getSex()
	{
		return sex;
	}
	public void setSex(String sex)
	{
		this.sex=sex;
	}
}
//解决方法只需两个线程中的一个对数据进行操作
//结束前不允许其他线程来操作
class Input implements Runnable//负责存入数据
{
	static Thread thread;
	public void run()
	{
		thread=Thread.currentThread();
		int x=1;
		Resource r = Resource.getInstance();
		while(true)
		{
			synchronized(r)//Resource为单例模式设计的,因此r肯定只有一个,可以作为锁
			{
					if(r.flag)
					{
						try{r.wait();}catch(Exception e){}
					}
					if(x==1)//模拟根据不同的情况,存入的每组数据都是不同的,此处设置两组
					{
						r.setName("mike");
						r.setSex("man");
					}
					else
					{
						r.setName("肥园");
						r.setSex("女");
					}
					r.flag=true;
					r.notify();
			}
			x=(x+1)%2;//通过x控制该run方法反复设置两组数据到Resource中
		}
	}
}
class Output implements Runnable//负责打印数据
{
	static Thread thread;
	public void run()
	{
		thread=Thread.currentThread();
		Resource r = Resource.getInstance();
		while(true)
		{
			synchronized(r)
			{
				if(!r.flag)//如果没有可用数据组
				{
					try{r.wait();}catch(Exception e){}
				}
				System.out.println(r.getName()+"......"+r.getSex());
				r.flag=false;
				r.notify();
			}
		}
	}
}
class ThreadMessageTest 
{
	public static void main(String[] args) 
	{
		Thread t1 = new Thread(new Input());
		Thread t2 = new Thread(new Output());
		t1.start();
		t2.start();
	}
}
运行图:



此时我们看到,不仅仅数据时正确的,而且是一条一条打印出来的了。


9.等待--唤醒机制

wait(),notify(),notifyAll()

这三个方法都是定义在Object类上的,因为它们都是由监视器(锁)调用的,而任何对象都可以作为锁,因此定义在Object类上;它们操作的必须是同一把锁上的线程,例如wait只能冻结同一把锁上的运行线程,而notify和notifyAll也只能唤醒同一把锁上的线程

锁.wait();作用是将当前持有该锁的线程对象冻结

锁.notify();作用是将锁上第一个被冻结的线程对象解冻

锁.notifyAll();作用是将锁上所有被冻结的线程解冻


10.生产者消费者问题

安全性问题:wait使当前线程进入冻结状态等待其他线程notify它,如果此处有三个线程,且他们的运行是由顺序的,假设进入了一个运行,两个等待的情况,那么当线程A运行完毕,notify时,可能唤醒的是线程C,而不是线程B,这时由于不满于线程C执行的条件,于是线程C进入wait状态,此时进入了类似死锁的状态,线程A,B,C都进入wait状态了

产生这种安全性问题的原因是:多线程的随机性,唤醒机制不会固定每次都依次唤醒线程A,B,C,而是随机唤醒锁上的冻结在线程池中的一个,这时就会可能唤醒到不想唤醒的线程,按照程序,根据条件判断线程是执行还是进入wait,所以如果唤醒到错的线程,那么该线程就会wait,导致此次没有唤醒到应该唤醒的线程,也就运行不了了,所有线程均wait。

解决方法:在程序中判断是否满足执行,如果不满足则wait的代码块中,加入判断是否应该运行当前线程的语句,如果不应该那么就notify其他线程,也就是说如果当前线程还不满足运行条件,那么执行两步:

1.wait;让该线程进入等待状态,将执行权让出来

2.if(不该这个线程运行)notify;如果此时该线程不应该执行,但是却获得了执行权,那么应该notify别的线程这样即便唤醒了错的线程,也能让该线程回到wait状态,且notify别的线程

加强版生产者--检查者--消费者代码:

/*
扩展:生产-》检查-》消费
wait使当前线程进入冻结状态等待其他线程notify它,
如果此处有三个线程,且他们的运行是由顺序的,假设进入了
一个运行,两个等待的情况,那么当线程A运行完毕,notify时,
可能唤醒的是线程C,而不是线程B,这时由于不满于
线程C执行的条件,于是线程C进入wait状态,
此时进入了类似死锁的状态,线程A,B,C都进入wait状态了;
*/
package com.helong.producerconsumer;
import java.util.concurrent.locks.*;

class Res 
{
	private String name;
	private int count=1;
	private boolean haved=false;//标志是否有可用货物
	private boolean checked=false;//标志货物是否已经检查
	private static Res r = new Res();
	private Res(){};
	public static Res getInstance()
	{
		return r;
	}
	public synchronized void set(String name)
	{
		if(!haved)//如果没有货物可用
		{
			this.name=name+"---"+count++;
			System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
			haved=true;//设置有可用货物
			checked=false;//设置该货物未检查
			this.notify();
		}
		else
		{
			try{this.wait();}catch(InterruptedException e){}
			//到此处有两种可能,1:这就是即将运行的线程,2:这不是将要运行的线程
			//如果这不是将要运行的线程,那么应该唤醒其他线程
			//每一个线程到此都应该判断,因为多线程的随机性,不一定轮到哪个线程
			//就是说应该它运行,此程序是有固定顺序的
			if(haved)//不该此线程运行
				this.notify();
		}
	}
	public synchronized void check()
	{
		if(haved&&!checked)//有货物并且未检查
		{
			System.out.println(Thread.currentThread().getName()+"...检查者..."+this.name);
			checked=true;
			this.notify();
		}
		else
		{
			try{this.wait();}catch(InterruptedException e){}
			if(!haved||checked)
				this.notify();
		}
	}
	public synchronized void out()
	{
		if(haved&&checked)
		{
			System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
			haved=false;
			this.notify();
		}
		else
		{
			try{this.wait();}catch(InterruptedException e){}
			if(!haved||!checked)
				this.notify();
		}
	}
}
class Producer implements Runnable
{
	public void run()
	{
		Res r = Res.getInstance();
		while(true)
		{
			r.set("电脑键盘");
		}
	}
}
class Checker implements Runnable
{
	public void run()
	{
		Res r = Res.getInstance();
		while(true)
		{
			r.check();
		}
	}
}
class Consumer implements Runnable
{
	public void run()
	{
		Res r = Res.getInstance();
		while(true)
		{
			r.out();
		}
	}
}
class ProducerConsumer 
{
	public static void main(String[] args) 
	{
		new Thread(new Producer()).start();
		new Thread(new Checker()).start();
		new Thread(new Consumer()).start();
	}
}
运行图:



我们可以看到,使用了等待--唤醒机制的生产--检查--消费问题,得到了很好的解决。


11.多个生产者消费者问题

安全性问题:

例如:生产者线程A,B,消费者线程C,D;假设先是A运行,生产了商品1,进入冻结,此时线程B运行,判断进入冻结状态,线程C运行,消费了商品2,唤醒了A,进入冻结,线程D判断进入冻结,A生产了商品2,唤醒了B,此时B的位置是wait处,被唤醒后直接向下执行,生产了商品3(产生错误,多次生产商品)

原因:

多线程的随机性,导致唤醒的不一定是应该执行的那个线程,但是由于线程运行代码内部只有一次判断,因此当一个线程wait后,下一次运行它时就不需要判断而直接执行,这就导致了错误的产生

解决方法:

1):将一次判断改为多次循环判断

如果仅仅是改为循环会导致所有线程等待,当B,C,D为冻结,A唤醒了B,A也冻结,此时由于循环判断不该B执行,因此B也进入冻结,也就是ABCD均冻结

2):唤醒该唤醒的线程,因为无法决定唤醒哪个线程,因此使用notifyAll唤醒所有

错误的线程将进入等待,而正确的线程将执行

package com.helong.threadmessagetest2;
class Resource
{
	private String name;
	private boolean flag=false;
	private int count=1;
	private static final Resource r = new Resource();
	private Resource(){}
	public static Resource getInstance()
	{
		return r;
	}
	public synchronized void set(String name)
	{
		if(flag)//如果当前有可用的数据,先冻结set,等待print将数据使用后,再存数据
			try{this.wait();}catch(InterruptedException e){}
		this.name=name+"---"+count++;
		System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
		flag=true;
		this.notify();
	}
	public synchronized void print()
	{
		if(!flag)//如果当前没有可用数据,先冻结print,等待set将数据存入后,再打印数据
			try{this.wait();}catch(InterruptedException e){}
		System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);
		flag=false;
		this.notify();
	}
}
class Input implements Runnable
{
	public void run()
	{
		Resource r = Resource.getInstance();
		while(true)
		{
				r.set("商品");
		}
	}
}
class Output implements Runnable
{
	public void run()
	{
		Resource r = Resource.getInstance();
		while(true)
		{
			r.print();
		}
	}
}
class ThreadMessageTest2 
{
	public static void main(String[] args) 
	{
		new Thread(new Input()).start();
		new Thread(new Input()).start();
		new Thread(new Input()).start();
		new Thread(new Output()).start();
		new Thread(new Output()).start();
		new Thread(new Output()).start();
	}
}
运行图:



我们可以看到,出现了很多生产一次,却消费了多次的情况,这肯定是不允许的。使用解决方法解决后:

package com.helong.threadmessagetest2;
class Resource
{
	private String name;
	private boolean flag=false;
	private int count=1;
	private static final Resource r = new Resource();
	private Resource(){}
	public static Resource getInstance()
	{
		return r;
	}
	public synchronized void set(String name)
	{
		while(flag)//如果当前有可用的数据,先冻结set,等待print将数据使用后,再存数据
			try{this.wait();}catch(InterruptedException e){}
		this.name=name+"---"+count++;
		System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
		flag=true;
		this.notifyAll();
	}
	public synchronized void print()
	{
		while(!flag)//如果当前没有可用数据,先冻结print,等待set将数据存入后,再打印数据
			try{this.wait();}catch(InterruptedException e){}
		System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);
		flag=false;
		this.notifyAll();
	}
}
class Input implements Runnable
{
	public void run()
	{
		Resource r = Resource.getInstance();
		while(true)
		{
				r.set("商品");
		}
	}
}
class Output implements Runnable
{
	public void run()
	{
		Resource r = Resource.getInstance();
		while(true)
		{
			r.print();
		}
	}
}
class ThreadMessageTest2 
{
	public static void main(String[] args) 
	{
		new Thread(new Input()).start();
		new Thread(new Input()).start();
		new Thread(new Input()).start();
		new Thread(new Output()).start();
		new Thread(new Output()).start();
		new Thread(new Output()).start();
	}
}
运行图:



我们看到那种生产一次,消费多次的情况已经没有了。

12.多线程小练习

/*
本质原因:
	不管是生产,检查,消费三个线程按顺序执行,还是多个生产,消费的执行,都会
	产生混乱的情况,例如生产,检查,消费中的其中某一步突然不运行了,而只运行
	其他两步,或者多个生产,消费中的生产了1,2,结果只消费了2,或者生产了3,
	结果两次消费了3。本质都是因为多线程的随机性导致任何线程都可能抢到执行权,
	由于代码中只进行了一次判断,因此当这些错的线程抢到执行权时,已经不需要判断
	而直接运行代码,这时就有错误产生了。线程顺序执行也一样,可能跳过了某个线程
	直接运行后一个线程,但由于不需要判断因此也能运行。
解决方法:
	处理好两点即可:
		1.将判断改为需要多次判断
		2.使得唤醒到对的线程,错误的线程即便唤醒了也要再次进入wait
*/


/*
生产-》检查-》消费
生产,生产-》检查,检查-》消费,消费
产生的问题:
	按照原来的代码实现方式,总是到某一时刻就有线程不在执行了,此处为检查线程
原因:
	例如:线程A-》线程B-》线程C
	由于多线程的随机性,且notify本身也是随机唤醒一个冻结线程很可能线程A结束之后
	运行线程C,而由于在代码中判断只是一次,而如果上一次判断失败了线程wait,那么
	线程被notify后就不用判断了,无论是否该此线程运行,它都会运行,此时不管哪个线程
	抢到线程执行权,都无需判断,直接运行,因此产生了不确定的错误。
	总结:
		1.只判断一次
		2.正确的线程没有被唤醒
解决方法:
	方法1:
		(1)将if改为while(循环判断)
			将一次判断改为循环判断,这时即便错误的线程抢到执行权,也会再判断,从而
			进入wait冻结,但是如果此时错误的在运行的线程wait了,那所有线程就都wait
			了,程序也没法运行下去了,因此还有第二步。
		(2)将notify改为notifyAll(唤醒所有线程)
			这是将所有冻结线程唤醒,因为其中肯定有正确的线程,这些线程被唤醒开始抢
			执行权,如果是正确线程抢到,那就运行代码,如果是错误线程抢到,那就继续
			wait,同时让其他线程运行,因此可能能够让正确的线程运行。
	方法2:
		原来if(...){wait}
			.............
		改为if(...){wait,notify}
			else{........}
		(1)由于调用者是循环调用这些方法,因此线程从if中出来后又循环回去判断
		(2)当错误线程被运行时,它先在if中notify了一个其他线程,再循环判断后
			自己又进入了wait状态,交出执行权给其他可能正确的线程。
	
*/
package com.helong.threadtest2;
class Res
{
	private String name;
	private int count=1;
	private boolean haved=false;
	private boolean checked=true;
	private static Res r = new Res();
	private Res(){}
	public static Res getInstance()
	{return r;}

	public synchronized void set(String name)
	{
		if(haved)
		{
			try{wait();}catch(Exception e){}
			notify();
		}
		else
		{
			this.name=name+"----"+count++;
			System.out.println(Thread.currentThread().getName()+"生产者"+this.name);
			haved=true;
			checked=false;
			notify();
		}
	}
	public synchronized void check()
	{
		if(!haved||checked)
		{
			try{wait();}catch(Exception e){}
			notify();
		}
		else
		{
			System.out.println(Thread.currentThread().getName()+"........检查者"+this.name);
			checked=true;
			notify();
		}
	}
	public synchronized void out()
	{
		if(!haved||!checked)
		{
			try{wait();}catch(Exception e){}
			notify();
		}
		else
		{
			System.out.println(Thread.currentThread().getName()+"................消费者"+this.name);
			haved=false;
			notify();
		}
	}
}
class Producer implements Runnable
{
	public void run()
	{
		Res r = Res.getInstance();
		while(true)
		{
			r.set("时尚服饰");
		}
	}
}
class Checker implements Runnable
{
	public void run()
	{
		Res r = Res.getInstance();
		while(true)
		{
			r.check();
		}
	}
}
class Consumer implements Runnable
{
	public void run()
	{
		Res r = Res.getInstance();
		while(true)
		{
			r.out();
		}
	}
}
class  Thread2
{
	public static void main(String[] args) 
	{
		//单一的生产者,检查者,消费者情况
		//new Thread(new Producer()).start();
		//new Thread(new Checker()).start();
		//new Thread(new Consumer()).start();



		//多个生产者,检车者,消费者情况
		new Thread(new Producer()).start();
		new Thread(new Checker()).start();
		new Thread(new Consumer()).start();
		new Thread(new Producer()).start();
		new Thread(new Checker()).start();
		new Thread(new Consumer()).start();
		new Thread(new Producer()).start();
		new Thread(new Checker()).start();
		new Thread(new Consumer()).start();
	}
}

运行图:

我们可以看到,在代码控制下,即便是多个生产者,检查者,消费者,也能正确的运行,且每次只打印一条。

12.JDK1.5新特性

Lock,Condition代替synchronized。可以看做是一个显式的锁机制。
二者对比:
a.原始同步synchronized
一个锁对应一把钥匙,不管多少个生产者和消费者都使用这把钥匙,但是释放钥匙给谁这是不确定的,因此操作大量无效操作,本该给消费者的却给了生产者
b.Lock,Condition
Lock对应原来的同步锁,主要方法lock,unlock
Condition对应原来的等待唤醒机制,主要方法await(wait),signal(notify),signalAll(notifyAll)
每个Lock对象,可以关联多个Condition对象,Condition对象可以通过Lock的newCondition()获取,Condition就好像锁上的不同的钥匙,但这些钥匙都能开锁,例如生产者与消费者例子,需要两个Condition对象,一个提供给生产者们使用,当需要wait时,调用该Condition的await方法,当需要唤醒对方线程时,调用对方Condition的signal方法,这就达到了单独唤醒对方的操作
本质:
之所以Lock,Condition机制能做到唤醒对方,是因为一个Lock对应着多个Condition,每一方各使用一个Condition,这就使得唤醒对方非常简单,只需调用对方的Condition的signal就可以了

package com.helong.threadmessagetest2;
import java.util.concurrent.locks.*;
class Resource
{
	private String name;
	private boolean flag=false;
	private int count=1;
	private Lock lock=new ReentrantLock(); 
	Condition setCon=lock.newCondition();  
    	Condition printCon=lock.newCondition();  
	private static final Resource r = new Resource();
	private Resource(){}
	public static Resource getInstance()
	{
		return r;
	}
	public void set(String name)throws InterruptedException
	{
		lock.lock();
		try
		{
			while(flag)//如果当前有可用的数据,先冻结set,等待print将数据使用后,再存数据
				setCon.await();
			this.name=name+"---"+count++;
			System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
			flag=true;
			printCon.signal();
		}
		finally
		{
			lock.unlock();
		}
	}
	public void print()throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag)//如果当前没有可用数据,先冻结print,等待set将数据存入后,再打印数据
				printCon.await();
			System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);
			flag=false;
			setCon.signal();
		}
		finally
		{
			lock.unlock();
		}
	}
}
class Input implements Runnable
{
	public void run()
	{
		Resource r = Resource.getInstance();
		while(true)
		{
				try{r.set("商品");}catch(Exception e){}
		}
	}
}
class Output implements Runnable
{
	public void run()
	{
		Resource r = Resource.getInstance();
		while(true)
		{
			try{r.print();}catch(Exception e){}
		}
	}
}
class ThreadMessageTest2 
{
	public static void main(String[] args)
	{
		new Thread(new Input()).start();
		new Thread(new Input()).start();
		new Thread(new Input()).start();
		new Thread(new Output()).start();
		new Thread(new Output()).start();
		new Thread(new Output()).start();
	}
}
运行图:




13.结束线程

老版本使用stop方法,但是该方法不管线程处于什么状态都结束它,有BUG,因此现在已经过时了
结束线程的本质是使run方法结束,通常run方法中是循环结构。因此我们要做的就是跳出循环
通过设置标记,对外提供改变标记的方法来结束循环
该方法在线程处于冻结状态时会失败,因为线程冻结了,也就不会去判断标记了,也就不会结束,而是停在wait处无限等待
解决方法:使用interrupt,该方法会强行唤醒由wait,sleep,join导致的冻结状态,使线程恢复到运行状态中来,那么线程就可以继续判断标记,由于更改了标记,因此判断失败,结束run方法。interrupt方法时强行将线程唤醒,因此会引发一个InterruptedException异常,我们可以将结束线程的过程写到该异常的catch中

/*
结束线程:
1.使用Thread的stop方法,但是该方法已经过时了,原因是会有BUG。
2.结束的本质是run方法结束,也就是说能控制run方法中的循环结构也就能结束线程。

特殊情况:
当run方法有同步时,此时线程0,1进入run方法,判断成功,进入wait冻结,轮到
main执行,即便改变了标记,由于线程0,1都在冻结状态,因此进入了main线程结束,
线程0,1一直冻结的状态。

解决方法:interrupt:清除由wait,sleep,join引发的冻结状态
清除线程0,1的冻结状态,使用Thread类的interrupt方法,该方法的功能是在线程处于
冻结(中断)状态时,强行唤醒线程,清除线程的冻结状态,使线程恢复到运行状态,
因此在一个线程不想醒时叫醒它,所以发生了InterruptedException异常。
*/
package com.helong.stopthreadtest;
class StopThread implements Runnable
{
	private boolean flag=true;
	public /*synchronized*/ void run()
	{
		while(flag)
		{
			/*try
			{
				wait();
			}
			catch (InterruptedException e)
			{
				System.out.println(Thread.currentThread().getName()+"....exception");
			}
			*/
			System.out.println(Thread.currentThread().getName()+"...run");
		}
	}
	public void stop()
	{
		flag=false;
	}
}
class StopThreadTest 
{
	public static void main(String[] args) 
	{
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		t1.setDaemon(true);
		t1.start();
		Thread t2 = new Thread(st);
		t2.setDaemon(true);
		t2.start();
		int i=0;
		while(true)
		{
			if(i++==60)
				/*
					当i++==60时,即之后i为61,运行st.stop改变标记,跳出循环,main线程结束,
					轮到线程0,1运行时,都是先打印一次,再判断标记,为false,跳出循环,结束
					线程。可得:线程0,1每次停止的地方都在判断后,因此轮到他们执行时会先打印
					一次,再判断失败跳出来。
				*/
			{
				//st.stop();//这一步也可以省略,在异常处理catch中结束循环,因为只要进入catch说明线程被强制唤醒。
				//t1.interrupt();
				//t2.interrupt();
				//由于两个线程进入了冻结状态,如果不强行唤醒,那么改变标记也没用。
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......"+i);
		}
	}
}

可以看到,我们并没有强制结束程序,但是线程依然自动结束了。


14.守护线程

也叫用户线程,后台线程

Thread方法setDaemon(true);将一个线程设置为后台线程,当所有正在运行的线程都是后台线程时,jvm结束

注意:

a.该方法必须在开启线程前执行

b.当一个进程中,前台线程都结束了,那么该进程也就结束了,不管后台进程的run方法是否结束

比喻:

前台线程相当于雅典娜们,守护线程相当于圣斗士们,当雅典娜们都over了,那么圣斗士们也就都失业了


15.小知识点

join方法:

在线程A代码中有线程B.join();该方法是让线程B进入到运行状态,而线程A进入冻结状态,直到线程B结束为止,但是不针对别的线程也就是说如果还有线程C在运行,那么就是线程B,C交替运行,而对于线程A来说,C与其无关,只有B结束,A才能从冻结状态中被唤醒
注意:
该方法会有InterruptedException异常,因为如果线程B进入了冻结状态,那么此时线程A,B就都进入了冻结状态,必须使用interrupt方法来强行唤醒,因此有该异常
package com.helong.joindemo;
class Join implements Runnable
{
	public void run()
	{
		for(int x=0;x<50;x++)
			System.out.println(Thread.currentThread().getName()+"......."+x);
	}
}
class JoinDemo 
{
	public static void main(String[] args) throws Exception
	{
		Join j=new Join();
		Thread t1 = new Thread(j);
		Thread t2 = new Thread(j);
		
		//线程main,0,1交替执行。
		/*
		t1.start();
		t2.start();
		for(int x=0;x<50;x++)
			System.out.println(Thread.currentThread().getName()+"......."+x);
		*/

		//主线程运行到t1.start()开启线程0,然后可能运行线程0的代码打印,或者继续向下执行t1.join主线程进入冻结,执行
		//线程0的代码打印,因此两种可能的结果都一样,那就是先打印完线程0的,然后主线程和线程1交替运行。
		/*
		t1.start();
		t1.join();
		t2.start();
		for(int x=0;x<50;x++)
			System.out.println(Thread.currentThread().getName()+"......."+x);
		*/

		//主线程创建了线程0,1,然后可能0,1交替运行也可能主线程继续执行t1.join,主线程进入冻结,结果都一样,那就是
		//线程0,1交替运行知道线程0结束,主线程被唤醒,此时可能线程1运行结束,那么就是单独运行主线程,或者线程1
		//未结束,那么就是主线程和线程1交替运行。
		/*
		t1.start();
		t2.start();
		t1.join();
		for(int x=0;x<50;x++)
			System.out.println(Thread.currentThread().getName()+"......."+x);
		*/

		//主线程在运行到t1.join时进入冻结状态,当线程0结束,主线程恢复时,又运行t2.join进入冻结,当线程1结束,主线程
		//再次恢复运行,知道主线程结束。因此这种情况运行结果是线程0,1交替运行知道两个都结束,再运行主线程。
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		for(int x=0;x<50;x++)
			System.out.println(Thread.currentThread().getName()+"......."+x);
	}
}
运行图:


线程优先级:

可以通过setPriority(1-10)来设置,通常设置为MIN_PRIORITY(1),NORM_PRIORITY(5),MAX_PRIORITY(10)
优先级高的线程抢到CPU执行权的几率更大,但不是一定都能抢到

yield方法(放弃)

使当前进程进入冻结状态,使其他线程进入运行状态


16.多线程总结

通过一些练习,基本掌握了多线程的使用方法,但是还是缺少对多线程的使用经验,不太明白到底什么样的情况适用于多线程。但是这些经验也不是说有就有的,这是通过以后学习工作中使用到的时候来掌握了。



------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:7475次
    • 积分:311
    • 等级:
    • 排名:千里之外
    • 原创:25篇
    • 转载:0篇
    • 译文:0篇
    • 评论:1条
    文章存档