多线程等待唤醒机制:从wait()和sleep()的差别说起

1.

wait():释放资源,释放锁

sleep():释放资源,不释放锁


wait():Object的方法,用在同步当中,是同步锁的方法,以锁控制线程

sleep():线程类Thread本身的静态方法

wait(),notify(),notifyAll()方法是用在同步当中的:必须是同步当中的同一把锁操作线程。所以这几个方法是Object的方法。试想想不在同步中的多线程,由于抢夺执行权结果不定,控制无意义,线程间也无法通信:怎样知道当前是哪个线程呢?怎样捕获这个线程操作它呢?


同步的窍门:如果加了同步还有线程安全问题,想两个前提:1,是不是两个或两个以上线程 2.用的是不是同一把锁

只同步了一个线程的代码是不行的,要将所有线程运行的代码都用同一把锁同步,这样一个执行同步代码,其他的判断同一把锁,才无法进入自己执行的代码

即使在一个Runnable的操作代码中是一句, 要同步的也不是一句:是几个要同步的线程要操作的所有代码


例子:

class Person
{
	String name;
	String gender;
	boolean flag=false;//标记,默认不输出
}
class Input implements Runnable
{
	private Person p;
	private int n=0;
	public Input(Person p){
		this.p=p;
	}

	public void run(){
		//别忘了while(true)!!
		while(true){
		synchronized(p){//两个Runnable用同一个Person初始化,就可保证p是同一个且唯一
			if(p.flag){
				try{p.wait();}catch(InterruptedException e){}
				
			}
			//not "else"
			if(n==0){
				p.name="Jason";
				p.gender="男";
			}else{
				p.name="Lily";
				p.gender="女";
			}
			n=(n+1)%2;
			p.flag=true;//修改标志位
			p.notify();//唤醒另一个
		}
		}
	}

}

class Output implements Runnable
{
	private Person p;
	public Output(Person p){
		this.p=p;
	}

	public void run(){
		//别忘了while(true)!!
		while(true){
		synchronized(p){
			//仍然是判断标志位,和上面的线程轮流等待,唤醒
			if(!p.flag){
				try{p.wait();}catch(InterruptedException e){}
			}
			System.out.println(p.name+"..."+p.gender);
			//别忘了改变标志位和唤醒另一个
			p.flag=false;//让自己回来等待
			p.notify();//同一个锁,唤醒锁上的另一个线程
		}//问题:一个线程再次进入锁,等待,另一个进程在此前唤醒了,此时能进入锁执行吗?-->能,wait()即会释放锁,也释放执行资格!
		}
	}
}

public class InputOutputWaitAndNotify  
{
	public static void main(String[] args) 
	{
		Person p=new Person();

		new Thread(new Input(p)).start();
		new Thread(new Output(p)).start();
		System.out.println("Hello World!");
	}
}

结果:

Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男


问题:

一个线程再次进入锁,等待,另一个进程在此前唤醒了,此时能进入锁执行吗?

解答:wait()是释放资源(放弃执行资格)并释放锁的,这样另一个线程得以进入并执行

2.一个不用锁和用锁的演化探讨:未经权威证实和反复确认的都是错的,多线程往往实验结果看似正确也是错的!

不加锁:用一个if块中的标志位判断和修改:

class Person
{
	String name;
	String gender;
	boolean flag=true;
}

class Input implements Runnable
{

	public Input(Person p){
		this.p=p;
	}

	private int n=0;
	private Person p;

	public void run(){//循环,并轮流执行的业务-->由简模型向完整模型演化,不犹疑,流利,快速思考!!
		
		
		while(true){
			try{Thread.sleep(1000);}catch(InterruptedException e){}

			if(p.flag==true){
				
				//try{Thread.sleep(1000);}catch(InterruptedException e){}
				if(n%2==0){
					p.name="Json";
					p.gender="男";
				}else{
					p.name="Lily";
					p.gender="女";
				}
				n=n+1;
				
				
				p.flag=false;//这样下次即使抢到也无法执行内容
			}//一个结尾改变状态的if语句避免多线程造成的错误
			
		}
		
	}
}

