多线程初阶(2)

说到多线程编程,一定少不了线程安全这个话题。我们前面了解了线程的原理以及线程与进程的关系。线程之间共享资源,这就代表了在多线程编程中一定会产生冲突,所以我们需要在敲代码时保证线程安全,避免这样的问题发生。

我们先看一个代码案例

public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();//保证线程都执行完Thread.sleep(1000);System.out.println(count);}
}

在我们看来,运行的结果应该是100000,但是事实并非如此。

运行结果:

1.随机调度

count++;

这段代码是看似是一个指令,但实际上是分步执行的。

分为三步的:

1.读取数据

2.修改数据

3.放回内存

在执行过程中,CPU资源是随时会被调度走的,也就是说,如果执行到了读取内存,有可能会被立刻调度走的。这就是所谓的随机调度。

为了解释上面的案例可以画一个时间线:

这也就是造成上面现象的原因之一。

那我们应该如何解决呢?

解决方案1:

利用join方法,在t1执行完后再执行t2,这样虽然能解决问题,但是失去了并发执行的意义,串行执行的效率也比较低。

public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t1.join();t2.start();t2.join();//保证线程都执行完Thread.sleep(1000);System.out.println(count);}
}

2.锁(synchronized)

对于解决上述的线程问题,引入了锁这一概念。

锁是什么,我们可以举个具体的例子来了解一下:

我们可以把锁看成家里的锁,当家里没人时,门是从外面锁上的,这时拥有钥匙的人就可以打开锁并进去执行任务,但为了在执行任务的时候是安全的,所以会从里面锁上,这时外面的人就算有钥匙也打不开门,当里面的人执行完任务的出来后还会把门带上,这时外面拥有钥匙的人就可以开门进去了。这其实就是锁是如何解决线程安全问题的类似原理。

public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();//        t1.start();
//        t1.join();
//        t2.start();
//        t2.join();//保证线程都执行完Thread.sleep(1000);System.out.println(count);}
}

运行结果:

我们先了解一下使用锁的格式:

synchronized(锁对象){}

这里的锁对象可以是任意引用类型的对象,但要保证在解决同一个非原子问题时是同一个对象。

synchronized的使用方法除了上述格式以外,还能用来修饰方法:

这里的锁对象是this。

class Counter{int count = 0;public synchronized void addCount(){count++;}
}public class Test11 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.addCount();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.addCount();}});t1.start();t2.start();Thread.sleep(100);System.out.println(counter.count);}
}

运行结果:

其实这样的解决方法也就是将局部的任务给串行化,只不过比直接将整个线程串行化来的含蓄,性能降低的少。

可重入

在Java中synchronized具有可重入的特点。

synchronized (locker) {synchronized (locker) {count++;}}

我们知道,锁是具有互斥性的,也就是在上锁后是需要解锁后才能让下一个拥有锁对象的任务执行,那上面这段代码就会形成一个死锁的现象,当进入第一个锁后,会遇到第二个锁,但是想要进入第二个synchronized是需要从第一个synchronized中出来的,但是要想从第一个synchronized中出来就需要进入第二个synchronized,所以这就形成了一个死循环,可以叫死锁。

Java的开发人员为了解决这一问题就赋予了synchronized可重入这一属性,也就是在上述情况下不会出现死锁的现象,Java会自动识别出来。

如果有很多层synchronized嵌套的话,当第一次进入的synchronized结束时,这把锁才会解开。

死锁 

上面讲可重入性的时候讲到了死锁这一概念,这里就详细讲讲死锁。

造成死锁有三种情况:

1.一个线程同一个锁加多次,这也是讲述可重入性时举的例子。

2.N个线程,M把锁。

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

运行结果:

进程并没有结束,使用jconsole查看线程状态,发现是BLOCKED,也就锁造成的无上限的阻塞等待。这是因为在线程t1拿到locker1和t2拿到locker2的情况下,t1要想拿到locker2就必须要让t2解锁,而t2要想解锁,就需要拿到locker1,但是locker1在t1手中,所以就形成了死循环,也就构成了死锁。

3.哲学家就餐问题

假设有七个哲学家围着一个桌子吃饭,每两个人中间放一根筷子,这样的话当一个人用筷子吃饭的时候,他两侧的人都没法用餐。

