【多线程综合】java何时考虑线程安全问题、怎么考虑、又怎么解决?

news2024/11/15 19:33:27

前言:在编程中,线程安全是一个非常重要的概念。它涉及到多个线程并发访问共享资源时的正确性和一致性。在Java中,为了确保线程安全,我们需要考虑一些关键因素。

1、什么是线程安全

线程安全是指当多个线程同时访问一个对象时,不会发生任何问题或者数据不一致的情况。这意味着每个线程都能正确地执行,并且最终得到正确的结果。现代计算机系统都是多核的,意味着多线程已经成为一种非常流行的编程方式。对于一个多线程程序来讲,线程安全是至关重要的,如果不考虑线程安全问题,可能会导致程序在某些情况下产生不可预测的、混乱的行为。

2.何时考虑线程安全

在Java中,我们需要考虑线程安全的情况有很多。下面是一些常见的场景:

1、共享资源:当多个线程同时访问共享的数据或对象时,需要考虑线程安全。例如,多个线程同时访问同一个变量或方法时。
2、并发访问集合类:Java提供了许多集合类,如ArrayList、HashMap等。当多个线程同时访问这些集合类时,需要考虑线程安全。否则可能会导致数据不一致或者异常。
3、多线程环境下的单例模式:在多线程环境下使用单例模式时,需要考虑线程安全。否则可能会创建多个实例,违反了单例模式的原则。

3、线程安全的实现方法*

线程安全的实现方法有以下几种:

2.1同步代码块

同步代码块是采用synchronized关键字来实现方法级别的同步。Java语言中,synchronized方法可以保证在同一时刻只有一个线程可以访问该方法,而同步代码块则可以保证在同一时刻只有一个线程可以访问该代码块,从而保证线程安全。

synchronized(共享资源对象){//对共享资源对象加锁
//代码(原子操作)
}
下面的共享资源是arr数组,{}大括号中是具体的操作。

注:
每个对象都有一个互斥锁标记,用来分配给线程的。
只有拥有对象互斥锁标记的线程,才可以进入该对象加锁的同步代码块。
线程退出同步代码块时,会释放相应的互斥锁标记。

例子:

package com.atguigu.gulimall.providerconsumer.util;

import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;

/**
 * @author: jd
 * @create: 2024-07-16
 */
@Slf4j
public class MultiThreadTest {
    //初始数组下标值  全局变量
    private static int INDEX = 0;

public static void main(String[] args) throws InterruptedException {
//    synchronized   同步代码块

    //定义一个长度为5的数组
    String[] arr = new String[5];
    //定义两个线程,分别往数组中插入不同的值。
    //这里我们期望可以得到["ONE","TWO",null,null,null]  ,而不是得到["ONE",null,null,null,null] 或者["TWO",null,null,null,null]
    //创建线程一(以匿名内部类实现线程创建)
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            //加锁的代码块,同一个时间只允许一个线程访问这个arr对象,并对其操作
            synchronized (arr){
                arr[INDEX] ="ONE";
                INDEX++;
            }
        }
    });

    //创建线程二(以匿名内部类+lamda表达式实现线程创建)
    Thread thread2 = new Thread(() -> {
        //加锁的代码块,同一个时间只允许一个线程访问这个arr对象,并对其操作
        synchronized (arr){
            arr[INDEX] ="TWO";
            INDEX++;
        }
    });
    //启动线程
    thread1.start();
    thread2.start();
    //这里为了保证输出时这两个线程已经执行完毕,使用join方法来阻塞主线程
    thread1.join();
    thread2.join();
//打印数据数据
    System.out.println(Arrays.toString(arr));
    
}

}

测试结果:
这时的结果无论执行多少次都不会出现元素覆盖的情况,结果为[ONE, TWO, null, null, null]或[TWO, ONE, null, null, null]。因为没有去控制线程执行顺序。
在这里插入图片描述
在这里插入图片描述
只有这两种结果,
如果我们把同步代码块给去掉的话,会出现,四个null的情况,但是概率挺低的。我见过一次,但是没截取下来 _

补充lamda表达式简化子线程的创建方式

  Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            // 这里是线程应该执行的代码
            System.out.println("Hello, World!");
        }
    });

    //上面代码t1的简化写法如下  见thread2创建
    Thread thread2 = new Thread(() -> {
        System.out.println("Hello, World!");
    });

    
