一.死锁
一旦有多个进程,且它们都要争用对多个锁的独占访问,那么就有可能发生死锁。如果有一组进程或线程,其中每个都在等待一个只有其他进程或线程才可以进行的操作,那么就称它们被死锁了。
要避免死锁,应该确保在获取多个锁时,在所有的线程都用相同的顺序获取锁
在下面的例子中,程序创建了两个类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唤醒。
一个线程的等待和唤醒过程可以用下图表示。