在一种极端情况下,每个哲学家都拿起他左手边的筷子,这样所有哲学家都在等待对方放下筷子,就形成了死锁。

public class Test13 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Object locker4 = new Object();Object locker5 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){}}});Thread t3 = new Thread(()->{synchronized (locker3){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){}}});Thread t4 = new Thread(()->{synchronized (locker4){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker3){}}});Thread t5 = new Thread(()->{synchronized (locker5){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker4){}}});t1.start();t2.start();t3.start();t4.start();t5.start();}
}

各线程状态: 

解决方案:

1.对筷子按顺序进行编号,先拿到左右小的编号的筷子,拿到后再拿左右大的编号的筷子。

刚开始1号先吃到饭,吃完后放下1号和7号筷子,2号拿到1号筷子,7号拿到7号筷子,7号可以就餐,以此类推,所有人都可以完成就餐。

public class Test13 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Object locker4 = new Object();Object locker5 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});Thread t2 = new Thread(()->{//保证t1先拿到locker1,如果t2先拿到locker1,还是会形成死锁try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){}}});Thread t3 = new Thread(()->{synchronized (locker2){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker3){}}});Thread t4 = new Thread(()->{synchronized (locker3){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker4){}}});Thread t5 = new Thread(()->{synchronized (locker4){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});t1.start();t2.start();t3.start();t4.start();t5.start();}
}

运行结果:

 4.死锁总结:

造成死锁的原因:

1.锁具有互斥性(锁的基本特性)

当一个锁被一个线程获取之后,当别的线程想要获取这个锁的时候,会线程阻塞。

2.锁不可抢占(锁的基本特性)

当这个锁已经被获取时,别的线程是不能强行抢占这个锁的, 必须等待获取。

3.请求和保持

当一个线程已经有至少一个锁的时候,尝试获取别的锁遇到阻塞,这时候该线程也不会放弃原来的锁。

4.循环等待

线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程5,线程5等待线程1,这样就产生了死循环。

解决方案:

1.把嵌套的锁改为并列的锁。(基于N个线程,M把锁的代码例子)

public class Test14 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){System.out.println("t1拿到locker1!");
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }}synchronized (locker2){System.out.println("t1拿到locker2!");}});Thread t2 = new Thread(()->{synchronized (locker2){System.out.println("t2拿到locker2!");
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }}synchronized (locker1){System.out.println("t2拿到locker1!");}});t1.start();t2.start();}
}

运行结果:

2.规定加锁顺序编号递增/递减(基于哲学家就餐问题)

代码案例在上述哲学家就餐问题中的Test13.

3.java标准库中的线程安全类:

StringBuffer,Hashtable,Vector,ConcurrentHashMap,String。。。。。。

前三个不推荐使用。

不安全类:
StringBuilder,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet。

以StringBuffer为例,查看标准库中的代码,发现其内部是由简单加synchronized实现的,当面对比较复杂的情况时,很有可能会出现bug~~

4.wait/notify

在有些情况下我们需要让某个线程处于阻塞状态,在完成某些任务后再进行唤醒。

注意在调用wait/notify时必须实在synchronized的代码块中,并且必须是相同的锁对象才行。

wait下的线程状态:WAITING

public class Test16 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(()->{synchronized (locker) {System.out.println("线程t1wait......");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程t1被唤醒!");}});Thread t2 = new Thread(()->{synchronized (locker) {try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2线程尝试唤醒t1......");locker.notify();}});t1.start();t2.start();}
}

运行结果:

在线程wait期间,该线程是主动放弃了CPU资源的,是解锁状态,暂时不会参与锁竞争。

这种情况下,当使用notify唤醒时只能唤醒其中一个,并且是随机的,这就有很大的不确定性在里面,所以java标准库中还提供了notifyAll方法,能够唤醒所有相同锁对象的wait。

对于notify是随即唤醒这一点还有可能会造成线程饿死,所谓线程饿死也就是某个线程长时间没有吃到CPU的资源。