2.2同步方法

同步方法是一种比同步代码块更加简单和方便的实现方式,通过给方法加上synchronized关键字,可以保证方法在被多个线程同时调用时,只有一个线程可以访问该方法,其它线程在等待该方法执行完毕后才能继续访问。

synchronized 返回值类型 方法名称(形参列表){//对当前对象this加锁
//代码(原子操作)
}

注:
只有拥有该对象互斥锁标记的线程才能进入该对象加锁的同步方法中。
线程退出同步方法时,会释放拥有的互斥锁标记。

例子:

package com.atguigu.gulimall.providerconsumer.util;

import lombok.extern.slf4j.Slf4j;

/**
 * @author: jd
 * @create: 2024-07-16
 */
@Slf4j
public class MultiThreadTest2 {

    //总票数
    private static int TICKET =100;

    public static void main(String[] args) {
        //定义售票窗口 t1,执行售票方法
        Thread t1 = new Thread(() -> {
            while (true) {
                if (!sale()) {
                    break;
                }
            }
        }, "t1");

        //定义售票窗口t2,执行售票方法
        Thread t2 = new Thread(() -> {
            while (true){
                if(!sale()){
                    break;
                }
            }
        }, "t2");

        //定义售票窗口t3,执行售票方法
        Thread t3 = new Thread(() -> {
            while (true) {
                if (!sale()) {
                    break;
                }
            }
        }, "t3");

        t1.start();
        t2.start();
        t3.start();

    }

    /**
     * 静态方法,锁的是当前方法所在的类
     * 非静态方法,锁的是this,调用该方法的对象。
     * @return
     */
    private synchronized static boolean sale() {
        if (TICKET <= 0) {
            return false;
        }
        System.out.println(Thread.currentThread().getName() + "销售了第" + TICKET + "张票");
        TICKET--;
        return true;
    }
}

现象:

t1销售了第100张票
t1销售了第99张票
t1销售了第98张票
t1销售了第97张票
t1销售了第96张票
t1销售了第95张票
t1销售了第94张票
t1销售了第93张票
t1销售了第92张票
t1销售了第91张票
t1销售了第90张票
t1销售了第89张票
t1销售了第88张票
t1销售了第87张票
t1销售了第86张票
t1销售了第85张票
t1销售了第84张票
t1销售了第83张票
t1销售了第82张票
t1销售了第81张票
t1销售了第80张票
t1销售了第79张票
t1销售了第78张票
t1销售了第77张票
t1销售了第76张票
t1销售了第75张票
t3销售了第74张票
t3销售了第73张票
t3销售了第72张票
t3销售了第71张票
t3销售了第70张票
t3销售了第69张票
t3销售了第68张票
t3销售了第67张票
t3销售了第66张票
t3销售了第65张票
t3销售了第64张票
t3销售了第63张票
t3销售了第62张票
t3销售了第61张票
t3销售了第60张票
t3销售了第59张票
t3销售了第58张票
t3销售了第57张票
t3销售了第56张票
t3销售了第55张票
t3销售了第54张票
t3销售了第53张票
t3销售了第52张票
t3销售了第51张票
t3销售了第50张票
t3销售了第49张票
t3销售了第48张票
t3销售了第47张票
t3销售了第46张票
t3销售了第45张票
t3销售了第44张票
t3销售了第43张票
t3销售了第42张票
t3销售了第41张票
t3销售了第40张票
t3销售了第39张票
t3销售了第38张票
t3销售了第37张票
t3销售了第36张票
t3销售了第35张票
t3销售了第34张票
t3销售了第33张票
t3销售了第32张票
t3销售了第31张票
t3销售了第30张票
t3销售了第29张票
t3销售了第28张票
t3销售了第27张票
t3销售了第26张票
t3销售了第25张票
t3销售了第24张票
t3销售了第23张票
t3销售了第22张票
t2销售了第21张票
t2销售了第20张票
t2销售了第19张票
t2销售了第18张票
t2销售了第17张票
t2销售了第16张票
t2销售了第15张票
t2销售了第14张票
t2销售了第13张票
t2销售了第12张票
t2销售了第11张票
t2销售了第10张票
t2销售了第9张票
t2销售了第8张票
t2销售了第7张票
t2销售了第6张票
t2销售了第5张票
t2销售了第4张票
t2销售了第3张票
t2销售了第2张票
t2销售了第1张票

