Java多线程(2)---线程控制和线程安全的详细讲解

目录

前言

一.线程控制方法

1.1启动线程--start()

1.2线程睡眠---sleep()方法

1.3中断线程--interrupt() 方法

1.4等待线程---join()

二.线程安全 

2.1数据不安全---数据共享

⭐不安全的演示和原因

 ⭐不安全的处理方法

⭐synchronized的使用

2.2数据不安全---内存可见性

⭐不安全的演示和原因

⭐不安全的处理方法

2.3 synchronized和volatile的区别

三.认识wait()、notify()

3.1wait()方法

3.2notify()方法

3.3wait()方法和sleep()方法的对比

四.总结


🎁个人主页:tq02的博客_CSDN博客-C语言,Java,Java数据结构领域博主
🎥 本文由 tq02 原创,首发于 CSDN🙉
🎄 本章讲解内容:线程的控制、安全讲解

🎥学习专栏:  C语言         JavaSE       MySQL基础  

前言

        JavaEE的多线程知识点,我们在 多线程(1) 中学习到了如何创建多线程、多线程的运行状态等,而本章主要讲解多线程的线程控制手段以及线程安全。

一.线程控制方法

       线程控制:就是控制线程的执行速率,让某某线程先执行等。 根据生命周期,我们可以知道线程控制方法大概有:start()、sleep()、interrupt()、join()、wait()、yield()、notify()、

1.1启动线程--start()

class MyThread extends Thread {@Overridepublic void run() {System.out.println("这里是线程运行的代码");}
}public class Text{public static void main(String[] args) {//创建MyThread实例MyThread t1=new MyThread();//调用start方法启动线程t1.start();//t1.run();  虽然也是执行run函数当中的操作,但是并不是启动线程,而是调用方法}
} 

通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。并且不是指调用run方法就可以使用线程运行,而是单纯的执行方法run,并不能达到并行的机制,只有使用start()方法才可以使线程运行

1.2线程睡眠---sleep()方法

        线程会暂停运行,并且可以设置暂停运行的时间,例如:sleep(1000),是休眠了1秒。

注:是暂停运行,而不是停止运行,而时间的设置长度更像是闹钟,时间一到,就开始运行。

1.3中断线程--interrupt() 方法

线程一旦开始运行,想让线程强制停止,目前有2种方法。

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

1.通过共享的标记来进行沟通:

        使用自定义的变量来作为标志位.并且给标志位上并且final修饰,后期会讲解volatile 关键字,使用它也可以执行操作。

public class Text{// 写作成员变量就不是触发变量捕获的逻辑了. 而是 "内部类访问外部类的成员" , //本身就是 ok 的~~public static boolean isQuit = false;public static void main(String[] args) throws InterruptedException {//或者 final boolean isQuit = false;Thread t = new Thread(() -> {while (!isQuit) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();// 主线程这里执行一些其他操作之后, 要让 t 线程结束.Thread.sleep(3000);// 这个代码就是在修改前面设定的标志位.isQuit = true;System.out.println("把 t 线程终止");}
} 

从上面的代码当中,我们可以发现isQuit变量用于lambda表达式,因此会有一个变量捕获的环节,而在变量捕获当中,捕获到的外部变量isquit不可以修改,因此需要使用final修饰,主线程执行完毕时,将isQuit修改为true;导致子线程强行结束。

2.调用 interrupt() 方法来通知

Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

Thread.currentThread():获取当前对象的引用,isInterrupted()方法是指提供一个标志位。

Thread.interrupted():将标志位改为true

 public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {// Thread.currentThread 就是 t .// 但是 lambda 表达式是在构造 t 之前就定义好的. 编译器看到的 lambda 里的 t 就会认为这是一个还没初始化的对象.因此wile的括号里不可以使用t.while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {// e.printStackTrace();break;}}});t.start();Thread.sleep(3000);// 把上述的标志位给设置成 truet.interrupt();}

当线程正在sleep()休眠当中,如果使用interrupt()唤醒,则会唤醒,继续运行

1.4等待线程---join()

        等待线程,是指两个线程进行时,一个线程等待另一个线程执行结束,才可以执行结束。

  public static void main(String[] args) {Thread b = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("张三工作");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("张三工作结束了");});Thread a = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("李四工作");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}try {// 如果 b 此时还没执行完毕, b.join 就会产生阻塞的情况b.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("李四工作结束了");});b.start();a.start();}

        例如,原本李四工作比张三工作要快,可是我使用了join()方法,然后让李四等待张三结束,才能结束。方法还可以使用其他参数用于不同意思。

方法作用
join()等待线程结束
jion(long millis)等待线程结束,或者等待一定时间自动结束

而除了这些操作方法之外,我们还有wait()、notify()方法,但是需要先学习线程安全才能更好的学习。

二.线程安全 

        线程安全指:多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程不安全出现的根本原因:

  1. 线程是抢占式执行
  2. 多个线程同时对共享数据进行操作,产生数据覆盖
  3. 内存可见性
  4. 指令重排序

2.1数据不安全---数据共享

⭐不安全的演示和原因

class Counter {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);
}

