Java多线程(二)线程安全

news2025/2/27 4:01:03

线程安全

线程安全(风险)

线程不安全的原因:

解决线程不安全:

synchronized

内存刷新

可重入

volatile 关键字

wait 和 notify

wait()

notify ()

wait与sleep的区别:


线程安全(风险)

某个代码在多线程的环境下执行,然后出现bug,其本质原因在于线程调度是不确定的。

比如:(代码有问题)

public class test3 {
     static int count=0;
    public   static void sum(){
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        long time= System.nanoTime();
        Thread t1=new Thread(()->{
                for (int i = 0; i < 10000; i++) {
                    sum();
                }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                sum();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);

}

我们会发现这个代码出现了一个问题,与我们想要的预期不一致。即出现bug

其本质是,count++操作,本质是有三个CPU指令构成

1.load,把内存中的数据读到cpu寄存器中。

2.add,就是把寄存器中的值,进行+1操作

3.save,把寄存器中值写回内存中。

大家肯定学过数学,那么对于组合,肯定是有过了解的。那么我问个问题,现在线程有两个,分别对count进行++操作。对于cpu来说有几种组合方式?3*3共有9种,那么问题来了,我们只要唯一的结果,不需要这么结果可能。

这个是我随便选择的两种情况,画出来的。箭头向下,表示时间的执行顺序。可以看到,第一个,t1的load执行后,t2的load开始执行了,然后执行t2的add操作,和save操作,再然后才执行t1的add操作和save操作。所以出现了不同的结果。

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 "工作内存" (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

 其实在进一步理解,可以理解为,两个线程对同一个变量,进行了相互作用。

线程不安全的原因:

1.抢占式执行(大部分的原因)

2.多个线程修改同一个变量(不安全)(而有几种情况是安全:

    一个线程改同一个变量(安全),多个线程读同一个变量(安全),多个线程修改不同变量)

3.修改操作,不是原子性的。

4.内存可见性,引起的线程不安全。

5.指令重排序,引起的线程不安全。

那么如何是的让++操作不会被干扰呢?

解决线程不安全:

主要思路是:诺是我们有一种操作,将++操作封装起来,让t1的++操作结束,再让t2的++操作开始。而这种操作就是加锁操作。

简单理解就是,目前有一个厕所,但是有很多要上厕所,怎么办,抢呗,总不可能等着膀胱爆炸吧,等待厕所里的人出来,并且打开厕所门,然后一群人看谁快,谁快谁就先如厕。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的

synchronized

java中提供了一个关键字:synchronized,监视器锁monitor lock

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

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

synchronized用的锁是存在Java对象头里的,底层是使用操作系统的mutex lock实现的.

内存刷新

工作流程:

synchronized 的工作过程:

1. 获得互斥锁

2. 从主内存拷贝变量的最新副本到工作的内存

3. 执行代码

4. 将更改后的共享变量的值刷新到主内存

5. 释放互斥锁

可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题

这个问题呢,在synchronized中不会出现。而可重入其实就是,有人厕所上完了,但是他很缺德,将门锁了,然后呢,他突然发现他包忘哪了,回去后他在厕所门口等着,结果等了半天,里面的人死活不出来。但是厕所里没人。

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}
  • increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)

可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息:

  1. 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增。
  2. 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)。

synchronized 使用:

  • 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
    public synchronized void methond() {
   }
}
  • 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
    public synchronized static void method() {
   }
}
  • 修饰代码块: 明确指定锁哪个对象.

(锁当前对象)

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

(锁类对象 )

public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

两个线程竞争同一把锁, 才会产生阻塞等待

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

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

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

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

volatile 关键字

volatile 能保证内存可见性

1.代码在写入 volatile 修饰的变量的时候,

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

2.代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

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) {
            // 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();
}

根据上面的代码,发现问题没有,无论我们在控制台中输入什么值,程序都不会结束。为什么呢,就像上面所说的那样,一个cup的寄存器中数据并没进行更新。另一个线程所拿到的数据没有进行跟换。其主要原因是计算机运算速度太快了。寄存器和缓存的速度都太快了,

使用特点:

1.volatile 不保证原子性

2.volatile 适用于一个线程读,一个线程写

3.synchronized 既能保证原子性, 也能保证内存可见性.

改正后:

static class Counter {
  volatile  public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // 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();
}

此时就可以结束进程了。

wait notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

完成这个协调工作, 主要涉及到三个方法

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

wait 做的事情:

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

wait 结束等待的条件:

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

notify 方法是唤醒等待的线程.

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
  2. 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  3. 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
  4. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
  5. 完,也就是退出同步代码块之后才会释放对象锁。

wait和notify的使用(只能一个结束,一个开始)

static class WaitTask implements Runnable {
    private Object locker;
    public WaitTask(Object locker) {
        this.locker = locker;
   }
    @Override
    public void run() {
        synchronized (locker) {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }
}
static class NotifyTask implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
   }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
       }
   }
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    Thread.sleep(1000);
    t2.start();
}

notify方法只是唤醒某一个等待线程.。使用notifyAll方法可以一次唤醒所有的等待线程。虽然是同时唤醒多个线程, 但是这些线程需要竞争锁。 所以并不是同时执行, 而仍然是有先有后的执行。

wait与sleep的区别:

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

1. wait 需要搭配 synchronized 使用.,sleep 不需要。

2. wait 是 Object 的方法 sleep 是 Thread 的静态方法。

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

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

相关文章

C# OpenVino Yolov8 Seg 分割

效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using OpenCvSharp;namespace OpenVino_Yolov8_Demo {public…

30天入门Python(基础篇)——第2天:Python安装(保姆级)与IDE的认识与选择+详细安装教程

文章目录 专栏导读上一节课回顾1、Python解释器的安装查看各个版本的Python解释器①、ok,双击安装②、这里我们选择【自定义】安装&#xff0c; 下面的【将Python添加在环境变量】大家一定要打个勾③、点击【Next】进行下一步④、这里不建议安装在C盘, 点击【Browse】我在F盘创…

『SpringBoot 源码分析』run() 方法执行流程:(3)刷新应用上下文-处理 @ComponentScan 注解

『SpringBoot 源码分析』run() 方法执行流程&#xff1a;&#xff08;3&#xff09;刷新应用上下文-处理 ComponentScan 注解 基于 2.2.9.RELEASE问题&#xff1a;当方法进行了注释标记之后&#xff0c;springboot 又是怎么注入到容器中并创建类呢&#xff1f; 首先创建测试主…

linux--进程--system与popen函数

1.system #include <stdlib.h>int system(const char *command); 返回值&#xff1a; 成功&#xff0c;则返回进程的状态值&#xff1b;不能源码execl函数&#xff0c;返回127&#xff1b;失败返回-1&#xff1b; 不能成功运行分析文章&#xff1a;linux下system函数详…

Java网络编程(二)Socket 套接字(TCP和UDP),以及TCP的回显

Socket 套接字 我们软件工作者&#xff0c;着重编写的是应用层的代码&#xff0c;但是发送这个数据&#xff0c;我们就需要将应用层传输到传输层&#xff0c;也就意味着我们需要调用应用层的API&#xff0c;统称为 Socket API。 套接字的分类&#xff1a; 流套接字&#xff…

2023数学建模国赛B题完整论文来啦!(含一二问求解代码及三四问仿真模拟代码)

大家好呀&#xff0c;从昨天发布赛题一直到现在&#xff0c;总算完成了全国大学生数学建模竞赛B题完整的成品论文。 本论文可以保证原创&#xff0c;保证高质量。绝不是随便引用一大堆模型和代码复制粘贴进来完全没有应用糊弄人的垃圾半成品论文。 说实话团队通宵一直到现在做…

汇川PLC学习Day1:跑马灯程序编写

汇川PLC学习Day1&#xff1a;跑马灯程序编写 一、 软件安装 进入官网下载软件 二、 使用帮助 三、 新建工程与功能代码实现 CtrlN 寻找内带输出模块的CPU并设置好工程名字与保存路径&#xff0c;语言选择想熟悉的类型 工程建立后&#xff0c;PLC_PRG即为用户编写程序文件…

【疑难杂症】解决 git 文件夹不显示绿色图标和红色图标的问题

目录 一、问题描述 二、问题解决前提 【2.1】首先保证电脑本机上有TortoiseGit这个软件 【2.2】TortoiseGit下载官网 【2.3】根据自己电脑位数进行下载&#xff0c;这里下载的是64位 【2.4】下载好之后&#xff0c;一路next进行安装&#xff0c;配置自己的邮箱和用户名 …

uni-app:重置表单数据

效果 代码 <template><form><input type"text" v-model"inputValue" placeholder"请输入信息"/><input type"text" v-model"inputValue1" placeholder"请输入信息"/><input type&quo…

用Python登录账户

1 问题 如何利用python登录账户&#xff1f; 2 方法 账户和密码存放在文件夹中从文件夹中读取并比较密码密文验证三次后&#xff0c;如不成功则锁定用户 通过。。。。。。。。等证明提出的方法是有效的&#xff0c;能够解决开头提出的问题。 代码清单 1 import osimport getpas…

FGO:使用chaIdea获取抽卡数据(mitmproxy抓包)

需求描述 最近逛贴吧看到好多master贴出自己的抽卡概率截图&#xff0c;本非洲杂鱼master也对自己的脸黑程度产生了好奇&#xff08;曾经15单芭娜娜池子1五星&#xff0c;6单道满池子1五星&#xff0c;梅莉池子330抽1五星&#xff0c;最近的芭娜娜复刻又330抽1五星&#xff09…

流程图用什么软件做比较好?这几个实用软件了解下

流程图用什么软件做比较好&#xff1f;流程图的制作对于企业管理、项目管理、产品设计等领域都非常重要。制作流程图可以帮助我们更好地理解事物之间的关系和流程&#xff0c;从而规划和组织工作。因此&#xff0c;选择一个合适的流程图软件是非常必要的。下面就给大家介绍几种…

C++中使用R“()“标记符书写多行字符串

在C#中使用表示的字符串能够跨越数行。用于在C#中写JS或SQL代码比较方便。 string sqlInsert "INSERT INTO tb_param(protocol, slave, number, ptype, pid, name, format) VALUES(2, 24, 0, 1, 1, a04005, .3);INSERT INTO tb_param(protocol, slave, number, ptype, …

day52(补)

300.最长递增子序列 力扣题目链接(opens new window) 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2…

ZFS了解

存储数据的管理通常涉及两个方面:对一个或多个块存储设备(如硬盘驱动器和SD卡)进行物理卷管理&#xff0c;并将它们组织成操作系统所看到的逻辑块设备(通常涉及卷管理器、RAID控制器、阵列管理器或合适的设备驱动程序)&#xff0c;以及对存储在这些逻辑块设备(文件系统或其他数…

使用 System.exit() 来优雅地终止 Spring Boot 项目

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; 使用 System.exit() 来优雅地终止 Spring Boot 项目 ⏱️ 创作时间&am…

智安网络|面临日益增长的安全威胁:云安全和零信任架构的重要性

随着云计算技术的快速发展和广泛应用&#xff0c;云安全和零信任架构变得愈发重要。在数字化时代&#xff0c;云计算技术得到了广泛的应用和推广。企业和组织借助云服务提供商的强大能力&#xff0c;实现了高效、灵活和可扩展的IT基础设施。然而&#xff0c;随着云环境的快速发…

java设计模式,简单工厂和抽象工厂有什么区别?

java设计模式&#xff0c;简单工厂和抽象工厂有什么区别&#xff1f; 简单工厂模式&#xff1a; 这个模式本身很简单而且使用在业务较简单的情况下。一般用于小项目或者具体产品很少扩展的情况&#xff08;这样工厂类才不用经常更改&#xff09;。 它由三种角色组成&#xf…

数据可视化:四大发明的现代转化引擎

在科技和工业的蓬勃发展中&#xff0c;中国的四大发明——造纸术、印刷术、火药和指南针&#xff0c;早已不再是古代创新的象征&#xff0c;而是催生了众多衍生行业的崭新可能性。其中&#xff0c;数据可视化技术正成为这些行业的一颗璀璨明珠&#xff0c;开启了全新的时代。 1…

OpenCV 02(色彩空间)

一、OpenCV的色彩空间 1.1 RGB和BGR 最常见的色彩空间就是RGB, 人眼也是基于RGB的色彩空间去分辨颜色的. OpenCV默认使用的是BGR. BGR和RGB色彩空间的区别在于图片在色彩通道上的排列顺序不同. 显示图片的时候需要注意适配图片的色彩空间和显示环境的色彩空间.比如传入的图片…