【JavaEE初阶 -- 多线程2】

这里写目录标题

  • 1.线程安全
    • 1.1 什么是线程安全
    • 1.2 原子性
    • 1.3 线程不安全的原因
    • 1.4 通过synchronized进行加锁解决线程安全问题
    • 1.5 可重入锁
    • 1.6 死锁
    • 1.7 Java标准库中的线程安全类
    • 1.8 通过volatile关键字解决内存可见性引起的线程安全问题
  • 2. wait 和notify
    • 2.1 wait() --使当前线程进行等待,释放当前锁;满足一定条件时被唤醒,重新尝试获取这个锁
    • 2.2 notify()--是唤醒等待的线程
    • 2.3 wait 和sleep直接的区别:

1.线程安全

1.1 什么是线程安全

某个代码在单线程下执行没有任何问题,在多线程下执行出现bug

1.2 原子性

把代码比如一个房间,每个线程就是要进入这个房间的人。如果没有任何保护机制,A进入之后,进行一些列隐私操作,然后B也可以进入房间,从而打断A,这样就是不具备原子性的。把线程A进入房间的一系列操作进行打包成一个整体进行上锁,其他线程就进不来,这样就保证了代码的原子性

1.3 线程不安全的原因

  • 根本原因:操作系统上的线程是随即调度、抢占式执行的,线程之间执行的顺序带来了很多变数
  • 代码结构:代码中多个线程,同时修改一个变量(一个线程修改,一个线程读也可能存在问题)
  • 直接原因:多线程的修改操作下,本身不是原子的一条Java语句不一定是原子的,也不一定只是一条指令,比如 i++,有三步操作:1.从内存把数据读到CPU;2.进行数据更新;3.把数据写回到CPU;
  • 多个CPU指令,一个线程执行这些指令,执行到一半被调度走,从而其他线程可能会被调度;每个CPU指令,都是原子的,要么执行完,要么不执行
  • 内存可见性问题
  • 指令重排序问题

1.4 通过synchronized进行加锁解决线程安全问题

synchronized关键字,随便放Object对象都行,两个线程之间是否使用的是同一个对象,是同一个会产生竞争,反则是不会。进入代码块加锁,出代码块是解锁。synchronized 修饰普通方法,相当于给this加锁(锁对象是this),修饰静态方法,相当于给类对象加锁