例如:以上2个线程,都执行50000次的count++,按理而言count最后的结果是100000次,可是执行之后却不足100000次。数据会存放在堆区,多个线程都可以访问

出现的原因

count++实际上操作:

  1. 内存的数据加载到CPU寄存器中
  2. 寄存器中的数据执行++
  3. 返回给内存。

      由于count在堆上,会被多个线程共享访问, t1和t2线程同时获取count元素,进行自增,然后将结果返回内存器当中,t1返回2,t2页返回2,将t1返回的值覆盖,因此少了一次自增


 ⭐不安全的处理方法

        线程不安全的原因主要是多个线程会共享访问数据,导致数据的原子性被破坏,因此我们可以使用一种方法,让其他线程知晓该数据正在被其他线程使用(可见性)。

方法加锁

当一个线程在执行操作时,对需要操作的数据或者方法进行加锁,这样其他线程使用不到了。

 例如,需要操作的方法或者数据是一个房间,线程一进去时,将门锁了,只有线程一操作结束,出来时,线程二才能进去

使用关键字synchronized,加锁操作:

class Counter {public int count = 0;synchronize 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);
}

将increase()方法进行加锁,当一个线程进入时,就会进行加锁,其他线程只能等待该线程释放。


⭐synchronized的使用

        synchronized关键字进行加锁时,是需要对相同的对象展开

加锁的目的:为了互斥使用资源,而加锁的对象也是需要相同的,两个线程对同一个对象进行加锁时,会产生锁竞争/锁冲突。

class Counter {public int count = 0;void increase() {synchronized(this){count++;}}
}

this是目前对象,而我们也可以使用其他对象。

2.2数据不安全---内存可见性

⭐不安全的演示和原因

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

        在这个代码中,我们只需要输入非零数,按逻辑而言,线程t1也应该会结束,可事实上,t1不会结束,原因:t1读取自己工作内存中的内容,而t2对flag进行修改,t1却无法得知他的修改。

问题产生的原因:

  1. counter.flag==0;实际上的指令:load(读指令)jcmp(比较指令)
  2. 编译器发现这个这个逻辑是一样的,每一次都是需要读取指令,结果却是一样的,因此,编译器会将逻辑优化掉,直接进行jcmp指令。
  3. 因此即使flag发生了改变,t1也无法得知。

⭐不安全的处理方法

        使用volatile关键字,使用volatile关键字修饰变量之后,编译器就会禁止优化。

static class Counter {public volatile int flag = 0;
}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {//代码操作}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.3 synchronized和volatile的区别

  1. volatile不保证原子性,只保证了内存可见性
  2. synchronized保证了原子性、也保证了内存可见性

三.认识wait()、notify()

        由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。但是实际开发中,我们希望可以协调控制多个线程的执行先后顺序。

而涉及到协调控制方法:

  1. wait() / wait(long timeout):让线程进入等待状态
  2. notify() / notifyAll(): 唤醒在当前对象上等待的线程
     

3.1wait()方法

        线程进入等待状态,如果没有唤醒,则会一直一直等待,并且需要搭配synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait()结束等待的条件:

  1. 其他线程调用该对象的 notify 方法.
  2. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  3. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

代码示例:

public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("等待中");object.wait();System.out.println("等待结束");}
}

3.2notify()方法

        用于唤醒wait()进入等待的线程。在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notifyAll()方法:则是唤醒所有等待的线程。注:虽然是同时唤醒,但是需要竞争锁,因此并不是同时执行,依然有先来后到的执行

3.3wait()方法和sleep()方法的对比

        理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间

        wait 需要搭配 synchronized 使用. sleep 不需要.
        wait 是 Object 的方法 sleep 是 Thread 的静态方法.

