JavaEE多线程(2)

在这里插入图片描述

文章目录

  • 1..多线程的安全
    • 1.1出现多线程不安全的原因
    • 1.2解决多线程不安全的⽅法
    • 1.3三种典型死锁场景
    • 1.4如何避免死锁问题
    • 2.线程等待通知机制
    • 2.1等待通知的作用
    • 2.2等待通知的方法——wait
    • 2.3唤醒wait的方法——notify

1…多线程的安全

1.1出现多线程不安全的原因

  1. 线程在系统中是随机调度,抢占式执⾏。
  2. 多个线程同时修改同⼀个变量
  3. 线程对变量的修改操作不是“原⼦”的
  4. 内存可⻅性问题
  5. 指令重排序、
    “原⼦”是什么?
    原⼦是指不可再拆分的最⼩单位,放到代码操作中就是⼀段代码对应⼀个cpu指令就是原⼦的,如果对应到多个cpu指令就不是原⼦的。

1.2解决多线程不安全的⽅法

从原因⼊⼿:

  1. 原因1由于是系统本⾝的原因⼈为⽆法⼲预。
  2. 原因2是⼀个解决多线程不安全的⽅法,但是只是在特定的场景下才可以实现。
  3. 原因3是解决多线程不安全最合适的⽅法,既然修改的这个操作不是⼀个原⼦,那我们只需要将这
    个不是“原⼦”的操作打包成⼀个原⼦的操作即可。
    引申出⼀个新的词“锁”
    我们可以通过锁来将之前不是原⼦操作打包成⼀个原⼦操作
    关于这个锁本⾝就是系统内核中的api,只不过是jvm将这个api封装了,为了让java可以更好的使⽤这
    个锁。
    关于锁主要操作的两个⽅⾯:
    1.加锁
    ⽐如对t1线程进⾏加锁,t2线程也要加锁,那么t2线程就会阻塞等待(互斥锁/竞争锁/锁冲突)
    2.解锁
    t1线程解锁之后,t2线程才可以进⾏加锁
    注意:此处的锁主要针对于锁对象,对于t1和t2线程是指的同⼀个锁对象,不是同⼀个锁对象那就没
    意义了。
    对锁的总结:
    1.两个操作⸺加锁,解锁
    2.锁的特性⸺互斥
    3.只有多个线程竞争同⼀把锁才会互斥,如果多个线程竞争不同的锁则不会产⽣互斥
    java中⽤⼀个关键字来描述锁⸺synchronized
    1.synchronized 怎么读?怎么拼写?
    synchronized
    2.synchronized()括号中写的是锁对象⸺锁对象可以是任意类实列出的对象
    注意:
    锁对象的⽤途只有⼀个,两个线程是否针对⼀个对象加锁
    如果是就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待
    如果不是就不会出现锁竞争/锁冲突/互斥,就不会引起阻塞等待
    和对象具体是什么类型,和它内部有什么属性,有什么⽅法,接下来是否要操作这个对象,统统都没
    有关系
    3.synchronized下⾯跟着{}
    当进⼊到这个代码块就是对上述(锁对象)进⾏了加锁操作
    当出了代码块就是对上述(锁对象)进⾏了解锁操作
    以下代码就是两个线程针对同⼀个对象locker加锁,当t1线程加锁之后,t2想加锁只能阻塞等待,只有
    等到t1线程解锁之后,t2线程才有可能加锁成功
