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

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

时间:2024-11-04 19:19:58浏览次数:5  
标签:Java synchronized Thread Object t1 死锁 线程 new 多线程

目录: 

一.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();
    }

 

标签:Java,synchronized,Thread,Object,t1,死锁,线程,new,多线程
From: https://blog.csdn.net/robin_suli/article/details/143434362

相关文章

  • java计算机毕业设计程序课程在线练习系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着信息技术的飞速发展,程序课程在教育体系中的重要性日益凸显。如今,计算机技术已经渗透到各个领域,掌握程序知识成为了许多行业对人才的基本要求......
  • java计算机毕业设计大学生学习交友平台(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着互联网技术的飞速发展,社交网络已经深入到人们生活的各个方面。大学生作为社会中活跃且具有强烈社交需求的群体,传统的交友方式已难以满足他们......
  • 基于java+SpringBoot+Vue的大学生就业招聘系统设计与实现
    项目运行环境配置:Jdk1.8+Tomcat7.0+Mysql+HBuilderX(Webstorm也行)+Eclispe(IntelliJIDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot+mybatis+Maven+mysql5.7或8.0等等组成,B/S模式+Maven管理等等。环境需要1.运行环境:最好是javajdk1.8,我们在这个......
  • 基于java+SpringBoot+Vue的校园资料分享平台设计与实现
    项目运行环境配置:Jdk1.8+Tomcat7.0+Mysql+HBuilderX(Webstorm也行)+Eclispe(IntelliJIDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot+mybatis+Maven+mysql5.7或8.0等等组成,B/S模式+Maven管理等等。环境需要1.运行环境:最好是javajdk1.8,我们在这个......
  • java计算机毕业设计大学生德育工程思想道德评价系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着社会的发展,高校对于大学生的思想道德教育日益重视。德育作为高校立德树人根本任务的重要支撑,其评价体系在整个教育环节中起到关键的导向作用......
  • java计算机毕业设计宠物养护系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着人们生活水平的提高,宠物在家庭中的地位日益重要,已经成为许多家庭不可或缺的成员。然而,目前宠物养护领域存在着一些问题。一方面,宠物主人在养......
  • java中的Math.round(-1.5)等于多少
       -1等于-1,因为在数轴上取值时,中间值(0.5)向右取整,所以正0.5是往上取整,负0.5是直接舍弃。(观点不认同)Math提供了三个与取整有关的方法:ceil、floor、round(1)ceil:向上取整;(2)floor:向下取整;(3)round:四舍五入;1、ceil:向上取整向上取整:无论小数点后面的数字是多少,都向上取整......
  • 用栈实现队列 [Java]
    用栈实现队列题目链接:LeetCode232描述请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):实现MyQueue类:voidpush(intx)将元素x推到队列的末尾intpop()从队列的开头移除并返回元素intpeek()返回队列开头的元素......
  • 大数加法 [Java]
    大数加法题目链接:牛客BM86描述以字符串的形式读入两个数字,编写一个函数计算它们的和,以字符串形式返回。示例输入:“1”,“99”返回值:“100”说明:1+99=100思路方法一:申请两个栈空间和一个标记位,然后将两个栈中内容依次相加。 与链表相加类似:7、链表相加方法二......