class Output implements Runnable
{
	public Output(Person p){
		this.p=p;
	}

	private Person p;

	public void run(){
		while(true){//这个程序就是无法高效利用线程:有一半左右的时间里抢到执行权的线程虽然运行但执行的业务是空的,对业务效率来说很浪费,只有一半左右
				
				try{Thread.sleep(1000);}catch(InterruptedException e){}
				if(p.flag==false){//即使上面线程执行一半这里抢到,也因flag未及修改而无法执行内容
					//try{Thread.sleep(1000);}catch(InterruptedException e){}
					System.out.println(p.name+"...."+p.gender);
					
				
				p.flag=true;
			}
			
		}
		
	}
}

public class InputOutputOpera0  
{
	public static void main(String[] args) 
	{
		Person p=new Person();
		Input in=new Input(p);
		Output out=new Output(p);
		new Thread(in).start();
		new Thread(out).start();

		System.out.println("Hello World!");
	}
}


增加破坏性实验:在各种地方设置线程sleep

结果:

Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女
Json....男
Lily....女

这个程序的正确性仍有待验证:实际运行结果和假想线程在各处尤其是结束if判断处被抢走执行权导致判断失效、错位的推演仍有可能存在错误。最保险的是不断进行破坏性条件的实验,和在能同步的业务上都加上同步,保证线程安全。可以只将需要同步的同步,将它们分成小块,只要保证用的是同一把锁。


我们将上面的程序需要的部分加上同步,结果是不变的,但这种设计仍然让人觉得不确定不保险,专业的做法仍然是第一个例子:标志位+轮流等待唤醒机制,这样可以保证线程安全(等待唤醒是锁上的机制,本来就只能在同步中使用),又可以保证两个线程轮替执行各自的业务代码(实际上抢夺执行权仍然是无序的,但我们设置的等待唤醒确保一个线程即使连续抢到执行权也可释放锁,释放执行权给另一方(而本方不再执行),保证交替和线程业务效率):

class Person
{
	String name;
	String gender;
	boolean flag=true;
}

class Input implements Runnable
{

	public Input(Person p){
		this.p=p;
	}

	private int n=0;
	private Person p;

	public void run(){//循环,并轮流执行的业务-->由简模型向完整模型演化
		
		//如果在这里同步,另一个线程就永远没有执行机会了
		while(true){
			if(p.flag==true){
				//把该同步的同步起来
				synchronized(p){//同一个锁
				if(n%2==0){
					p.name="Json";
					p.gender="男";
				}else{
					p.name="Lily";
					p.gender="女";
				}
				n=n+1;
				p.flag=false;
				}
				//p.flag=false;
			}
			
		}
		
	}
}

class Output implements Runnable
{
	public Output(Person p){
		this.p=p;
	}

	private Person p;

	public void run(){
		while(true){
			if(p.flag==false){//这样代码中修改了flag下一次即使它抢到执行权也不打印
				//如果这里不用同一个锁同步,那么上面的同步也失去作用,因为两个线程还是各行其是,这里的线程不需要锁,抢夺到
				//执行权即可打印,这样仍然导致错误
				synchronized(p){
					System.out.println(p.name+"...."+p.gender);
					p.flag=true;
				}//同步后的问题:两线程抢夺不定,无法交替打印男女:那么加上标志位,保证交替执行业务!!
				
				//p.flag=true;
			}
			
		}
		
	}
}

public class InputOutputOpera  
{
	public static void main(String[] args) 
	{
		Person p=new Person();
		Input in=new Input(p);
		Output out=new Output(p);
		new Thread(in).start();
		new Thread(out).start();

		System.out.println("Hello World!");
	}
}

为什么加标志位:如果只是同步代码块,线程执行权抢夺是不定的,无法保证两个线程轮替执行业务,但轮替执行业务部代表线程被百分之百利用于做业务,因为在没有等待唤醒机制的这种程序里,连续抢夺到执行权的线程不是真的不执行,而只是执行内容为空而已,就像第二个例子。


你可能感兴趣的:(Java,多线程,面试)