java基础--26.死锁问题的出现和解决

Posted 大数据小小罗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java基础--26.死锁问题的出现和解决相关的知识,希望对你有一定的参考价值。

同步弊端:

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

死锁问题及其代码重现

死锁:

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

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

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

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

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

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

//定义锁对象
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执行程序时的随机性
解决方法
利用 等待 – 唤醒 机制,让两个线程交替执行

以上是关于java基础--26.死锁问题的出现和解决的主要内容,如果未能解决你的问题,请参考以下文章

Java并发基础使用“等待—通知”机制优化死锁中占用且等待解决方案

Java死锁排查和Java CPU 100% 排查的步骤整理

Java死锁排查和Java CPU 100% 排查的步骤整理(转)

MySQL基础知识

JAVA笔记(20)--- 死锁;如何解决线程安全问题;守护线程;定时器;Callable 实现线程;wait ( ) 和 notify ( ) ;实现生产者和消费者模式;

MarkLogic Java API 死锁检测