public class Test17 {public static void main(String[] args) {Object l1 = new Object();Thread t1 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t1被唤醒!");} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t2被唤醒!");}    catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t3 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t3被唤醒!");} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t4 = new Thread(()->{System.out.println("输入任意内容唤醒所有线程:");Scanner sc = new Scanner(System.in);sc.next();synchronized (l1){l1.notifyAll();}});t1.start();t2.start();t3.start();t4.start();}
}

运行结果:

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Ubuntu】安裝向日葵远程控制

前言 在Ubuntu 24.04.2下安装向日葵远程控制出错&#xff0c;少了一些依赖&#xff0c;需要安装一些依赖。 1.安装gconf2-common wget http://mirrors.kernel.org/ubuntu/pool/universe/g/gconf/gconf2-common_3.2.6-6ubuntu1_all.deb sudo dpkg -i gconf2-common_3.2.6-6ub…

【Python开源】深度解析:一款高效音频封面批量删除工具的设计与实现

&#x1f3b5; 【Python开源】深度解析&#xff1a;一款高效音频封面批量删除工具的设计与实现 &#x1f308; 个人主页&#xff1a;创客白泽 - CSDN博客 &#x1f525; 系列专栏&#xff1a;&#x1f40d;《Python开源项目实战》 &#x1f4a1; 热爱不止于代码&#xff0c;热情…

JAVA房屋租售管理系统房屋出租出售平台房屋销售房屋租赁房屋交易信息管理源码

一、源码描述 这是一套房屋租售管理源码&#xff0c;基于SpringBootVue框架&#xff0c;后端采用JAVA开发&#xff0c;源码功能完善&#xff0c;涵盖了房屋租赁、房屋销售、房屋交易等业务。 二、源码截图

一篇文章讲清楚mysql的聚簇索引、非聚簇索引、辅助索引

聚簇索引与非聚簇索引最大的区别就是&#xff1a; 聚簇索引的索引和数据是存放在一起的&#xff0c;都是在叶子结点&#xff1b; 非聚簇索引的索引和数据是分开存储的&#xff0c;叶子节点存放的是索引和指向数据文件的地址&#xff0c;通过叶子节点找到索引&#xff0c;再通…

使用ESPHome烧录固件到ESP32-C3并接入HomeAssistant

文章目录 一、安装ESPHome二、配置ESP32-C3控制灯1.主配置文件esp32c3-luat.yaml2.基础通用配置base.yaml3.密码文件secret.yaml4.围栏灯four_light.yaml5.彩灯rgb_light.yaml6.左右柱灯left_right_light.yaml 三、安装固件四、HomeAssistant配置ESPHome1.直接访问2.配置ESPHom…

什么是变量提升?

变量提升&#xff08;Hoisting&#xff09; 是 JavaScript 引擎在代码执行前的一个特殊行为&#xff0c;它会将变量声明和函数声明自动移动到当前作用域的顶部。但需要注意的是&#xff0c;只有声明会被提升&#xff0c;赋值操作不会提升。 ​​核心概念​​ 变量声明提升&…

【万字长文】深入浅出 LlamaIndex 和 LangChain:从RAG到智能体,轻松驾驭LLM应用开发

Langchain系列文章目录 01-玩转LangChain&#xff1a;从模型调用到Prompt模板与输出解析的完整指南 02-玩转 LangChain Memory 模块&#xff1a;四种记忆类型详解及应用场景全覆盖 03-全面掌握 LangChain&#xff1a;从核心链条构建到动态任务分配的实战指南 04-玩转 LangChai…

2025 后端自学UNIAPP【项目实战:旅游项目】3、API接口请求封装,封装后的简单测试以及实际使用

一、创建请求封装目录 选中自己的项目&#xff0c;右键鼠标---->新建---->目录---->名字自定义【我的是api】 二、创建两个js封装文件 选中封装的目录&#xff0c;右键鼠标---->新建---->js文件---->名字自定义【我的两个js文件分别是my_http和my_api】 三…

autojs和冰狐智能辅助该怎么选择?

最近打算做自动化脚本&#xff0c;在autojs和冰狐智能辅助中做选择&#xff0c;不知道该怎么选。没办法只能花费大量时间仔细研究了autojs和冰狐智能辅助&#xff0c;综合考虑功能需求、开发复杂度、编程经验及项目规模等因素。以下是两者的核心对比及选择建议&#xff0c;仅供…