public static int count = 0;
//创建一个对象作为一个锁对象
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(()-> {
        for (int i = 1; i <= 5000; i++) {
            synchronized(locker) {
                count++;
            }
        }
    });
    Thread t2 = new Thread(()-> {
        for (int i = 1; i <=5000 ; i++) {
            synchronized(locker) {
                count++;
            }
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("count = "+ count);
}

以下代码就是两个线程对不同锁对象进行加锁,就不会产生锁冲突/锁竞争/互斥。

public static int count = 0;

public static void main(String[] args) throws InterruptedException {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        for (int i = 1; i <=5000 ; i++) {
            synchronized(locker1) {
                count++;
            }
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 1; i <=5000 ; i++) {
            synchronized(locker2) {
                count++;
            }
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("count = " + count);
}

当t1线程解锁之后,不一定是t2线程拿到锁,有可能是其他线程拿到锁。
join与锁的区别:
join是一个线程完了,再让第二个线程执行。
锁只是针对加锁的那一块代码,就像上述代码中加锁的count++就会变成串行执行,但剩余的代码还是并发执行。
注意:加锁不是针对线程,而是针对共享资源的访问操作,比如现在我对t1线程中的操作1进行了加锁,但是系统内核将t1线程调度走了,可以让其他线程调度到t1线程的位置继续执行操作1,此时t2线程还是无法加到锁.
另外加锁的方式:
1.写一个方法将加锁的关键字放在方法中:

public static int count = 0;
synchronized public void add() {
    count++;
}
public static void main(String[] args) throws InterruptedException {
    Deom14 deom14 = new Deom14();
    Thread t1 = new Thread(()-> {
        for (int i = 1; i <= 5000; i++) {
            deom14.add();
        }
    });
    Thread t2 = new Thread(()-> {
        for (int i = 1; i <=5000 ; i++) {
            deom14.add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("count = "+ count);
}

对于这种方式加锁还有一种写法:

public static int count = 0;

public void add() {
    synchronized (this){
        count++;
    }
}
public static void main(String[] args) throws InterruptedException {
    Deom14 deom14 = new Deom14();
    Thread t1 = new Thread(()-> {
        for (int i = 1; i <= 5000; i++) {
            deom14.add();
        }
    });
    Thread t2 = new Thread(()-> {
        for (int i = 1; i <=5000 ; i++) {
            deom14.add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("count = "+ count);
}

利用this,谁调用了这个方法就用这个对象作为锁对象,
2.synchronized对static方法进行加锁,相当于对类的类对象进行加锁

public static int count = 0;
synchronized static void func() {
    count++;
}

public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(()-> {
        for (int i = 1; i <= 5000; i++) {
            Deom15.func();
        }
    });
    Thread t2 = new Thread(()-> {
        for (int i = 1; i <=5000 ; i++) {
            Deom15.func();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("count = "+ count);
}

对于这种写法有个致命的缺点,一旦有多个线程调用func,则这些线程都会触发锁竞争。
4. 原因4引起的线程不安全——编译器优化产生的线程不安全

public static int count;
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        while(count == 0) {
            //未执行操作
        }
        System.out.println("退出t1线程");
    });

    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个数:");
        count = scanner.nextInt();
    });
    t1.start();
    t2.start();
}

在t1线程中while(count == 0)这个操作在指令的角度来分析就两个指令分别为load和cmp指令
load指令将内存中数据读入cpu中寄存器
cmp在cpu寄存器中进行比较
1.内存读取数据的速度远远小于寄存器读取的数据的速度就会造成load指令执行的速度远远慢于cmp指令执行。
2.在t2线程未修改count之前load指令执行的结果是一样的。
由上述两个原因,java编译器为了提高效率就会将load指令这个操作优化,所以当t2线程修改了count的值t1线程也不会感知到。
如果在t1线程的循环体中加一些I/O操作或者阻塞操作,这样java编译器就不会去优化load指令。

public static int count;
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        while(count == 0) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("退出t1线程");
    });

    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个数:");
        count = scanner.nextInt();
    });
    t1.start();
    t2.start();
}

如何解决由编译器优化引起的线程不安全?
1.上述在循环体中加I/O操作或者阻塞操作可以解决
2.用volatile关键字来修饰需要修改的变量——这个关键字只能解决编译器优化带来的内存可见性问题,不能解决原因三带来的问题。
volatile关键字
当为count加上volatile关键字就会告知编译器这个变量不能随便被优化`在这里插入代码片

public volatile static int count;
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        while(count == 0) {
        }
        System.out.println("退出t1线程");
    });

    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个数:");
        count = scanner.nextInt();
    });
    t1.start();
    t2.start();
}
  1. 原因5引起的线程不安全——编译器优化策略导致
    指令重排序旨在编译器在优化的时候将你的代码重新调整执行顺序来提高效率,优化前的逻辑与优化后的逻辑是等价的,在单线程中指令重排序这个优化策略是不会造成线程不安全,但是在多线程中就会导致线程不安全。
    面对指令重排序我们采取用volatile关键字
class SinlgetonLazy1 {
    private static volatile SinlgetonLazy1 instance = null;
    public static SinlgetonLazy1 getInstance() {
        Object locker = new Object();
        if(instance == null) {
            synchronized(locker) {
                if (instance == null ) {
                    instance = new SinlgetonLazy1();
                }
            }
        }
        return instance;
    }
    private SinlgetonLazy1() {

    }
}
public class Deom24 {
    public static void main(String[] args) throws InterruptedException {
        SinlgetonLazy1 s = SinlgetonLazy1.getInstance();
        Thread t1 = new Thread(() -> {
            SinlgetonLazy1 s1 = SinlgetonLazy1.getInstance();
        });
        Thread t2 = new Thread(() -> {
            SinlgetonLazy1 s2 = SinlgetonLazy1.getInstance();
        });
        t1.start();
        t2.start();
    }
}

1.3三种典型死锁场景

场景一:锁是不可重入锁,并且一个线程针对一个锁对象,连续被加锁两次。
采取可重入锁(synchronized)就可以对这个问题迎刃而解了
场景二:两个线程两把锁
现有t1线程t2线程和locker1锁locker2锁, locker2锁要对t1线程中内容加锁,但同时locker2锁对t2线程还未解锁,所以t1线程需要阻塞等待,而现在locker1锁也要对t2线程中的内容加锁,但是locker1对t1线程还未解锁,所以t2线程需要阻塞等待,这样就导致你等我,我等你的死锁现象`

public static void main(String[] args) {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker2) {
                System.out.println("t1线程获取到了两把锁");
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker2) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker1) {
                System.out.println("t2线程获取到了两把锁");
            }
        }
    });
    t1.start();
    t2.start();
}