四.总结

        volatile 能够保证内存可见性. 强制从主内存中读取数据,synchronize既可以保证原子性也可以保证内存可见性。

Java多线程实现数据共享的原理:

        JVM 把内存分成了这几个区域:
        方法区, 堆区, 栈区, 程序计数器.
        其中堆区这个内存区域是多个线程之间共享的.
        只要把某个数据放到堆内存中, 就可以让多个线程都能访问到

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

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

相关文章

性能测试—Jmeter工具

文章目录 性能测试1. 术语介绍2. 方法3. 应用场景4. 工具&#xff08;Jmeter&#xff09;4.1 介绍4.2 元件和组件4.2.2 元件4.2.1 组件 4.3 作用域4.4 参数化4.5 执行脚本 性能测试 1. 术语介绍 响应时间(Response time)&#xff1a;对请求作出响应所需要的时间。 在互联网上对…

QT学习笔记-QT安装oracle oci驱动

QT学习笔记-QT安装oracle oci驱动 0、背景1、环境以及条件说明2、编译驱动2.1 下载oracle instant client2.2 编译qt oci驱动2.2.1 修改oci.pro2.2.2 MinGW64构建套件编译2.2.3 MSVC2019_64构建套件编译 3、访问数据库运行成功 0、背景 在使用QT开发应用的过程中&#xff0c;往…

5G用户逼近7亿,5G发展迈入下半场!

尽管普遍认为5G投资高峰期正在过去&#xff0c;但是从2023年上半年的情况来看&#xff0c;我国5G建设仍在衔枚疾走。 近日举行2023年上半年工业和信息化发展情况新闻发布会上&#xff0c;工信部人士透露&#xff0c;截至今年6月底&#xff0c;我国5G基站累计达到293.7万个&…

LVS负载均衡部署-NAT

文章目录 LVS-NAT模式配置原理实验环境DR添加网卡准备两台Real Server134&#xff1a;145&#xff1a;测试&#xff1a; 修改RS网关配置DR&#xff1a;开启路由转发下载软件配置查看配置 测试 LVS-NAT模式配置 原理 当客户端访问DR时即访问的外网接口然后由DR将流量在本地转发…

番外13:使用ADS进行容差分析(蒙特卡洛分析、灵敏度分析、良率分析、良率优化),以带通滤波器设计为例

番外13&#xff1a;使用ADS进行容差分析&#xff08;蒙特卡洛分析、灵敏度分析、良率分析、良率优化&#xff09;&#xff0c;以带通滤波器设计为例 资源下载 https://download.csdn.net/download/weixin_44584198/88210327 技术背景 容差分析是当前电子可靠性设计中最先进…

Docker源码阅读 - goland环境准备

docker 源码分为两部分 cli 和 moby&#xff08;docker&#xff09; tips: docker是从moby拷贝过去的&#xff1b;docker整体是一个C-S架构&#xff0c;cli客户端&#xff0c;docker服务端 docker-ce&#xff1a;https://github.com/docker/docker-ce cli&#xff1a;https://…

【数据结构与算法】十大经典排序算法-选择排序

&#x1f31f;个人博客&#xff1a;www.hellocode.top &#x1f3f0;Java知识导航&#xff1a;Java-Navigate &#x1f525;CSDN&#xff1a;HelloCode. &#x1f31e;知乎&#xff1a;HelloCode &#x1f334;掘金&#xff1a;HelloCode ⚡如有问题&#xff0c;欢迎指正&#…

C语言属刷题训练【第八天】

文章目录 &#x1fa97;1、如下程序的运行结果是&#xff08; &#xff09;&#x1f4bb;2、若有定义&#xff1a; int a[2][3]; &#xff0c;以下选项中对 a 数组元素正确引用的是&#xff08; &#xff09;&#x1f9ff;3、在下面的字符数组定义中&#xff0c;哪一个有语法错…

美团软件测试工程师高频面试题和答案

前言 8月底了&#xff0c;马上到了大家的找工作的高峰期了&#xff01;为了帮助大家更好的备战面试和跳槽&#xff0c;可以在众多求职者中脱颖而出&#xff0c;我帮大家准备了丰富的企业真实面试题&#xff0c;大家赶紧收藏吧&#xff01; 1、说下你最近做的项目&#xff0c;你…

DatawhaleAI夏令营第三期机器学习用户新增预测挑战赛baseline新手教程

