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 {
    @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()方法进行加锁,当一个线程进入时,就会进行加锁,其他线程只能等待该线程释放。


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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构刷题训练:用栈实现队列(力扣OJ)

目录 前言 1. 题目&#xff1a;用栈实现队列 2. 思路 3. 分析 3.1 定义 “ 队列 ” 3.2 创建队列 3.3 入队 3.4 队头数据 3.5 出队 3.6 判空和销毁 4.题解 总结 前言 栈和队列是数据结构中的两个重要概念&#xff0c;它们在算法和程序设计中都有着广泛的应用。本文将带你深入了…

4.时间与窗口

4.1 时间类型 在Flink中定义了3种时间类型&#xff1a; 事件时间&#xff08;Event Time&#xff09;:事件的发生事件&#xff0c;数据本身自带时间字段。处理时间&#xff08;Processing Time&#xff09;&#xff1a;计算引擎处理时的系统时间。和摄取时间&#xff08;Inge…

(el-Form)操作(不使用 ts):Element-plus 中 Form 表单组件校验规则等的使用

Ⅰ、Element-plus 提供的 Form 表单组件与想要目标情况的对比&#xff1a; 1、Element-plus 提供 Form 表单组件情况&#xff1a; 其一、Element-plus 自提供的 Form 代码情况为(示例的代码)&#xff1a; // Element-plus 自提供的代码&#xff1a; // 此时是使用了 ts 语言环…

ELK中grok插件、mutate插件、multiline插件、date插件的相关配置

目录 一、grok 正则捕获插件 自定义表达式调用 二、mutate 数据修改插件 示例&#xff1a; ●将字段old_field重命名为new_field ●添加字段 ●将字段删除 ●将filedName1字段数据类型转换成string类型&#xff0c;filedName2字段数据类型转换成float类型 ●将filedNam…

【移动机器人运动规划】04 ——轨迹生成

文章目录 前言相关代码整理: 介绍Minimum Snap OptimizationDifferential Flatness(微分平坦)Minimum-snapSmooth 1D TrajectorySmooth Multi-Segment TrajectoryOptimization-based Trajectory Generation Convex Optimization&#xff08;凸优化&#xff09;凸函数和凸集凸优…

List list=new ArrayList()抛出的ArrayIndexOutOfBoundsException异常

1.应用场景&#xff0c;今天生产日志监控到一组new ArrayList() 进行add 异常&#xff0c;具体日志如下&#xff1a; eptionHandler.handler(178): TXXYBUSSINESS|执行异常 java.util.concurrent.CompletionException: java.lang.ArrayIndexOutOfBoundsException: Index 1 out…

SpringBoot禁用Swagger3

Swagger3默认是启用的&#xff0c;即引入包就启用。 <dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version> </dependency> <dependency><groupId…

理解-面向对象

目录 对象&#xff1a; 举例&#xff1a; 封装: 好处: 继承: 多态&#xff1a; 类和对象之间的关系 对象&#xff1a; 把一个东西看成对象&#xff0c;我们就可以孤立的审查它的性质&#xff0c;行为&#xff0c;进而研究它和其他对象的关系。 对象是一个应用系统中用…

Spring5 AOP 默认使用 JDK

这是博主在使用dubbo实现远程过程调用的时候遇到的问题&#xff1a; 我们如果在服务提供者类上加入Transactional事务控制注解后&#xff0c;服务就发布不成功了。原因是事务控制的底层原理是为服务提供者类创建代理对象&#xff0c;而默认情况下Spring是基于JDK动态代理方式创…

ssh-keygen 做好免密登录后不生效

免密说明 通常情况下&#xff0c;我们ssh到其他服务器需要知道服务器的用户名和密码。对于需要经常登录的服务器每次都输入密码比较麻烦&#xff0c;因此我们可以在两台服务器上做免密登录&#xff0c;即在A服务器可以免密登录B服务器。 在A服务器上登录B服务器时&#xff0c;…

数字图像处理 --- 相机的内参与外参(CV学习笔记)

Pinhole Camera Model&#xff08;针孔相机模型&#xff09; 针孔相机是一种没有镜头、只有一个小光圈的简单相机。 光线穿过光圈并在相机的另一侧呈现倒立的图像。为了建模方便&#xff0c;我们可以把物理成像平面(image plane)上的图像移到实际场景(3D object)和焦点(focal p…

Spring-2-透彻理解Spring 注解方式创建Bean--IOC

今日目标 学习使用XML配置第三方Bean 掌握纯注解开发定义Bean对象 掌握纯注解开发IOC模式 1. 第三方资源配置管理 说明&#xff1a;以管理DataSource连接池对象为例讲解第三方资源配置管理 1.1 XML管理Druid连接池(第三方Bean)对象【重点】 数据库准备 -- 创建数据库 create …

Python基础小项目

今天给大家写一期特别基础的Python小项目&#xff0c;欢迎大家支持&#xff0c;并给出自己的完善修改 &#xff08;因为我写的都是很基础的&#xff0c;运行速率不是很好的 目录 1. 地铁票价题目程序源码运行截图 2. 购物车题目程序源码运行截图 3. 名片管理器题目程序源码运行…

opencv实战项目 实现手势跟踪并返回位置信息(封装调用)

OpenCV 是一个基于 Apache2.0 许可&#xff08;开源&#xff09;发行的跨平台计算机视觉和机器学习软件库&#xff0c;可以运行在Linux、Windows、Android和Mac OS操作系统上。 需要提前准备opencv 和 mediapipe库 pip --default-timeout5000 install -i https://pypi.tuna.tsi…

nodejs+vue+elementui社区流浪猫狗救助救援网站_4a4i2

基于此背景&#xff0c;本研究结合管理员即时发布流浪猫狗救助救援信息与用户的需求&#xff0c;设计并实现了流浪猫狗救助救援网站。系统采用B/S架构&#xff0c;java语言作为主要开发语言&#xff0c;MySQL技术创建和管理数据库。系统主要分为管理员和用户两大功能模块。通过…

【Linux取经路】进程的奥秘

文章目录 1、什么是进程&#xff1f;1.1 自己写一个进程 2、操作系统如何管理进程&#xff1f;2.1 描述进程-PCB2.2 组织进程2.3 深入理解进程 3、Linux环境下的进程3.1 task_struct3.2 task_struct内容分类3.3 组织进程3.4 查看进程属性 4、结语 1、什么是进程&#xff1f; 在…

FreeRTOS

FreeRTOS官网&#xff1a;www.freertos.org 调度 实时操作系统 特点&#xff1a;如果有一个任务需要执行&#xff0c;实时操作系统会马上&#xff08;在较短时间内&#xff09;执行该任务&#xff0c;不会有较长的延时。这种特性保证了各个任务的及时执行。 实现方式&#…

web前端之CSS操作

文章目录 一、CSS操作1.1 html元素的style属性1.2 元素节点的style属性1.3 cssText属性 二、事件2.1 事件处理程序2.1.1 html事件2.1.2 DOM0事件&#xff08;适合单个事件&#xff09;2.1.3 DOM2事件&#xff08;适合多个事件&#xff09; 2.2 事件之鼠标事件2.3 事件之Event事…

详解Kafka分区机制原理|Kafka 系列 二

Kafka 系列第二篇&#xff0c;详解分区机制原理。为了不错过更新&#xff0c;请大家将本号“设为星标”。 点击上方“后端开发技术”&#xff0c;选择“设为星标” &#xff0c;优质资源及时送达 上一篇文章介绍了 Kafka 的基本概念和术语&#xff0c;里面有个概念是 分区(Part…

计算机竞赛 opencv python 深度学习垃圾图像分类系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; opencv python 深度学习垃圾分类系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;4分 这是一个较为新颖的竞…