Process finished with exit code 0

可以看到上面的是没有重复的;可以证明线程安全。

2.3volatile关键字

volatile关键字是一种比较特殊的同步实现方式,它可以用来修饰一个对象或者变量,使得其在多线程环境下的操作具有可见性。也就是说,通过使用volatile关键词修饰的变量,当一个线程修改了该变量的值,另一个线程就可以立刻看到这个变量的最新值,从而避免了多线程环境下没同步带来的数据不一致的问题。

2.3.1特点:

保证可见性:一个线程对Volatile变量的修改立即对其他线程可见。
不保证原子性:Volatile关键字不能替代Synchronized关键字,不能确保复合操作的原子性。
与其他同步机制的区别:相较于锁机制,Volatile具有轻量级和更好的性能。

2.3.2用途:

**标记共享变量:**当多个线程需要访问同一个变量,并且至少有一个线程会修改这个变量的值时,这个变量应该被声明为 volatile。这样可以确保当一个线程修改了变量的值后,其他线程能够立即看到这个修改。
**避免指令重排序:**在 Java 中,编译器和处理器可能会对代码进行指令重排序以提高性能。然而,在某些情况下,指令重排序可能会导致程序出现意外的行为,尤其是在多线程环境下。将变量声明为 volatile 可以防止编译器对包含该变量的代码进行指令重排序。
**作为同步的轻量级替代:**在某些情况下,如果不需要使用 synchronized 关键字带来的重量级锁(因为 synchronized 可能会导致线程阻塞和上下文切换),volatile 可以作为一个轻量级的替代方案。但是,需要注意的是,volatile 不能替代 synchronized 在所有场景下的使用,特别是当需要保证多个操作的原子性时。

2.3.3 例子:

假设有一个布尔变量 isRunning,用于控制某个线程的运行状态:

public class WorkerThread extends Thread {  
    private volatile boolean isRunning = true;  
  
    public void run() {  
        while (isRunning) {  
            // 执行任务  
        }  
    }  
  
    public void stopRunning() {  
        isRunning = false;  
    }  
}

在这个例子中,isRunning 被声明为 volatile,以确保当 stopRunning() 方法被调用时,isRunning 的新值能够立即被 run() 方法中的循环看到,从而正确地停止线程的执行。如果没有将 isRunning 声明为 volatile,那么由于编译器优化或缓存的影响,run() 方法中的循环可能无法及时看到 isRunning 的变化,导致线程无法停止。
注意事项
volatile 变量只能保证变量的可见性,但不能保证操作的原子性。例如,对于 volatile int count = 0;,count++ 操作就不是原子的,因为它包含了读取、修改和写入三个步骤。
在使用 volatile 时,需要仔细考虑其使用场景,避免错误地将其作为解决所有并发问题的万能药。

2.4线程安全集合

Java中提供了一些线程安全的集合类型,如ConcurrentHashMap和ConcurrentLinkedQueue等,这些集合在多线程并发访问时会自动对访问进行加锁,从而保证了线程安全。

4. 线程安全面临的挑战

在多线程编程中,线程安全不是一件容易的事情。多线程程序中需要解决以下挑战:

3.1 竞态条件

当多个线程试图同时访问和修改共享数据时,就可能会造成竞态条件(race condition)的问题,导致程序出现死锁、死循环、数据不一致等异常。为了避免出现竞态条件,需要采用一些同步技术来保证线程安全。

3.2 死锁

死锁是指两个或多个线程因为互相等待对方释放锁而陷入无限等待的状态。由于线程的执行顺序和时间是不确定的,因此在多线程应用程序中,要避免任何可能导致死锁的情况。

3.3 内存可见性

多个线程并发读写共享变量时,可能会由于线程之间的不同步而导致内存不可见性问题,即一个线程所做的修改对另外一个线程是不可见的。为了解决内存可见性问题需要采用一些同步技术(如synchronized关键字和volatile关键字)来保证线程安全。

5. 线程安全的应用场景

在实际开发中,需要注意以下几个方面来保证线程安全:

4.1 多个线程访问同一个对象时需要保证其线程安全。

如果多个线程需要访问同一个对象,那么就需要采用同步机制来保证该对象的线程安全。

4.2 多个线程访问多个对象时也需要保证线程安全。