本教程会带领大家项目制学习&#xff0c;由浅入深&#xff0c;逐渐进阶。从竞赛通用流程与跑通最简的Baseline&#xff0c;到深入各个竞赛环节&#xff0c;精读Baseline与进阶实践技巧的学习。 千里之行&#xff0c;始于足下&#xff0c;从这里&#xff0c;开启你的 AI 学习之旅…

python——案例17:判断某年是否是闰年

案例17&#xff1a;判断某年是否是闰年 判断依据&#xff1a;闰年就是能被400整除&#xff0c;或者能被4整除的年份numint(input(输入年份&#xff1a;))if num%1000: if num%4000: #整百年份的判断print("%s年是闰年"%num) #…

容器虚拟化基础之NameSpace

“只想从这无边的寂寞中逃出来。” 一、什么是虚拟化、容器化&#xff1f; 物理机:实际的服务器或者计算机。 这是相对于虚拟机而言的对实体计算机的称呼&#xff0c;物理机提供虚拟机以硬件环境&#xff0c;有时候也称为"宿主"或"寄主"。 虚拟机: 指通过软…

JDK、JRE、JVM:揭秘Java的关键三者关系

文章目录 JDK&#xff1a;Java开发工具包JRE&#xff1a;Java运行环境JVM&#xff1a;Java虚拟机关系概述 案例示例&#xff1a;Hello World结语 在Java世界中&#xff0c;你可能经常听到JDK、JRE和JVM这几个概念&#xff0c;它们分别代表了Java开发工具包、Java运行环境和Java…

Java类型转换

总是忘&#xff0c;总是记混&#xff0c;气气气&#xff01; 基本类型 4整型、2浮点型、1布尔、1字符 关键字大小取值范围包装类型byte8-27~27-1Byteshort16-215~215-1Shortint32-231~231-1Integerlong64-263~263-1Longfloat323.4e-38~3.4e38Floatdouble641.7e-38~1.7e38Dou…

Easy Rules规则引擎(1-基础篇)

目录 一、序言二、Easy Rules介绍三、定义规则(Rules)1、规则介绍2、编程式规则定义3、声明式规则定义 四、定义事实(Facts)五、定义规则引擎(Rules Engine)1、规则引擎介绍2、InferenceRulesEngine规则引擎示例(1) 定义触发条件(2) 定义规则触发后的执行行为(3) 测试用例 一、…

【CSH 入门基础 5 -- csh 文件监控脚本实现】

文章目录 背景CSHLL 代码实现cshell 中 unset 的介绍bash 中监控文件的方法 背景 由于开发代码是在外网编译&#xff0c;而镜像的烧写是在内网中的EDA工具中进行的&#xff0c;所以就需要先将代码在外网编译好后&#xff0c;再通过FTP工具将镜像传输到内网中&#xff0c;然后在…

uniapp 左右滑动切换页面并切换tab

实现效果如图 要实现底部内部的左右滑动切换带动上方tab栏的切换&#xff0c;并且下方内容要实现纵向滚动 &#xff0c;所以需要swiper&#xff0c;swiper-item,scroll-view组合使用 tab栏部分 <view class"tabs"><view class"tab_item" v-for&…

Matlab时频工具箱tftb下载及安装

Matlab 时频工具箱下载及安装 首先下载安装包安装包地址如下 链接: https://tftb.nongnu.org/ 点击下面的download 跳转到如下界面&#xff0c;选择下面的安装包下载 下载之后得到一个压缩包 然后找到Matlab的安装目录&#xff0c;右键桌面图标&#xff0c;打开文件所在位置…

网络编程小项目-tftp下载

tftp下载模型 TFTP通信过程总结 服务器在69号端口等待客户端的请求服务器若批准此请求&#xff0c;则使用 临时端口 与客户端进行通信。每个数据包的编号都有变化&#xff08;从1开始&#xff09;每个数据包都要得到ACK的确认&#xff0c;如果出现超时&#xff0c;则需要重新发…

SSL握手协议相关概念

下图为握手协议的流程图&#xff0c;具体的解释参考博客&#xff1a; 【下】安全HTTPS-全面详解对称加密&#xff0c;非对称加密&#xff0c;数字签名&#xff0c;数字证书和HTTPS_tenfyguo的博客-CSDN博客 下面梳理一下SSL协议中的一些细节。首先是相关名词&#xff1a;证书、…