上述代码就是场景二死锁的实现代码
通过jconsole窗口观察:
在这里插入图片描述
此时就是t1线程想要获取到locker2锁,但locker2锁被t2线程给加锁了并且没有解锁,所以此时的t1线程想要获取locker2锁只能阻塞等待。
在这里插入图片描述
此时就是t2线程想要获取到locker1锁,但locker1锁被t1线程给加锁了并且没有解锁,所以此时的t2线程想要获取locker1锁只能阻塞等待。
场景三:N个线程M把锁——哲学家就餐问题
约定每一个哲学家必须先获取编号小的筷子,后获取编号大的筷子,就可以解决哲学家就餐问题。

1.4如何避免死锁问题

出现死锁的四大必要条件,少一个都不会出现死锁。

  1. 锁具有互斥特性(基本特点,一个线程拿到锁之后,其他线程就得阻塞等待)——基本特点
  2. 锁不可抢占(不可被剥夺)——基本特点
  3. 请求和保持(一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁)——代码结构
  4. 循环等待(多个线程获取多个锁的过程中,出现了循环等待,A线程等待B线程,B线程又等待A线程)——代码结构
    要避免死锁,由于第一和第二点是锁的基本特性所以我们无法避免,我们只能从第三点和第四点出发避免死锁。
    针对第三点:我们尽量不要出现嵌套锁。
    针对第四点:我们可以约定加锁的顺序,让所有的线程按照加锁的顺序来获取锁。
    针对上述的代码出现了死锁,我们就可以约定加锁的先后顺序来避免死锁,我们约定locker1先加锁,locker2后加锁。
public static void main(String[] args) {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker2) {
                System.out.println("t1线程获取到了两把锁");
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker2) {
                System.out.println("t2线程获取到了两把锁");
            }
        }
    });
    t1.start();
    t2.start();
}

2.线程等待通知机制

2.1等待通知的作用

通过条件,判断当前逻辑是否能够执行,如果不满足条件不能执行,那么就主动进行阻塞(wait)让其他线程来调度cpu的资源,等到条件满足的时候,再让其它线程(阻塞的线程)来唤醒。

2.2等待通知的方法——wait

  1. wait方法是Object类提供,所以任何对象都能调用这个方法
  2. wait方法和sleep一样会被interrupt打断并且自动清空标志位
  3. wait方法不仅仅可一个阻塞等待还可以解锁
  4. wait方法要放在synchronized内部使用
