自学内容网 自学内容网

Java多线程编程(三)一>详解synchronized, 死锁,wait和notify

目录: 

一.synchronized 的使用:    

二. 常见死锁情况: 

三 .如何避免死锁:  

四.wait和notify

一.synchronized 的使用: 

我们知道synchronized锁具有互斥的特点:
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行 到同⼀个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

那他如何使用呢?有什么注意事项? 

 

明确指定锁哪个对象: 

public class SynchronizedDemo {
 private int count;
 private Object locker = new Object();
 
 public void method() {
 synchronized (locker) {
     count++;
 }

 }

}

锁当前对象:
public class SynchronizedDemo {
  public void method() {
    synchronized (this) {
   
    }
  }
}

直接修饰普通方法或者静态方法: 
public class SynchronizedDemo {
 public synchronized void methond() {
 }
}
public class SynchronizedDemo {
 public synchronized static void method() {
 }
}

注意:两个线程竞争同⼀把锁, 才会产生阻塞等待. 



二. 常见死锁情况:

大致理解死锁:
⼀个线程没有释放锁, 然后⼜尝试再次加锁.
第⼀次加锁, 加锁成功
第⼆次加锁, 锁已经被占⽤, 阻塞等待.

 

我们知道在使用锁的时候不仅要考虑适不适合,可能还会用处一些问题,可能产生死锁的情况下面我们来介绍常见死锁情况和如何避免 

 


1.可重入锁(一个线程一把锁): 

一个线程连续对同一把锁加锁两次,不会触发阻塞等待

public class Demo {
    public static int count;

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                for (int i = 0; i < 50000; i++) {
                    synchronized (locker) {
                        synchronized (locker) {
                            count++;
                        }
                    }
                }
            }

            System.out.println("t1 结束");
        });

        t1.start();
    }
}


不可重入锁,就是在上面可重入锁情况下阻塞等待,但是Java中没有不可重入锁。


2.两个线程两把锁:

这个情况是当每个线程获取到一把锁的时候,不解锁,尝试去获取另一个线程的锁 

代码:这里注意先让t1睡眠一下,让t1拿t2占据的锁 

public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1线程两个锁都获取到了");
                }
            }

            System.out.println("t1 结束");
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {

                synchronized (locker1){
                    System.out.println("t2线程两个锁都获取到了");
                }
            }
        });

        t1.join();
        t2.join();

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

死锁状态: 


3. N个线程M把锁:

这里我们引入一个哲学家就餐问题: 

哲学家想吃到面条,就要拿到两双筷子和两把锁,这里5个哲学家就相当于5个线程,5把筷子就相当于5把锁,大部分情况下哲学家都可以吃到面条

但是极端情况下:每个哲学家都想吃面条,同时拿起左手的筷子,这时候每个哲学家已经拿不了右手的筷子了 
 



三 .如何避免死锁: 

要避免死锁我们就要知道死锁的构成:

 构成死锁的四个必要条件:

1.锁是互斥的:一个线程拿到锁另一个线程要想拿到这个锁就要阻塞等待 

2.锁是不可抢占的:当t1线程拿到锁还没解锁情况下,t2线程想拿到这个锁,必须阻塞等待,达到t1释放锁。  

 

3.请求和保持:当一个线程t1拿到锁,在不释放锁的前提下,去拿另一把锁。

4.循环等待:像哲学家就餐的极端情况下,多个线程多把锁之间构成循环,A等待B,B也等待A... 

 


1,2两条是打破不了的,属于锁的基本特性了我们可以打破 3,4: 

打破请求和保持:不要嵌套的加锁: 

public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (locker2){
                System.out.println("t1");
            }

            System.out.println("t1 结束");
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            synchronized (locker1){
                System.out.println("t2");
            }

            System.out.println("t2 结束");
        });

        t1.join();
        t2.join();

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


打破循环等待: 约定好加锁的顺序 

如果非要嵌套加锁可以约定加锁顺序: 

 public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1线程两个锁都获取到了");
                }
            }

            System.out.println("t1 结束");
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker1) {

                synchronized (locker2){
                    System.out.println("t2线程两个锁都获取到了");
                }
            }

            System.out.println("t2 结束");
        });

        t1.join();
        t2.join();

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



四.wait和notify 

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序. 

1.wait()方法:

属于Object 类的方法,使当前执行代码的线程进行等待,释放当前的锁用notify()唤醒

wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常 
因为wait()方法会,先进行解锁,如果事先没有锁,会抛出异常
2. notify()方法:也 属于Object 类的方法专门唤醒wait()方法,都是要在wait()方法之后执行才可以。 
3.和join一样也有带参数的版本。

3.解决的场景:

可以解决线程饿死:

线程饿死就是当,很多线程竞争一把锁的时候,有一个线程拿到锁,发现场景不适合释放了锁,然后这个线程这时属于就绪状态,很容易再次拿到这把锁,导致后面的线程,不能及时拿到锁导致后面的线程“饿死了”。 

解决:这个时候可以用wait()方法,当这个线程拿到锁的时候阻塞等待一下,时机到了就用notify()​​​​​​​方法唤醒即可。 


4.wait和notify使用场景:

notify和wait必须针对同一个对象锁才可以生效 

 public static void main(String[] args) {
        Object locker = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(()->{

            try {
                System.out.println("wait 之前");
                synchronized (locker) {
                    locker.wait();
                }
                System.out.println("wait 之后");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容, 通知唤醒 t1");
            scanner.next();

            synchronized (locker) {
                locker.notify();
            }
        });


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

 


如果锁对象不一样: 

 无法唤醒: 


wait必须在notify之前运行:

结合一个例题,要求三个线程打印ABC10次:

这里巧妙使用wait和notify唤醒线程

注意:如果在主线程不加Thread.sleep(1000),可能导致主线程的notify,被执行  

 public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Object locker3 = new Object();

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                synchronized (locker1){
                    try {
                        locker1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("A");
                synchronized (locker2) {
                    locker2.notify();
                }
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                synchronized (locker2){
                    try {
                        locker2.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("B");
                synchronized (locker3) {
                    locker3.notify();
                }
            }
        });

        Thread t3 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                synchronized (locker3){
                    try {
                        locker3.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("C");
                synchronized (locker1) {
                    locker1.notify();
                }
            }
        });

        t1.start();
        t2.start();
        t3.start();


        // 主线程中, 先通知一次 locker1, 让上述逻辑从 t1 开始执行
        // 需要确保上述三个线程都执行到 wait, 再进行 notify
        
        Thread.sleep(1000);
        synchronized (locker1) {
            locker1.notify();
        }

 


主线程不加Thread.sleep(1000)很大概率主线程先执行,就不满足(wait必须在notify之前运行)


5.notifyAll():唤醒所有线程

notifyAll也属Object 类的方法 

    public static void main(String[] args) {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            try {
                System.out.println("t1 wait 之前");
                synchronized (locker) {
                    locker.wait();
                }
                System.out.println("t1 wait 之后");
            } catch (InterruptedException e) {
                throw new RuntimeException();
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                System.out.println("t2 wait 之前");
                synchronized (locker) {
                    locker.wait();
                }
                System.out.println("t2 wait 之后");
            } catch (InterruptedException e) {
                throw new RuntimeException();
            }
        });

        Thread t3 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容, 唤醒所有线程");
            scanner.next();
            synchronized (locker) {
//                locker.notify();
                locker.notifyAll();
            }

        });

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

 


原文地址:https://blog.csdn.net/robin_suli/article/details/143434362

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!