public class ThreadDemo19 {private static int count = 0;private static int count2 = 0;public static void main(String[] args) throws InterruptedException {// 随便创建个对象Object locker = new Object(); //两个线程之间是否使用的是同一个对象,是同一个会产生竞争,反则是不会Object locekr2 = new Object();// 创建两个线程,每个线程都针对上述count变量循环5w次 循环自增的代码存在线程安全问题int tmp = 0;Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {/*// 1)一下线程修改一个变量,没有影响count++;*/// 加锁  synchronizedsynchronized(locker) { //进入大括号 就会加锁count++;} // 出大括号 就会解锁// 2)多个线程读取同一个变量,不会影响//System.out.println(count); //只是读取变量,变量的内容是固定不变的}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {//3)多个线程修改不同的变量,没有影响//count2++;//System.out.println(count);//两个线程针对不同对象加锁,存在不了锁竞争,就会出现线程安全问题/*synchronized (locekr2) {count++;}*/synchronized (locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();//打印count的结果System.out.println("count = "+ count);}
}

synchronized 加锁的效果也称为 互斥性

class Test {public int count;// synchronized 是加到static方法上,就等价于给 类对象加锁/*synchronized public static void func() {}public static void func() { // 使用较少synchronized (Test.class) {}}*/// 等同于下面 synchronized (this)的代码/*synchronized public void add() {count++;}*/public void add() {/*synchronized (this) {count++;}*/// 获取Test类的对象 (在一个java进程中,一个类的类对象都是只有一个)// 所以在这里第一线程和第二个线程拿到的类对象是同一个类对象,因此锁竞争仍然存在能保证线程安全synchronized (Test.class) {count++;}}}
public class ThreadDemo20 {public static void main(String[] args) throws InterruptedException {Test test = new Test();Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {test.add();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {test.add();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = "+test.count);}
}

1.5 可重入锁

可重入锁:一共就只有一把锁,同一个线程,此时锁对象就知道第二次加锁的线程就是持有锁的线程,第二次进行加锁的发现加锁线程和持有锁线程是同一个线程,即能加锁
判定当前加锁线程是否是加锁的线程,如果不是同一个线程,阻塞;如果是同一个线程,++计数器。

public class ThreadDemo21 {public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(() -> {synchronized (locker) { //可重入锁:在最外层的{进行加锁 真正加锁,同时把计数器+1(初始是0,+1//之后就变成了1,说明当前这个对象被该线程加锁一次 )同时记录线程是谁//再加一个锁,当前由于是同一个线程,此时锁对象就知道了第二次加锁的线程,就是持有锁的线程// 第二次操作,就可以直接放行通过,不会出现阻塞   ——》这个特性 称为“可重入”synchronized (locker) { //第二次加锁的时候,发现加锁线程和持有锁线程是同一个线程,即能加锁//成功,++计数器,如果不是同一个线程,阻塞System.out.println("hello");} // 把计数器-1,2-1=》1,不为0.不会真的解锁} // 在最外层的}进行解锁  1-1=0》 进行解锁});t.start();}
}

1.6 死锁

加锁是解决线程安全问题,但是加锁方式不当就可能产生死锁。
死锁三种典型场景:

  1. 一个线程,一把锁,如果锁不是可重入锁,并且是一个线程对这把锁加锁两次就会出现死锁。(钥匙锁屋里了)
  2. 两个线程,两把锁,线程1获取到 锁A,线程2 获取到锁B,然后线程1 尝试获取B,线程2 尝试获取 锁A,就会出现死锁。(钥匙锁车里,车钥匙锁房间里),解决方案:约定加锁顺序,先对A进行加锁,再对B进行加锁)
public class ThreadDemo22 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {//sleep一下,给t2时间,让t2也能拿到Btry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//尝试获取B,并没有释放Asynchronized (B) {System.out.println("t1拿到了两个线程");}}});/*Thread t2 = new Thread(() -> {synchronized (B) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//尝试获取A,并没有释放Bsynchronized (A) {System.out.println("t2拿到了两个线程");}}});*/Thread t2 = new Thread(() -> {synchronized (A) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 解决方案:先对A进行加锁,再对B进行加锁synchronized (B) {System.out.println("t2拿到了两个线程");}}});t1.start();t2.start();}
}
  1. N个线程,M把锁(哲学家就餐问题),解决方案:指定加锁顺序,针对五把锁,都进行编号,约定每个线程获取锁的时候,一定要先获取编号小的锁,后获取编号大的锁。

产生死锁的四个必要条件

  1. 互斥使用:获取锁的过程是互斥的,一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞 等待(锁最基本的特性,不太好破坏)
  2. 不可抢占:一个线程拿到了这把锁,只能主动解锁,不能让别的线程强行把锁抢走。(也是锁最基本的特性)
  3. 请求保持:一个线程拿到了锁A之后,在持有锁A的前提下,尝试获取锁B。(根据代码结构看实际情况可破坏)
  4. 循环等待/环路等待 (代码结构最容易破坏的
    解决死锁问题,核心思路就是破坏上述的必要条件,破坏其一,就能解决死锁问题。只要指定一定的规则,就可以有效的避免循环等待

1.7 Java标准库中的线程安全类

Java标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

还有一些是线程安全的,使用了一些锁机制来控制:

  • Vector(不推荐使用)
  • HashTable(不推荐使用)
  • ConcurrentHashMap
  • StringBuffer
    没加锁,不涉及修改,仍然是线程安全的:
  • String

1.8 通过volatile关键字解决内存可见性引起的线程安全问题

  • volatile能够保证内存可见性;另一个功能:禁止指令重排序
  • 什么叫内存可见性:就是高度依赖编译器的优化的具体实现
  • 如果一个线程写,一个线程读也可能存在线程安全问题

设计一个预期通过 t2 线程输入的整数,只要输入的不为0,就可以使t1线程结束。

  • 下面代码中 t2修改了内存,但是 t1没有看到这个内存的变化,即内存可见性问题。
  • 在这里的内存可见性问题:
    在多线程下,编译器对代码的优化出现了误判,本来编译器期望 把读内存操作给优化成读寄存器中缓存的值,这样优化有助于我们提高循环的执行效率,并且编译器发现没有谁来修改flag,从而进行了错误的判断,在后续通过scanner用户输入来修改flag,导致这边 t2 线程修改了,而上面 t1 线程判断flag是否为0这边没有生效,因此出现了这边不能让t1结束的bug

原理
解决方案:给判断的变量 添加volatile关键字,在写入的时候

  • 改变线程工作中内存volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主存中

在读取volatile修饰的变量的时候

  • 从内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本
import java.util.Scanner;
public class ThreadDemo23 {// t2修改了内存,但是t1没有看到这个内存的变化,就称为 内存可见性 问题//volatile关键字 核心功能:保证内存可见性; 另一个功能:禁止指令重排序private volatile static int flag = 0;public static void main(String[] args) {// 预期通过t2线程输入的整数,只要输入的不为0,就可以使t1线程结束Thread t1 = new Thread(() -> {while (flag == 0) {// 循环体里,没有内容/*try {// 不加sleep,一秒循环上百亿次,load操作的整体开销非常大,优化的迫切程度就更高// 加了之后,一秒循环1000次,load整体开销就没这么大,优化的迫切程度就降低了Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}*/}System.out.println("t1 线程结束");});Thread t2 = new Thread(() -> {System.out.println("请输入flag的值");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}
}

volatile和synchronized区别

  • volatile保证的是内存可见性
  • synchronized保证的是原子性,也可以保证内存可见性

会出现线程安全:

// 会出现线程安全,无法保证最后结果是 100000
static class Counter {volatile public int count = 0;void increase() {count++;}}public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);
}

使用synchronized,加锁,去掉volatile,给t1的循环内部加锁,并借助counter对象加锁:

static class Counter {public int flag = 0;
}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (true) {synchronized (counter) {if (counter.flag != 0) {break;}}// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();
}

2. wait 和notify

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序是随机的。

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.
  • 这些都是Object方法

2.1 wait() --使当前线程进行等待,释放当前锁;满足一定条件时被唤醒,重新尝试获取这个锁

wait做的事情:

  • 使当前执行代码的线程进行阻塞等待,(把线程放到等待队列中)
  • 释放当前锁
  • 满足一定条件时被唤醒,重新尝试获取这个锁

wait() 要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常。即要先有锁,才能调用wait且对象必须是同一对象
wait结束等待的条件:

  • 其他线程调用该对象的notify方法
  • wait等待时间超时( wait(long timeout),来指定等待时间)
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

wait 被唤醒后也要重新参与锁竞争

public class ThreadDemo24 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("wait 之前");object.wait();System.out.println("wait 之后");}}
}

2.2 notify()–是唤醒等待的线程

使用的哪个对象就是唤醒哪个对象的wait,如果两个wait是同一个对象调用的,随机唤醒其中一个,而notifyAll 唤醒这个对象所有等待的线程

下面代码执行的过程:

  1. t1 执行起来之后,就会立即拿到锁,并且打印t1 wait之前,进入wait方法(释放锁+阻塞等待)
  2. t2 执行起来之后,先进行sleep(这个sleep作用是让 t1 能够先拿到锁)
  3. t2 sleep结束之后,由于t1是wait状态,锁是释放的,t2 就能拿到锁,接下来打印 t2 notify之前,执行notify操作,即唤醒t1,(此时t1就从WAITING状态恢复回来)
  4. 但是由于t2 此时还没有释放锁,t1 WAITING恢复之后,尝试获取锁,就可能会出现一个小小的阻塞,这个阻塞时由于锁竞争引起的。
  5. t2 执行完t2 notify之后,释放锁,t2 执行完毕,t1 的wait就可以获取锁,继续执行打印t1 wait之后。
public class ThreadDemo25 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker) {System.out.println("t1 wait之前");try {// 释放锁,阻塞等待locker.wait();  // 死等//locker.wait(100);  带有超时的等待,ms 如果这个时间内没有进行notify,就不等} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 wait之后");}});Thread t2 = new Thread(() -> {try {// 如果sleep写到synchronized外面的话,由于t1和t2执行顺序不确定,就可能t2先拿到锁,// t1 没执行到 wait  t2就先notifyThread.sleep(5000); // 让t1先拿到锁// 由于locker.wait(),锁是释放的,t2就能拿到锁synchronized (locker) {System.out.println("t2 notify 之前");// 唤醒t1,t1从WAITING 状态恢复过来// 由于t2此时还没有释放锁,t1恢复之后尝试获取锁,就可能出现锁竞争从而导致阻塞locker.notify();System.out.println("t2 notify 之后");}} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t2.start();}
}

2.3 wait 和sleep直接的区别:

