Java从入门到删库跑路——多线程的同步学习(2)

一.死锁

一旦有多个进程,且它们都要争用对多个锁的独占访问,那么就有可能发生死锁。如果有一组进程或线程,其中每个都在等待一个只有其他进程或线程才可以进行的操作,那么就称它们被死锁了。

要避免死锁,应该确保在获取多个锁时,在所有的线程都用相同的顺序获取锁

在下面的例子中,程序创建了两个类A和B,它们分别具有方法funA()和funB(),在调用对方的方法前,funA()和funB()都睡眠一会儿。主类DeadLockDemo创建A和B实例,然后产生第2个线程以构成死锁条件。funA()和 funB()使用sleep()方法来强制死锁条件出现。而在真实程序中,死锁是较难发现的。

package dataStructure;

class A {
	synchronized void funA(B b) {
		String name = Thread.currentThread().getName();
		System.out.println(name+" 进入 A.foo");
		try {
			Thread.sleep(1000);
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}
		System.out.println(name + "调用B中的last()");
		b.last();
	}
	synchronized void last() {
		System.out.println("A类中的last()");
	}
	
	
}

class B {
	synchronized void funB(A a) {
		String name = Thread.currentThread().getName();
		System.out.println(name+" 进入B类中");
		try {
			Thread.sleep(1000);
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}
		System.out.println(name + "调用A中的last()");
		a.last();
	}
	synchronized void last() {
		System.out.println("B类中的last()");
	}		
}
class DeadLockdemo implements Runnable{
	A a =new A();
	B b =new B();
	DeadLockdemo(){
		//设置当前线程的名称
		Thread.currentThread().setName("Main >>Thread");
		new Thread(this).start();
		a.funA(b);
		System.out.println("main  ----end");
	}
	public void run() {
		Thread.currentThread().setName("Test >>Thread");
		b.funB(a);
		System.out.println("其他线程运行完毕");
	}
	public static void main(String[] args) {
		new DeadLockdemo();
	}
	
	
}

从运行结果可以看到,Test  >> Thread 进入了b的监视器,然后又在等待a的监视器。同时Main  >> Thread进入了a的监视器,并等待b的监视器。这个程序永远不会完成。

二.线程间通信 

此时数据存储空间包含两部分(一部分存储姓名,一部分存储年龄)

此时有两个线程:生产者线程(向数据存储空间添加数据);消费者线程(从数据存储空间取出数据)

 

可能出现的问题:

1.假设生产者线程刚向数据存储空间中添加了一个人的姓名,还没有加入这个人的性别,CPU就切换到了消费者线程,消费者线程则把这个人的姓名和上一个人的性别联系到了一起。

2.生产者放入了若干次数据,消费者才开始取数据,或者是,消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已取过的数据。

package dataStructure;

public class ThreadCommunication {

	public static void main(String[] args) {
		P q= new P();
		new Thread(new Producer(q)).start();
		new Thread(new Consumer(q)).start();
		
	}
}

class P{
	String name ="鲲";
	String sex ="男";
}

class Producer implements Runnable{
	P q=null;
	public Producer(P q) {
		// TODO Auto-generated constructor stub
		this.q =q;
	}
	public void run() {
		int i=0;
		while (true) {
			if(i==0) {
				q.name="鹿";
				q.sex="女";
			}
			else {
				q.name ="鲲";
				q.sex ="男";
			}
			i=(i+1)%2;
			
		}
		
	}
}
class Consumer implements Runnable{
	P q=null;
	public Consumer(P q) {
		// TODO Auto-generated constructor stub
		this.q =q;
	}
	public void run() {
		while (true) {
			System.out.println(q.name+"--->"+q.sex);
		}
		
	}
}

 

从程序中可以看到,Producer 类和Consumer类都是操纵了同一个P类,这就有可能Producer类还未操纵完P类,Consumer 类就已经将P类中的内容取走了,这就是资源不同步的原因。为此可以在P类中增加两个同步方法: set()和 get(),具体代码如下所示。

package dataStructure;

public class ThreadCommunication {

	public static void main(String[] args) {
		P q= new P();
		new Thread(new Producer(q)).start();
		new Thread(new Consumer(q)).start();
		
	}
}

