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

news2024/11/15 13:42:21

目录

前言

一.线程控制方法

1.1启动线程--start()

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

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

1.4等待线程---join()

二.线程安全 

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

⭐不安全的演示和原因

 ⭐不安全的处理方法

⭐synchronize的使用

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 {
    @Override
    public 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);
        // 把上述的标志位给设置成 true
        t.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()方法进行加锁,当一个线程进入时,就会进行加锁,其他线程只能等待该线程释放。


⭐synchronize的使用

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

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

class Counter {
    public int count = 0;
        
    void increase() {
        synchronize(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.coloradmin.cn/o/854339.html

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

相关文章

条条大路通罗马系列—— 使用 Hiredis-cluster 连接 Amazon ElastiCache for Redis 集群

前言 Amazon ElastiCache for Redis 是速度超快的内存数据存储&#xff0c;能够提供亚毫秒级延迟来支持 实时应用程序。适用于 Redis 的 ElastiCache 基于开源 Redis 构建&#xff0c;可与 Redis API 兼容&#xff0c;能够与 Redis 客户端配合工作&#xff0c;并使用开放的 Re…

【动态规划】最长上升子序列 LIS

算法提高课笔记。 目录 问题的开始&#xff1a;最长上升子序列思路代码 怪盗基德的滑翔翼题意思路代码 登山题意思路代码 友好城市题意思路代码 拦截导弹题意思路代码 导弹防御系统题意思路代码 最长公共上升子序列题意思路代码 问题的开始&#xff1a;最长上升子序列 原题链…

linux后台运行程序命令screen

前言 我们在服务器终端或者是使用ssh连接服务器的时候&#xff0c;需要长期后台运行项目&#xff0c;但是我们一关终端可能程序进程就会被kill掉了&#xff0c;我们之前学习过2>&1 &后台部署的命令&#xff0c;但是这样我们查看项目进程的时候还需要去查询运行的PI…

TartanVO: A Generalizable Learning-based VO 论文阅读

论文信息 题目:TartanVO: A Generalizable Learning-based VO 作者&#xff1a;Wenshan Wang&#xff0c; Yaoyu Hu 来源&#xff1a;ICRL 时间&#xff1a;2021 代码地址&#xff1a;https://github.com/castacks/tartanvo Abstract 我们提出了第一个基于学习的视觉里程计&…

Netty使用和常用组件辨析

Netty 使用和常用组件 简述 <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId <version>4.1.42.Final </version> <scope>compile</scope> </dependency> Netty 的优势 1 、 AP…

42. 疯狂爬取王者荣耀所有皮肤高清海报(文末源码)

目录 前言 目的 思路 代码实现 1. 导包&#xff0c;部署好环境 2. 伪装请求头 3. 访问英雄列表&#xff0c;获取英雄ID 4. 分别访问各英雄主页&#xff0c;查看图片详情 5. 写入本地文件夹&#xff08;文件夹自动命名&#xff09; 完整源码 运行效果 总结 前言 阔…

流量、日志分析分析

这周主要以做题为主 先找找理论看然后在buuctrf以及nssctf找了题做 了解wireshark Wireshark是一款开源的网络协议分析软件&#xff0c;具有录制和检查网络数据包的功能&#xff0c;可以深入了解网络通信中的传输协议、数据格式以及通信行为。Wireshark可以捕获发送和接收的数…

冶金化工操作VR虚拟仿真实验软件提高员工们协同作业的配合度

对于高风险行业来说&#xff0c;开展安全教育培训是企业的重点工作&#xff0c;传统培训逐渐跟不上时代变化和工人需求&#xff0c;冶金安全VR模拟仿真培训系统作为一种新型的教育和培训工具&#xff0c;借助VR虚拟现实技术为冶金行业的工人提供一个安全、高效的培训环境。 冶金…

堡塔面板系统加固使用说明

更新日志&#xff1a; 宝塔系统加固5.0- 正式版 2023-08-07 1.加固php 配置文件 2.加固nginx 启动文件 宝塔系统加固4.1- 正式版 1、【修复】系统加固不会随系统启动自动开启的问题 2、【优化】大幅降低CPU使用率 宝塔系统加固4.0- 正式版 1、【增加】等保加固相关加固功能 2、…

Win10聚焦锁屏壁纸保存

前言 Win10聚焦锁屏每天都会推荐新的壁纸&#xff0c;其中有些质量超高的优秀壁纸&#xff0c;用户自然想下载保存下来&#xff0c;下文介绍如何保存。 若用户仅想保存当天的聚焦锁屏壁纸&#xff0c;则推荐方法1&#xff1b;若用户想保存以前的聚焦锁屏壁纸&#xff0c;则推…

git的简单介绍和使用

git学习 1. 概念git和svn的区别和优势1.1 区别1.2 git优势 2. git的三个状态和三个阶段2.1 三个状态&#xff1a;2.2 三个阶段&#xff1a; 3. 常用的git命令3.1 下面是最常用的命令3.2 git命令操作流程图如下&#xff1a; 4. 分支内容学习4.1 项目远程仓库4.2 项目本地仓库4.3…

通过anvt X6和vue3实现图编辑

通过anvt X6 X6地址&#xff1a;https://x6.antv.antgroup.com/tutorial/about&#xff1b; 由于节点比较复杂&#xff0c;使用vue实现的节点&#xff1b; x6提供了一个独立的包 antv/x6-vue-shape 来使用 Vue 组件渲染节点。 VUE3的案例&#xff1a; <template><div…

WebRTC | 信令服务器

目录 一、相关术语 1.NAT 2.STUN服务器 3. TURN服务器 4.打洞 二、WebRTC一对一架构 三、信令 1. 信令传输协议的选择 2. 信令服务器的实现方案 3. 信令服务器的业务逻辑 信令服务器的作用主要有两个&#xff1a;一是实现业务层的管理&#xff0c;如用户创建房间&…

【软件工程】数据流图/DFD概念符号/流程图分层/数据字典

【软件工程】数据流图/DFD概念符号/流程图分层/数据字典 目录 【软件工程】数据流图/DFD概念符号/流程图分层/数据字典 一、数据流图 ( DFD ) 简介 二、数据流图 ( DFD ) 概念符号 1、数据流 2、加工 ( 核心 ) 3、数据存储 4、外部实体 三、数据流图 ( DFD ) 分层 1、…

扫雷(超详解+全部码源)

C语言经典游戏扫雷 前言一.游戏规则二.所需文件三.创建菜单四.游戏核心内容实现1.创建棋盘2.打印棋盘3.布置雷4.排查雷5.game()函数具体实现 五.游戏运行实操六.全部码源 前言 &#x1f600;C语言实现扫雷是对基础代码能力的考察。通过本篇文章你将学会如何制作出扫雷&#xff…

一般透视投影VS正交投影VS弱透视投影

一般透视投影&#xff1a; 正交投影 (Orthographic Projection) 正交投影 (Orthographic Projection) 是一种将三维物体沿着垂直于成像平面的方向投影到成像平面上的方法&#xff0c;它保持了三维空间中的平行关系和角度&#xff0c;但是失去了深度信息和透视效果。正交投影可…

软件测试计划模板的编写

1 概述 在整个系统测试阶段,相关的系统测试工作的开展需要进行各方面的明确,在系统测试计划中主要是针对系统测试阶段各个不同岗位所担负的相关职责,防范由于职责不清所造成的系统测试工作的混乱现象.明确定义相关的系统测试范围,防止由于测试分工而造成的遗测.在该计划中一定…

gitee linux免密/SSH 方式连接免登录

目录 生成SSH公钥通过 ssh-keygen 程序创建找到SSH公钥 在gitee中添加公钥 生成SSH公钥 通过 ssh-keygen 程序创建 shell> ssh-keygen -t rsa -C "xxxxxx.com" Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rs…

《网络是怎样连接的》(三)

《网络是怎样连接的》&#xff08;二.2&#xff09;_qq_38480311的博客-CSDN博客 本文主要取材于 《网络是怎样连接的》 第三章。 简述&#xff1a;本文主要内容是解释 通过网线传输出去的包是如何经过集线器、交换机和路由器等网络设备&#xff0c;最终进入互联网的。 信号…

AI量化模型预测挑战赛 第二次学习笔记

有关竞赛信息以及基础baseline代码解读请看我的上一篇文章 AI量化模型预测——baseline学习笔记_寂ღ᭄秋࿐的博客-CSDN博客 在经过baseline进行详细的分析之后&#xff0c;接下来的方向肯定是奔着提分去的&#xff0c;下面我就从五个方面进行一一列出提分思路 提取更多的特征…