虽然多个线程访问多个对象时不会产生死锁等问题,但是由于多个线程同时访问多个对象也可能会引发各种线程安全问题(如竞争条件、内存可见性问题等),因此对于多线程访问多个对象的场景同样需要注意线程安全问题。

4.3 线程池同样需要考虑线程安全。

在使用线程池时,需要注意要保证线程池本身的线程安全,同时要保证任务的线程安全。

4.4 对象的状态也需要考虑线程安全。

在编写多线程程序时,还需要注意对象状态的线程安全问题。具体来讲,就是需要考虑对象的状态可变性和不变性。对于可变对象,在多线程访问时需要进行同步处理;对于不可变对象则不存在线程安全问题。

线程安全在多线程编程中起着不可替代的重要作用。保证多线程程序的线程安全既需要采用相应的同步机制,也需要了解多线程程序面临的挑战及应用场景。同时,在软件开发中,也需要深入了解线程安全技术的实现原理,来避免出现因为线程安全而影响开发效率的问题。

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

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

相关文章

十大排序 之 快速排序

&#xff01;&#xff01;&#xff01;排序仅针对于数组哦本次排序是按照升序来的哦代码后边有图解哦 介绍 快速排序英文名为Quick Sort 基本思路 快速排序采用的是分治思想&#xff0c;即在一个无序的序列中选取一个任意的基准元素base&#xff0c;利用base将待排序的序列分…

基于RK3588的8K视频解码显示案例分享!引领超高清工业视频时代

8K、4K、2K显示对比 2K分辨率&#xff1a;也称为全高清&#xff08;FULL HD&#xff09;&#xff0c;它具有1920 x 1080像素的分辨率。这是目前大多数消费者电视和电脑显示器的标准分辨率&#xff0c;可以提供良好的图像质量。 4K分辨率&#xff1a;也称为4K超高清&#xff0…

我无法给博客园出钱,那我就出点建议吧

相信这张图大家都已经看见过了&#xff0c;从去年就传出博客园经营困难的情况&#xff0c;其实很多平台&#xff0c;不止是博客园&#xff0c;包括现在国内的很多公司都一样&#xff0c;经营是一件大难题&#xff0c;但很多公司我们不知道&#xff0c;悄无声息的倒下了。而博客…

【雅思备考IELTS】写作第一部分Writing Part One

Tips for IELTS Writing (Part 1) By James Lee 2024/7/15 Part 1: 图表数据分析 Analysis of a Graph / Chart / Curve, etc. 这部分一般是让分析一张图表&#xff08;Graph或Chart&#xff09;&#xff0c;用时约20分钟&#xff0c;字数不用太多&#xff0c;150词以上即可。…

推荐一个可以体验正版ChatGPT的平台

在鱼龙混杂的API市场&#xff0c;智创聚合API以其卓越的性能和创新的服务理念&#xff0c;为用户带来了前所未有的体验。我们自豪地宣布&#xff0c;现在加入我们的限时官方API渠道&#xff0c;您将享受到更快速率提升&#xff0c;以及更高质量的回复服务&#xff0c;而这些仅需…

Python酷库之旅-第三方库Pandas(028)

目录 一、用法精讲 71、pandas.tseries.api.guess_datetime_format函数 71-1、语法 71-2、参数 71-3、功能 71-4、返回值 71-5、说明 71-6、用法 71-6-1、数据准备 71-6-2、代码示例 71-6-3、结果输出 72、pandas.util.hash_array函数 72-1、语法 72-2、参数 72…

【PostgreSQL】PostgreSQL简史

博主介绍&#xff1a;✌全网粉丝20W&#xff0c;CSDN博客专家、Java领域优质创作者&#xff0c;掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围&#xff1a;SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…

Java SHA-256哈希算法

一、SHA-256算法简介 SHA-2&#xff08;Secure Hash Algorithm 2&#xff09;&#xff0c;一种散列函数算法标准&#xff0c;由美国国家安全局研发&#xff0c;由美国国家标准与技术研究院&#xff08;NIST&#xff09;在2001年发布&#xff0c;属于SHA算法之一&#xff0c;是…

C++ std::lock_guard和 std::unique_lock

二者都是 C 标准库中用于管理互斥锁&#xff08;mutex&#xff09;的 RAII&#xff08;Resource Acquisition Is Initialization&#xff09;机制的类。这些类可以确保互斥锁在构造时被获取&#xff0c;在析构时被释放&#xff0c;从而避免死锁和资源泄漏问题。不过&#xff0c…

