当前位置: 首页 > news >正文

JavaEE初阶——线程安全(多线程)

线程安全

  • 线程安全概念
  • synchronized锁
    • 锁的使用方式
    • 锁的互斥和可重入
    • 死锁问题
  • volatile 关键字
  • wait和notify

线程安全概念

可以简单理解为,如果多线程环境下,其代码的运行结果是符合我们预期结果,其可以认为其是线程安全的
例如:在多线程下计算50000 + 50000

public class demo13 {private 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();t1.join();t2.join();System.out.println(count);}
}

像上面这个代码,我们使用线程来对同一个变量修改,理想的结果是100000,但是真实的运行结果如下,并且结果不是唯一的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面这个代码之所以结果不是100000,是因为其1.操作系统调度随机,2.在多线程下运行,3.count++操作不是原子性的
为了让其结果符合操作,一半都是通过3.让其操作变成原子性的即可

原本的count++可以大概分为下面三个指令
1.从内存把数据读到CPU (load)
2. 进⾏数据更新 (add)
3. 把数据写回到CPU (save)
因为其count++对应多个指令不是原子性的,所以在这个多线程下运行结果不符合我们要求
因为一个线程的修改还没有load可能被另一个线程覆盖,这样就会导致一些count无效
在这里插入图片描述
那这个结果可能<50000吗?
其实是可以的,可能一个线程执行中间,另一个线程执行了多次"无效"相加
在这里插入图片描述

线程不安全的原因

1.操作系统对于线程调度是随机的
2.多线程下修改共享资源(同一变量)
3.操作不是原子性,操作系统是以指令来执行的,一个代码语句可能对应多个指令
4.内存可见性
5.指令重排序

synchronized锁

因此可以使用synchronized加锁,让其操作变成原子性的

public class demo13 {private static int count = 0;private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {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.join();t2.join();System.out.println(count);}
}

锁的使用方式

修改时代码块,指定锁对象

任意对象

public class demo13 {private static int count = 0;private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {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.join();t2.join();System.out.println(count);}
}

当前对象
使用this

class Counter {public int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public void add() {synchronized (this) {count++;}}
}
public class demo14 {public static void main(String[] args) throws InterruptedException {Counter counter  = new Counter();Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

通过反射,使用类对象

class Counter {public int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public void add() {synchronized (demo13.class) {count++;}}
}

对方法加锁

class Counter {public int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public synchronized void add() {count++;}
}

静态方法加锁

class Counter {public static int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public synchronized static void add() {count++;}
}

上面这些都是可以解决上面线程安全问题的,但是这里都是对同一个对象加锁,两个线程对同一个对象加锁,才会产生阻塞等待,如果两个线程分别对应两把不同的锁,此时不会发生竞争,线程安全问题也不会解决
两个线程两把锁,其线程安全问题仍然没有解决
在这里插入图片描述
一个加锁,另一个不加,仍然存在线程安全问题
在这里插入图片描述

总结
1.进入{是加锁,对应的}表示解锁
2.加锁只是防止其他线程插队,并不影响线程调度
3.锁对象,两个线程针对统一对象加锁,才会有锁竞争,反之不会

锁的互斥和可重入

互斥
synchronized会起到互斥的效果,某个线程执行到了这个synchronized中时,其他线程如果也执行到同一对象的锁,就会阻塞等待

进入synchronized修改的代码块,相当于加锁
退出synchronized修改的代码块,相当于解锁

在这里插入图片描述
在这里插入图片描述

可重入
java中锁是可重入的,对同一对象加两次锁不会出现锁死情况

class Counter {public static int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public  void add() {synchronized (this){count++;}}
}
public class demo14 {public static void main(String[] args) throws InterruptedException {Counter counter  = new Counter();Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {//连续加同一把锁synchronized (counter){synchronized (counter){counter.add();}}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (counter){counter.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

在这里插入图片描述
虽然这里对同一线程连续加了同一把锁,但是java中会自行判断,因此这里只有第一次是加锁成功,后面其会判断是否和第一次加锁是同一线程、同一把锁,如果是同一线程,第二次加锁,相当于”直接跳过
在这里插入图片描述

死锁问题

1.一个线程一把锁,但是连续加锁多次,java中可重入锁,解决了这个问题
2.两个线程两把锁(可能会出现你等我,我等你的问题,像门钥匙锁车里了,车钥匙锁家里了)
3.n个线程,m把锁

两个线程两把锁

//两个线程两把锁
public class demo18 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized (locker1){System.out.println("t1获取locker1");try {Thread.sleep(10);} 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(10);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2获取locker1");}}});t1.start();t2.start();}
}

这是两个线程两把锁,此时当t1获取到locker1,t2获取到locker2后,出现了问题,但是此时两个锁都没有释放,但是在t1线程中locker1锁中又有获取locker2锁,在t2线程中locker2锁中又有获取locker1锁,但此时要获取锁,必须要等到锁释放,但是这里释放锁,又要需要你获取锁,就这样无线循环,导致线程锁住了
在这里插入图片描述
在jdk中bin目录下jconsole.exe文件中可以发现,这两个线程的状态是BLOCKED,此时都是因为锁导致阻塞
在这里插入图片描述

在这里插入图片描述
上面这个问题,其实我们可以让其按照一定的顺序进行加锁,规定都是先获取locker1,在获取locker2

public class demo18 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized (locker1){System.out.println("t1获取locker1");try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1获取locker2");}}});Thread t2 = new Thread(() ->{synchronized (locker1){System.out.println("t2获取locker2");try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2获取locker1");}}});t1.start();t2.start();}
}