class P{
	private String name ="鲲";
	private String sex ="男";
	public synchronized void set(String name,String sex) {
		this.name= name;
		this.sex= sex;
	}
	public synchronized void get() {
		System.out.println(this.name+"---->"+this.sex);
	}
}

class Producer implements Runnable{
	P q=null;
	public Producer(P q) {
		// TODO Auto-generated constructor stub
		this.q =q;
	}
	public void run() {
		int i=0;
		while (true) {
			if(i==0) {
				q.set("鹿", "女");
			}
			else {
				q.set("鲲", "男");
			}
			i=(i+1)%2;
			
		}
		
	}
}
class Consumer implements Runnable{
	P q=null;
	public Consumer(P q) {
		// TODO Auto-generated constructor stub
		this.q =q;
	}
	public void run() {
		while (true) {
			q.get();
		}
		
	}
}

可以看到程序的输出结果是正确的。但是这里又有一个新的问题产生了,从程序的执行结果来看,Consumer 线程对Produce线程放入的一次数据连续地读取了多次,这并不符合实际的要求。实际要求的结果是,Producer 放一次数据,Consumer 就取一次; 反之,Producer 也必须等到Consumer取完后才能放入新的数据,而这个问题的解决就需要使用下面要讲到的线程间的通信。Java是通过Object类的wait、notify、 notifyAll 这几个方法来实现线程间的通信的,又因为所有的类都是从Object继承的,所以任何类都可以直接使用这些方法。

下面是这3个方法的简要说明。

wait:告诉当前线程放弃监视器并进入睡眠状态,直到其他线程进入同一监视器并调用notify为止。

notify:唤醒同一对象监视器中调用wait 的第1个线程。这类似排队买票,一个人买完之后,后面的人才可以继续买。

notifyAll:唤醒同一对象监视器中调用wait的所有线程,具有最高优先级的线程首先被唤醒并执行。

如果想让上面的程序符合预先的设计需求,就必须在类P中定义一个新的成员变量bFull来表示数据存储空间的状态。当Consumer线程取走数据后,bFull 值为false, 当Producer 线程放入数据后,bFull 值为true。只有bFull为true 时,Consumer 线程才能取走数据,否则就必须等待Producer线程放入新的数据后的通知;反之,只有bFull为false, Producer 线程才能放入新的数据,否则就必须等待Consumer 线程取走数据后的通知。

package dataStructure;

public class ThreadCommunication {

	public static void main(String[] args) {
		P q= new P();
		new Thread(new Producer(q)).start();
		new Thread(new Consumer(q)).start();
		
	}
}

class P{
	private String name ="鲲";
	private String sex ="男";
	boolean bFull = false;
	public synchronized void set(String name,String sex) {
		if (bFull) {
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO: handle exception
			}			
		}
		this.name =name ;
		try {
			Thread.sleep(10);
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}
		this.sex= sex;
		bFull =true;
		notify();
	}
	public synchronized void get() {
		if (!bFull) {
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO: handle exception
			}
			
		}
		System.out.println(name+"---->"+sex);
		bFull =false;
		notify();
	}
}

class Producer implements Runnable{
	P q=null;
	public Producer(P q) {
		// TODO Auto-generated constructor stub
		this.q =q;
	}
	public void run() {
		int i=0;
		while (true) {
			if(i==0) {
				q.set("鹿", "女");
			}
			else {
				q.set("鲲", "男");
			}
			i=(i+1)%2;
			
		}
		
	}
}
class Consumer implements Runnable{
	P q=null;
	public Consumer(P q) {
		// TODO Auto-generated constructor stub
		this.q =q;
	}
	public void run() {
		while (true) {
			q.get();
		}
		
	}
}

本程序满足了设计的需求,解决了线程间通信的问题。 

wait、notify、notifyAll这3个方法只能在synchronized方法中调用,即无论线程调用一个对象的wait 还是notify方法,该线程必须先得到该对象的锁标记。这样,notify 就只能唤醒同一对象监视器中调用wait的线程。而使用多个对象监视器,就可以分别有多个wait、notify 的情况,同组里的wait只能被同组的notify唤醒。

一个线程的等待和唤醒过程可以用下图表示。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值