public static void main(String[] args) {
    Object locker = new Object();
    Thread t1 = new Thread(() -> {
        
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
    });

在这里插入图片描述

2.3唤醒wait的方法——notify

  1. notify方法要放在synchronized内部使用
  2. notify方法是Object类提供,所以任何对象都能调用这个方法wait方法是Object类提供,所以任何对象都能调用这个方法
  3. notify是随机唤醒被阻塞的线程
  4. notifyAll()方法可以唤醒所有被阻塞的线程,但这种方法不常用。`
public static void main(String[] args) {
    Object locker = new Object();
    Thread t1 = new Thread(() -> {
        System.out.println("t1线程等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        System.out.println("t1线程等待结束");
    });
   Thread t2 = new Thread(() -> {
       System.out.println("t2线程唤醒t1线程之前");
       synchronized (locker) {
           try {
               Thread.sleep(3000);
               locker.notify();
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
       }
       System.out.println("t2线程唤醒结束");
   });
   t1.start();
   t2.start();
}

此时的t1线程的状态转化为:WAITTING——RUNNABLE——BLOCKED

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/726769.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

思维导图之计算机网络整体框架

高清自行访问&#xff1a;计算机网络整体框架 (yuque.com)

嵌入式开发者转战AI大模型,是机遇还是挑战?

前言 在当今日新月异的科技浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;大模型无疑是技术前沿的明星领域&#xff0c;它们以其卓越的性能和广泛的应用前景&#xff0c;吸引了全球范围内的关注。对于嵌入式开发者而言&#xff0c;从熟悉的硬件嵌入式领域转战AI大模型…

如何恢复删除的文件?五种方法,高效恢复全解析整理

随着科技的日新月异&#xff0c;电子设备已无处不在地融入我们的日常生活和工作之中。无论是手机还是电脑&#xff0c;它们已成为我们存储个人数据、工作文档和学习资料的重要载体。然而&#xff0c;在享受数字时代便捷的同时&#xff0c;我们也需警惕一个潜在风险——文件丢失…

ccie在香港值钱吗?ccie认证很难考吗?

思科ccie认证可以算得上是网络高级工程师的一个标配证书&#xff0c;特别是在香港工作的朋友更是需要尽早拿下这个认证&#xff0c;它能让你的求职之路更为顺利。而已经入职的工程师为了不被时代所淘汰&#xff0c;也需要该证书保驾护航。 你知道ccie在香港值钱吗?ccie认证是不…

01 基础入门 编写你的第一个 Go 语言程序

从这节课开始&#xff0c;我会带你走进 Go 语言的世界。我会用通俗易懂的语言&#xff0c;介绍 Go 语言的各个知识点&#xff0c;让你可以从零开始逐步学习&#xff0c;再深入它的世界。不管你以前是否接触过 Go 语言&#xff0c;都可以从这个专栏中受益。 现在&#xff0c;让…

odoo 会计学习记录

产品类别 原材料 库存计价 成本方法&#xff1a;平均成本 库存计价&#xff1a;自动 会计库存属性 库存计价科目&#xff1a;1403 原材料 库存日记账&#xff1a;库存计价 进货科目&#xff1a;220201 暂估应付 出货科目&#xff1a;1406 发出商品 科目属性 收入科目: …

如何建立私域流量?私域流量怎么运营,一文读懂

当全网都在讨论私域流量&#xff0c;你是不是也有很多问号呢&#xff1f; 互联网高速发达&#xff0c;消费形式日新月异&#xff0c;跟不上时代就会被时代淘汰&#xff0c;接下来&#xff0c;我们就从3个层面深度讨论下私域流量究竟是什么&#xff1f;为什么要玩转私域流量&am…

解释React中的“端口(Portals)”是什么,以及如何使用它来渲染子节点到DOM树以外的部分。

React中的“端口&#xff08;Portals&#xff09;”是一种将子节点渲染到DOM****树以外的部分的技术。在React应用中&#xff0c;通常情况下组件的渲染是遵循DOM的层次结构&#xff0c;即子组件会渲染在父组件的DOM节点内部。然而&#xff0c;有些情况下&#xff0c;开发者可能…

C++语法06 格式化输出及保留小数点后指定位数

格式化输出 格式化输出所用的函数为 printf&#xff0c;它可以输出任意位数的小数。 使用格式&#xff1a;printf(“%.nf”,a)。这句话的作用是将变量a保留n位小数输出。 注意事项&#xff1a; 1、这里的n&#xff0c;需要具体化为一个数字&#xff0c;保留几位小数&#x…

【网络安全】网络安全基础精讲 - 网络安全入门第一篇

目录 一、网络安全基础 1.1网络安全定义 1.2网络系统安全 1.3网络信息安全 1.4网络安全的威胁 1.5网络安全的特征 二、入侵方式 2.1黑客 2.1.1黑客入侵方式 2.1.2系统的威胁 2.2 IP欺骗 2.2.1 TCP等IP欺骗 2.2.2 IP欺骗可行的原因 2.3 Sniffer探测 2.4端口扫描技术…

【前端开发工具】VS Code安装和使用

文章目录 一、前言二、下载三、安装四、配置五、使用5.1 导入项目5.2 本地运行项目5.3 修改界面文案&#xff0c;验证效果5.4 添加日志打印5.5 代码调试5.6 代码提交到Git仓库 六、总结 一、前言 本文介绍一下在前端vue项目中&#xff0c;VS Code的安装和配置。 什么是VS Code…

智慧乡村和美人家信息化系统

一、简介 智慧乡村和美人家信息化系统是一个综合管理平台&#xff0c;集成了首页概览、一张图可视化、数据填报、智能评估、便捷申报、公开公示、任务管理、活动发布和灵活配置等功能。该系统不仅提升了乡村管理效率&#xff0c;也优化了家庭生活的便捷性。通过一张图&#xf…

SOLIDWORKS安装运行环境建议 慧德敏学

SOLIDWORKS是一款要求很高的软件。无可否认。您的电脑功能越强大&#xff0c;运行得越好&#xff0c;但是我们也要考虑购买成本&#xff0c;因此&#xff0c;选择正确的配置很重要。在选择用于SOLIDWORKS的电脑配置时&#xff0c;需要综合考虑多个方面以确保软件能够流畅、以更…

hbuilderx如何打包ios app,如何生成证书

hbuilderx可以打包ios app, 但是打包的时候&#xff0c;却需要两个证书文件&#xff0c;我们又如何生成这两个证书文件呢&#xff1f; 点击hbuilderx的官网链接&#xff0c;教程是需要使用mac电脑苹果开发者账号去创建这两个文件&#xff0c;可是问题来了&#xff0c;我们没有…

osgearth提示“simple.earth: file not handled”

在用vcpkg编译完osg和osgearth后&#xff0c;为了验证osgearth编译是否正确&#xff0c;进行测试&#xff0c;模型加载代码如下&#xff1a; root->addChild(osgDB::readNodeFile("simple.earth")); 此时以为是simple.earth路径的问题&#xff0c;遂改为以下代码…

ruoyi登录功能源码分析

Ruoyi登录功能源码分析 上一篇文章我们分析了一下若依登录验证码生成的代码&#xff0c;今天我们来分析一下登录功能的代码 1、发送登录请求 前端通过http://localhost/dev-api/login向后端发送登录请求并携带用户的登录表单 在后端中的com.ruoyi.web.controller.system包下…

扩散模型荣获CVPR2024最佳论文奖,最新成果让评估和改进生成模型更加效率!

CVPR 2024最佳论文奖新鲜出炉 其中一篇是Rich Human Feedback for Text-to-Image Generation&#xff0c;受大模型中的RLHF技术启发&#xff0c;团队用人类反馈来改进Stable Diffusion等文生图模型。 作者提出了收集丰富的细粒度人类反馈信息&#xff0c;用于更好地评估和改进…

深度优先遍历-在二叉树中找到两个节点的最近公共祖先

一、问题描述 二、解题思路 使用深度递归的方式&#xff0c;如果当前结点val为o1时&#xff0c;返回1&#xff0c;如果当前结点是val为o2时&#xff0c;返回2&#xff1b; 1.当前结点的左右子树结点返回值分别为1和2时&#xff0c;说明该结点是最近的公共祖先结点 2.当前结点…

联邦学习——学习笔记1:FedAvg算法

文章目录 本笔记参考自b站up主&#xff1a;丸一口 原视频链接 如上图&#xff0c;现有6个医院&#xff1a;眼科、儿科、妇科、骨科、综合医院1、综合医院2。中间节点为政府。 现政府要求用各个医院的数据训练某个模型&#xff0c;希望对某些疾病进行一些预测&#xff0c;数据…

【Linux】—在Linux中搭建Python环境

文章目录 前言一、检查Linux系统是否自带Python版本。二、安装依赖包(重要)三、下载Python-3.9.5安装包四、下载完成后&#xff0c;通过xftp6上传到Linux服务器上五、解压Python安装包六、编译安装Python七、配置Python环境变量八、运行Python&#xff0c;查看是否可用九、pyth…