使用windows批量解压和布局ImageNet ISLVRC2012数据集

使用的系统是windows&#xff0c;找到的解压命令很多都linux系统中的&#xff0c;为了能在windows系统下使用&#xff0c;因此下载Git这个软件&#xff0c;在其中的Git Bash中使用以下命令&#xff0c;因为Git Bash集成了很多linux的命令&#xff0c;方便我们的使用。 ImageNe…

【博士每天一篇文献-算法】 PNN网络启发的神经网络结构搜索算法Progressive neural architecture search

阅读时间&#xff1a;2023-12-23 1 介绍 年份&#xff1a;2018 &#xff1a;Chenxi Liu,Google DeepMind研究科学家;Barret Zoph,OpenAI;Maxim Neumann,Goolge 会议&#xff1a;B区会议&#xff0c; Proceedings of the European conference on computer vision (ECCV). 引用…

【Android14 ShellTransitions】(七)Transition就绪

Transition.onTransactionReady的内容比较长&#xff0c;我们挑重点的部分逐段分析&#xff08;跳过的地方并非不重要&#xff0c;而是我柿子挑软的捏&#xff09;。 1 窗口绘制状态的流转以及显示SurfaceControl 注意我们这里的SurfaceControl特指的是WindowSurfaceControll…

Excel办公技巧:制作二级联动下拉菜单

分享制作二级联动下拉菜单的方法&#xff0c;即使数据有增删&#xff0c;菜单也能自动更新&#xff01; 可以通过先定义名称&#xff0c;再结合数据验证&#xff0c;来做二级联动下拉菜单。 1. 准备数据 首先&#xff0c;我们需要准备好要进行二级联动下拉菜单的数据&#xff…

K8S 上部署 Emqx

文章目录 安装方式一&#xff1a;快速部署安装方式二&#xff1a;定制化部署1. 使用 Pod 直接部署 EMQX Broker2. 使用 Deoloyment 部署 Pod3. 使用 Services 公开 EMQX Broker Pod 服务4. 通过 kubernetes 自动集群 EMQX MQTT 服务器5. 修改 EMQX Broker 的配置6. 赋予 Pod 访…

共享自助台球厅系统,扫码开台,物联网开灯,智能计费

共享自助台球厅系统&#xff0c;扫码开台&#xff0c;物联网开灯&#xff0c;智能计费 含小程序&#xff0c;商家手机端和pc管理端 后端php 前端uniapp 纯开源 可定制 持续更新

常用的点云预处理算法

点云预处理是处理点云数据时的重要部分&#xff0c;其目的是提高点云数据的质量和处理效率。通过去除离群点、减少点云密度和增强特征&#xff0c;可以消除噪声、减少计算量、提高算法的准确性和鲁棒性&#xff0c;从而为后续的点云处理和分析步骤&#xff08;如配准、分割和重…

实战打靶集锦-31-monitoring

文章目录 1. 主机发现2. 端口扫描3. 服务枚举4. 服务探查4.1 ssh服务4.2 smtp服务4.3 http/https服务 5. 系统提权5.1 枚举系统信息5.2 枚举passwd文件5.3 枚举定时任务5.4 linpeas提权 6. 获取flag 靶机地址&#xff1a;https://download.vulnhub.com/monitoring/Monitoring.o…

算法力扣刷题记录 四十九【112. 路径总和】和【113. 路径总和ii】

前言 二叉树篇继续。 记录 四十九【112. 路径总和】和【113. 路径总和ii】 一、【112. 路径总和】题目阅读 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 target…

VsCode远程ssh连接失败:Could not establish connection to XXX

一、问题描述 在VsCode中按下"F1"&#xff0c;选择Remote-SSH:Connect to Host 选择一个已经配置好的SSH主机&#xff0c;比如我选择的是192.168.0.104&#xff1a; 结果提示&#xff1a;Could not establish connection to XXX 二、解决方法 观察VsCode的输出信息…

走进NoSql

一、引入 1.1什么是NoSql NoSQL&#xff08;Not Only SQL&#xff09;是一组非关系型数据库&#xff08;或称为非SQL数据库&#xff09;的统称&#xff0c;它们提供了与传统的关系型数据库不同的数据存储和检索方式。NoSQL数据库通常用于处理大量的、分布式的、非结构化或半结…