死锁问题的出现和解决

同步弊端:

  • 效率低
  • 如果出现了同步嵌套,就容易产生死锁问题

死锁问题及其代码重现

死锁:

  是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象 
这里写图片描述

举例: 
中国人、美国人吃饭案例

正常情况: 
中国人:筷子两支 
美国人:刀和叉

现在: 
中国人:筷子一支,刀一把 
美国人:筷子一支,叉一把

产生死锁问题: 
中国人拿着刀的同时等着美国人把另一只筷子给他,美国人拿着一支筷子的同时等着中国人把刀给他

同步代码块的嵌套案例–重现死锁现象

//定义锁对象
public class MyLock {
    public static final Object objA = new Object();
    public static final Object objB = new Object();
}

//定义Thread类的run()方法
public class DeadLock extends Thread {
    //定义一个flag变量,用来标记此时持有的是哪个锁
    private boolean flag;

    public DeadLock(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run() {
        if(flag){
            synchronized (MyLock.objA) {
                System.out.println("if objA");
                synchronized (MyLock.objB) {
                    System.out.println("if objB");
                }
            }
        }else{
            synchronized (MyLock.objB) {
                System.out.println("if objB");
                synchronized (MyLock.objA) {
                    System.out.println("if objA");
                }
            }
        }
    }
}

//测试用例
public class DeadLockDemo {

    public static void main(String[] args) {
        DeadLock dl1 = new DeadLock(true);
        DeadLock dl2 = new DeadLock(false);

        dl1.start();
        dl2.start();
    }

}
  • 两个线程一旦启动,则将陷入互相等待的循环之中,就成为了死锁

死锁问题的解决

解决方式1:线程间通信–通过构造方法共享数据

有一个Student类作为资源(锁)

public class Student {
    /*
     * 默认权限修饰符,是为了让同一个包下的其他类也能访问到其成员变量
     */
    String name;
    int age;
    boolean flag; //默认情况下false:没有数据,如果是true:说明有数据
}

模拟生产者-消费者模型: 
  创建两个线程对象,一个是设置 Student 属性的线程 SetThread ,另一个是获取 Student 属性的线程 GetThread

public class SetThread implements Runnable {


    private Student st ;
    private int x=0;
    public SetThread(Student stu) {
        this.st = stu;
    }