  • wait提供了一个带有超时的版本,sleep也是能指定时间,都是时间到就继续执行,解除阻塞。
  • wait 和sleep 都可以被提前唤醒,wait通过notify,sleep通过interrupt唤醒
  • 使用wait 在不知道要等多久的前提下使用,所谓超时时间,即兜底的
  • 而使用sleep,要知道具体等多久,能提前唤醒,但是是异常唤醒,=。

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

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

相关文章

【30天】Python从入门到精通详解版—第一天—Python 基础语法详细讲解-上

【30天】Python从入门到精通详解版—第一天—Python 基础语法详细讲解-上 Python变量Python数据类型数字类型&#xff08;Number&#xff09;字符串类型&#xff08;String&#xff09;列表类型&#xff08;List&#xff09;元组类型&#xff08;Tuple&#xff09;字典类型&…

数字华容道项目总结

目录 项目名称&#xff1a; 项目背景&#xff1a; 项目目标&#xff1a; 项目成果&#xff1a; 经验教训及学习&#xff1a; 未来优化&#xff1a; 项目名称&#xff1a; 数字华容道小游戏 项目背景&#xff1a; 在学习编程过程中&#xff0c;为了加强特性技术和对概念…

Text-to-SQL 工具Vanna进阶|数据库对话机器人的多轮对话

跟数据库对话机器人对话,我可不止一个问题。 可能基于第一句问话,还有第二句、第三句问话。。。第N句对话。所以本文测试了多轮对话功能。 单轮对话的环境搭建参考博客 Text-to-SQL 工具Vanna + MySQL本地部署 | 数据库对话机器人 我的数据是这样 1. 基础配置 import vann…

Hadoop基础架构及其特点解析

​一、Hadoop基础架构概述 Hadoop是一个由Apache基金会所开发的分布式系统基础架构&#xff0c;它利用集群的威力进行高速运算和存储。用户可以在不了解分布式底层细节的情况下&#xff0c;开发分布式程序。Hadoop不仅稳定可靠&#xff0c;而且具有强大的可伸缩性&#xff0c;…

Parade Series - WebRTC ( < 300 ms Low Latency )

Parade Series - FFMPEG (Stable X64) C:\Conda\parading-cam>ffmpeg -f dshow -i video"Surface Camera Front" -vcodec libx264 -preset:v ultrafast -tune:v zerolatency -an -rtsp_transport tcp -f rtsp://127.0.0.1:8554/cam0801

SCI 图像处理期刊

引用 一区 1. IEEE TRANSACTIONS ON PATTERN ANALYSIS AND MACHINE INTELLIGENCE 顶刊:是 出版商:IEEE 2. IEEE Transactions on Multimedia 顶刊:是 出版商:IEEE 3. Information Fusion 顶刊:是 出版商:ELSEVIER 4.IEEE TRANSACTIONS ON IMAGE PROCESSING 顶刊:是 出版商:I…

前端面试题 ===> 【Ajax、请求】

Ajax 面试题总结 1. 什么是Ajax&#xff0c;原理是什么&#xff1f; 定义&#xff1a; 在网页中利用XMLHttpRequest对象和服务器进行数据交互的方式称为Ajax&#xff1b; XMLHttpRequest是Ajax的核心机制&#xff0c;他是IE5首先引入的&#xff0c;是一种支持异步请求的技术&…

ABB IORR2100-30拍合式接触器

ABB的IORR2100-30拍合式接触器是一款高性能的电气元件&#xff0c;主要用于控制和保护电路。这种接触器具有快速响应、高可靠性以及良好的耐久性等特点&#xff0c;广泛应用于工业自动化、电力系统和电机控制等领域。 拍合式接触器的设计使其能够在需要时迅速接通或断开电路。当…

45. 跳跃游戏 II(力扣LeetCode)

文章目录 45. 跳跃游戏 II贪心 45. 跳跃游戏 II 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j <…

《次神光之觉醒》游戏简单拆解

文章目录 一、 介绍二、 游戏拆解图三、 部分模块拆解1.主界面关卡制放置挂机2.养成升级战斗属性值角色养成装备养成技能养成天赋养成藏品养成契约养成宠物养成挖矿养成 3.副本4.任务5.pvppvp竞技场锦标赛黄金矿工锦标赛公会营地 6.社交游戏圈世界频道好友系统 一、 介绍 《次…

谈谈Darknet53为啥这么难训练

在我使用Imagenet2012对Darknet53进行预训练的时候&#xff0c;往往训练到一半&#xff0c;就会出现过拟合&#xff0c;导致无法继续向下训练&#xff0c;尝试了很多方法&#xff0c;最后发现问题出现在下图红框的部分。 得出这个结论是因为当我使用Resnet中&#xff0c;包含有…

Java集合基础知识总结(绝对经典)

List接口继承了Collection接口&#xff0c;定义一个允许重复项的有序集合。该接口不但能够对列表的一部分进行处理&#xff0c;还添加了面向位置的操作。 实际上有两种list&#xff1a;一种是基本的ArrayList&#xff0c;其优点在于随机访问元素&#xff0c;另一种是更强大的L…

利用HubSpot出海CRM和人工智能技术提升出海业务的效率和效果

在当今数字化时代&#xff0c;智能化营销已经成为企业获取客户和扩大市场份额的关键策略。特别是对于出海业务而言&#xff0c;利用智能化营销技术来应对不同文化、语言和市场的挑战&#xff0c;已经成为企业竞争的关键优势。今天运营坛将带领大家探讨如何利用HubSpot CRM和人工…

大模型在智能推荐系统中的应用与挑战

摘要&#xff1a; 本文将深入探讨大模型在智能推荐系统中的应用与挑战&#xff0c;并详细介绍实现智能推荐系统的关键步骤。文章结构将包括引言、基础知识回顾、核心组件、实现步骤、代码示例、技巧与实践、性能优化与测试、常见问题与解答、结论与展望以及附录。 引言&#…

技术方案|某工业集团PaaS容灾方案

在当今快速发展的数字化时代&#xff0c;业务的连续性和稳定性已成为企业核心竞争力的重要组成部分。然而&#xff0c;由于各种原因&#xff0c;企业常常面临着数据丢失、系统瘫痪等潜在风险。因此&#xff0c;制定一套科学、高效的容灾方案至关重要。本文将围绕某全球领先的工…

css实现梯形?

HTML: <div class"box"></div> CSS: .box{width:50px;height:0;border-bottom:50px solid pink;border-left:50px solid #fff;border-right:50px solid #fff; } 效果&#xff1a;

opencv dnn模块 示例(25) 目标检测 object_detection 之 yolov9

文章目录 1、YOLOv9 介绍2、测试2.1、官方Python测试2.1.1、正确的脚本2.2、Opencv dnn测试2.2.1、导出onnx模型2.2.2、c测试代码 2.3、测试统计 3、自定义数据及训练3.1、准备工作3.2、训练3.3、模型重参数化 1、YOLOv9 介绍 YOLOv9 是 YOLOv7 研究团队推出的最新目标检测网络…

22.4 docker

22.4 docker 1. docker简介2. docker架构3. 容器和仓库4. Docker安装:基于Linux5. docker容器生成与运行5.1 docker换源5.2 启动容器5.3 docker search6. docker基于redis演示6.1 run -i -t:终端运行redis容器(不推荐)6.2 访问docker正在运行容器6.3 -d:后台运行redis容器6.4 停…

IDEA开启Run Dashboard

1、Run Dashboard是什么&#xff0c;为什么要使用 Run Dashboard 是 IntelliJ IDEA 中的一个工具窗口&#xff0c;用于管理和监视项目中正在运行的应用程序和配置。它提供了一种集中管理运行和调试过程的方式&#xff0c;可以让开发人员更方便地查看和控制正在运行的应用程序。…

2061:【例1.2】梯形面积

时间限制: 1000 ms 内存限制: 65536 KB 提交数:201243 通过数: 79671 【题目描述】 在梯形中阴影部分面积是150平方厘米&#xff0c;求梯形面积。 【输入】 (无&#xff09; 【输出】 输出梯形面积&#xff08;保留两位小数&#xff09;。 【输入样例】 &#xff…