在这个场景下,可以使用这种方法解决这个问题
在这里插入图片描述

n个线程,m把锁

科学家就餐问题

在这里插入图片描述

出现死锁的四个必要条件
1.锁的基本特性,锁是互斥的
2.锁不可被抢占,A获取到locker,当A线程还没有释放的的时候,B把locker锁抢过来了,导致A线程阻塞
3.请求 和 保持 A线程持有locker1锁,还没有释放,但是又开始获取locker2
4.循环等待,门钥匙锁车里了,车钥匙锁家里了

如何避免呢?
1.避免请求和保持,也就是要避免锁的嵌套
2.打破循环等待,按照顺序进行加锁,如果一个线程有多把锁,可以进行编号,让其从小到大顺序进行加锁

在这里插入图片描述

volatile 关键字

volatile修饰的变量可以保证”内存可见性

public class demo15 {private static int  flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() ->{while (flag == 0){//这里什么都不做}System.out.println("t1 线程结束");});Thread t2 = new Thread(() ->{Scanner sc = new Scanner(System.in);System.out.println("请输入非0数结束线程t1");flag = sc.nextInt();System.out.println("t2结束");});t1.start();t2.start();}}

我们输入一个非0元素,其t1线程的循环会结束,但是这里并没有结束
在这里插入图片描述
内存可见性问题其实是由编译器优化导致的
由javac将其 .java文件 => .class文件
jvm文件执行.class文件
在这里插入图片描述
再java中,由工作内存和主内存
主内存可以看成内存
工作内存可以看成 寄存器 / 缓存
因此可以使用volatile修饰这里的flag变量,这里就会打破上面的优化,这里加上这个修饰,其就会还是从内存中读取值放入寄存器/缓存中,这样就不会出现上面的问题,但是这里虽然准确了,但是其速度变慢了

public class demo15 {private volatile static int  flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() ->{while (flag == 0){//这里什么都不做}System.out.println("t1 线程结束");});Thread t2 = new Thread(() ->{Scanner sc = new Scanner(System.in);System.out.println("请输入非0数结束线程t1");flag = sc.nextInt();System.out.println("t2结束");});t1.start();t2.start();}}

在这里插入图片描述
使用sleep

public class demo15 {private static int  flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() ->{while (flag == 0){//这里什么都不做try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1 线程结束");});Thread t2 = new Thread(() ->{Scanner sc = new Scanner(System.in);System.out.println("请输入非0数结束线程t1");flag = sc.nextInt();System.out.println("t2结束");});t1.start();t2.start();}}

为什么这里使用sleep也会是正确的呢,其sleep并不是可以解决内存可见性,而是因为sleep对应的指令非常多,比上面的load还多,所以这里编译器就不会优化掉
在这里插入图片描述

wait和notify

因为多线程下,线程调度是随机的,但是我们其实并不希望其是随机的,我们想要确定其线程调度顺序,因此这里就要使用wait和notify这两个方法

方法说明
public final void wait()让线程进入等待,等到notify结束
public final native void wait(long timeoutMillis)有时间限制等待,精确到毫秒
public final void wait(long timeoutMillis, int nanos)同理,精确到纳秒

wait()方法
1.是当前线程执行的代码进入等待
2.释放当前锁
3.满足一定条件唤醒当前锁(notify),会重新获取锁
这里的wait方法要结合synchronized锁来使用,否则会报错
唤醒方式
1.使用该对象的notify方法唤醒
2.如果是时间限制wait方法,就等到时间限制结束也可以唤醒
3.或者暴力方法,调用interrupted方法,抛出InterruptedException异常
在这里插入图片描述

方法说明
public final native void notify()唤醒一个wait状态的线程 ,如果有多个就随机唤醒一个
public final native void notifyAll()唤醒全部wait等待的线程

使用notify()方法唤醒wait()线程等待,让其重新获取该对象锁
但是这里要执行完notify()方法中的所有逻辑,结束以后才释放锁,这样wait()才会开始重新获取锁

public class demo16 {private static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized(locker){System.out.println("t1 wait之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1 wait之后");}});Thread t2 = new Thread(() ->{synchronized (locker){System.out.println("notify 之前");Scanner sc = new Scanner(System.in);System.out.println("请输入任意内容,触发notify");sc.nextInt();locker.notify();System.out.println("notify 之后");}});t1.start();t2.start();}
}

运行结果如下
在这里插入图片描述
唤醒所有wait

public class demo17 {private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{synchronized (locker){System.out.println("t1开始");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1结束");}});Thread t2 = new Thread(() ->{synchronized (locker){System.out.println("t2开始");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2结束");}});Thread t3 = new Thread(() ->{synchronized (locker){System.out.println("t3开始");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t3结束");}});Thread t4 = new Thread(() ->{synchronized (locker){System.out.println("t4开始");Scanner sc = new Scanner(System.in);System.out.println("输入任意,唤醒wait");sc.next();locker.notifyAll();System.out.println("t4结束");}});t1.start();t2.start();t3.start();t4.start();}
}

在这里插入图片描述
此时如果使用notify,这时候只会随机唤醒一个线程,并不会唤醒所有,程序也不会结束
在这里插入图片描述

wait和sleep区别
相同:都可以让线程等待,都可以设置最大等待时间
都可以被interrupt唤醒,但是wait更希望被notify唤醒,而sleep和interrupt,这可能会导致线程直接中止
1.wait需要搭配synchronized 使用,而sleep不需要
2.wait是Object的方法,sleep是Thread静态方法
3.wait是为了被notify,等待时间只是最后才会使用,而sleep就是休眠一定时间
4.wait会先释放锁,再获取锁,而sleep休眠不会释放锁

http://www.dtcms.com/a/452844.html

相关文章:

  • [工作流节点16] 更新他表记录的自动化方案:跨表数据联动的关键实现
  • 南京金融网站建设wordpress热门文章调用
  • 针对 OpenMMLab 视频理解(分类)的 MMAction2 的环境配置
  • 中国电信用户行为实时分析系统运维实战
  • HTTP、WebSocket、XMPP、CoAP、MQTT、DDS 六大协议在机器人通讯场景应用
  • 长春网站制作招聘信息上海网站被查
  • 做自媒体视频搬运网站网站建设与管理淘宝
  • IP 协议的相关特性
  • 《投资-88》价值投资者的认知升级与交易规则重构 - 第三层:估值安全边际,“再好的公司,如果买贵了,也会变成一笔糟糕的投资。”
  • 工程师 - Raspberry Pi Pico程序:读取SPI数据后从串口输出
  • 虚幻引擎5 GAS开发俯视角RPG游戏 P04-12 可缩放浮点数的曲线表
  • 接口请求工具对比 apifox apipost swagger postman等
  • C++联合体(Union)详解:与结构体的区别、联系与深度解析
  • LangChain部署RAG part2.搭建多模态RAG引擎(赋范大模型社区公开课听课笔记)
  • SSM--day4--SpringMVC(补充)
  • Flink Checkpoint与反压问题排查手册:从日志分析到根因定位
  • 元宇宙的教育应用:重构学习体验与知识传递
  • 建设99网站江西网站开发哪家好
  • RabbitMQ高可用集群搭建教程(基于CentOS 7.9 + Erlang 23.2.7 + RabbitMQ 3.8.8)
  • 【LangChain】P14 LangChain 输出解析器深度解析:Json解析器、XML解析器、字符串及列表、日期解析器
  • 仿真软件-多机器人2
  • 《基于 ERT 的稀疏电极机器人皮肤技术》ICRA2020论文解析
  • 聚焦CRISPR技术配套工具链的开源生态建设
  • 网站做视频窗口接口收费么免费搭建自己的网站
  • ​​Avalonia UI 开发核心注意事项:从理念到部署的避坑指南​
  • 从chatGPT获取的关于相机焦距与其他参数的关系
  • 拒绝做网站的理由wordpress自适应 slide
  • 【IT老齐456】Spring Boot优雅开发多线程应用,笔记01
  • 网站收录怎么弄极路由4 做网站
  • 备考华为HCIA - 云计算,培训与自学到底该怎么选?