    @Override
    public void run() {
        while(true){
            synchronized (st) {
                //判断有没有数据
                if(st.flag){
                    try {
                        st.wait();  //如果有数据就等待消费者消费
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                if(x%2 == 0){
                    st.name = "紫霞仙子";
                    st.age=27;
                }else{
                    st.name = "刘意";
                    st.age=30;
                }
                x++;
                //修改标记
                st.flag = true;
                //唤醒线程
                st.notify();


            }
        }
    }

}



public class GetThread implements Runnable {


    private Student st ;
    public GetThread(Student stu) {
        this.st = stu;
    }

    @Override
    public void run() {
        while(true){
            synchronized (st) {//加锁,防止出现姓名--年龄不匹配的问题
                if(!st.flag){   //没有数据等待
                    try {
                        st.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println(st.name+"--------"+st.age);
                //修改标记
                st.flag = true;
                //唤醒线程
                st.notify();

            }
        }

    }

}

测试用例:

public class StudentDemo {
    public static void main(String[] args) {
        Student stu = new Student();

        SetThread st = new SetThread(stu);
        GetThread gt = new GetThread(stu);

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

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

}

运行结果:

...
紫霞仙子--------27
紫霞仙子--------27
紫霞仙子--------27
紫霞仙子--------27
紫霞仙子--------27
刘意--------30
刘意--------30
刘意--------30
刘意--------30
刘意--------30
...
  • 发现结果并不是那么理想,同一组数据出现多次 
    出现这种状况的原因: 
    CPU的一点点时间片执行时间,足以让一个线程中的代码执行多次

解决方式2:改进1:通过构造方法共享数据,并使用 (等待–唤醒) 机制实现线程间通信

存在问题:

  • A:如果消费者先抢到了CPU的执行权,就会去消费数据,但是现在的数据是默认值。

    这样没有意义,应该等到数据有意义再去消费

  • B:如果生产者先抢到CPU的执行权,就会去生产数据,但是生产完数据之后,还拥有执行权它会继续生产。

    这是有问题的,应该等待消费者将数据消费掉才能继续生产。

等待唤醒机制

这里写图片描述

A:生产者—先看看是否有数据,有就等待;没有就生产,然后通知消费者进行消费

B:消费者—先看看是否有数据,有就消费,消费完通知生产者继续生产;没有就等待

Object类中提供了3个方法:

    wait():等待
    notify():唤醒单个线程
    notifyAll():唤醒所有线程

问:wait和notify方法为什么不定义在Thread类上而是Object类上呢?

  这些方法的调用必须通过锁对象来调用,而我们刚才使用的锁对象是任意对象 
  所以,这些方法必须定义在Object类中。

解决方式3:改进2:私有化锁对象的成员变量,自己提供get和set方法供外界调用

方式2中,同样也存在不适用的情况:一旦Student类中的成员变量被private修饰符私有化,那么其他Thread类就无法直接访问到其成员变量了。 
解决方法: 
Student自己实现get和set方法,外部Thread类直接调用其方法进行数据交换即可。

public class Student {
    /*
     * 一旦权限修饰符改为private,其他类就无法访问到
     * 改进:
     *      将set和get方法写在 Student类中
     */
    String name;
    int age;
    boolean flag; //默认情况下没有数据,如果是true,说明有数据
    //student的set和get过程(包括同步)都是由自己来操作,外界调用就行
    public synchronized void set(String name, int age){
        //如果有数据就等待
        if(this.flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //设置数据
        this.name = name;
        this.age = age;

        //修改标记并唤醒线程
        this.flag = true;
        this.notify();
    }

    public synchronized void get(){
        //如果没有数据则等待
        if(!this.flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //获取数据
        System.out.println(this.name +"---"+this.age);
        //修改标记并唤醒线程
        this.flag = false;
        this.notify();
    }
}


/**
 * 生产者类
 * @author llj
 *
 */

public class SetThread implements Runnable {

    private Student st ;
    private int x=0;
    public SetThread(Student stu) {
        this.st = stu;
    }

    @Override
    public void run() {
        while(true){
                if(x%2 == 0){
                    st.set("紫霞仙子", 27) ;
                }else{
                    st.set("刘意", 30) ;
                }
                x++;
        }

    }
}

/**
 * 消费者类
 * @author llj
 *
 */
public class GetThread implements Runnable {

    private Student st ;
    public GetThread(Student stu) {
        this.st = stu;
    }

    @Override
    public void run() {
        while(true){
                st.get();
        }

    }

}

测试用例:
public class StudentDemo {

    public static void main(String[] args) {

        Student stu = new Student();

        //将student引用作为参数传递实现线程间通信
        SetThread st = new SetThread(stu);
        GetThread gt = new GetThread(stu);

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        t1.start();
        t2.start();
    }
}
  • 运行结果:
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
紫霞仙子---27
刘意---30
...

​​​​​​​总结

A.姓名和年龄出现不匹配

原因: 
线程运行的随机性 
解决方法: 
  加锁,在一个线程对共享资源操作的时候,其他线程只能等待该锁释放 
注意: A: 不同种类的线程都要加锁 
    B: 不同种类的线程加的锁都必须是同一把

B.执行结果重复出现

原因: 
CPU的一点点时间片执行时间,足以让一个线程中的代码执行多次 
 CPU执行程序时的随机性 
解决方法: 
利用 等待 – 唤醒 机制,让两个线程交替执行

posted @ 2017-09-19 20:27  車輪の唄  阅读(10)  评论(0编辑  收藏  举报  来源