python24-匿名函数

课程&#xff1a;B站大学 记录python学习&#xff0c;直到学会基本的爬虫&#xff0c;使用python搭建接口自动化测试就算学会了&#xff0c;在进阶webui自动化&#xff0c;app自动化 匿名函数 匿名函数实践是检验真理的唯一标准 匿名函数 匿名函数是指没有名字的函数&#xff…

Android 查看 Logcat (可纯手机方式 无需电脑)

安装 Logcat Reader Github Google Play 如果有电脑 使用其ADB方式可执行如下命令 后续无需安装Termux # 使用 ADB 授予 android.permission.READ_LOGS 权限给 Logcat Reader adb shell "pm grant com.dp.logcatapp android.permission.READ_LOGS && am force-…

驱动开发硬核特训 · Day 30(上篇):深入理解 I2C 总线驱动模型(以 at24 EEPROM 为例)

作者&#xff1a;嵌入式Jerry 视频教程请关注 B 站&#xff1a;“嵌入式Jerry” 一、写在前面 在上一阶段我们已经深入理解了字符设备驱动与设备模型之间的结合方式、sysfs 的创建方式以及平台驱动模型的实际运用。今天我们迈入总线驱动模型的世界&#xff0c;聚焦于 I2C 总线…

超详细讲解注意力机制、自注意力机制、多头注意力机制、通道注意力机制、空间注意力机制

在如今的机器学习和深度学习领域&#xff0c;注意力机制绝对是一个热度居高不下的话题。事实上&#xff0c;注意力机制并不是一个全新的概念&#xff0c;早在多年前就已经被提出并应用。比如在图像分类任务中&#xff0c;SENet 和 ECA-Net 等模型中都运用了注意力机制&#xff…

Wireshark基本使用

本文会对Wireshark做简单介绍&#xff0c;带大家熟悉一下Wireshark的界面&#xff0c;以及如何使用过滤器。 接着会带大家查看TCP五层模型下&#xff0c;带大家回顾各层首部的格式。 最后会演示 Wireshark 如何抓取三次握手和四次挥手包的过程。 目录 一.Wireshark简介 二…

加速项目落地(Trae编辑器)

目录 vscode安装python支持 vscode常用插件 Trae编辑器 两个界面合成 补充&#xff08;QT开发的繁琐&#xff09; AI编程哪家强&#xff1f;Cursor、Trae深度对比&#xff0c;超详细&#xff01; - 知乎 Trae兼容vscode的插件&#xff0c;我们可以先在vscode里面装好再一…

stable diffusion的attention-map:提取和可视化跨注意力图

项目&#xff1a; wooyeolbaek/attention-map-diffusers: &#x1f680; Cross attention map tools for huggingface/diffusers 参考&#xff1a;【可视化必备技能&#xff08;1&#xff09;】SD / Flux 文生图模型的 Attention Map 可视化-CSDN博客

多环串级PID

文章目录 为什么要多环程序主函数内环外环 双环PID调参内环Kp调法Ki调法 外环Kp 以一定速度到达指定位置封装 为什么要多环 单环只能单一控制速度或者位置&#xff0c;如果想要同时控制多个量如速度&#xff0c;位置&#xff0c;角度&#xff0c;就需要多个PID 速度环一般PI…

基于Kubernetes的Apache Pulsar云原生架构解析与集群部署指南(上)

#作者&#xff1a;闫乾苓 文章目录 概念和架构概述主要特点消息传递核心概念Pulsar 的消息模型Pulsar 的消息存储与分发Pulsar 的高级特性架构BrokerBookKeeperZooKeeper 概念和架构 概述 Pulsar 是一个多租户、高性能的服务器到服务器消息传递解决方案。Pulsar 最初由雅虎开…

电子电气架构 --- 如何有助于提安全性并减少事故

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…

rest_framework学习之认证 权限

权限 DRF提供如下几种常见权限&#xff1a; IsAuthenticated, 认证通过 IsAdminUser, 管理员权限 IsAuthenticatedOrReadOnly, 登录用户增删改 非登录用户只能查询 AllowAny&#xff0c;无需认证&#xff08;默认&#xff09; 在rest_framework的APIView基础类中&#xf…