【JAVA语言-第20话】多线程详细解析(二)——线程安全,非线程安全的集合转换成线程安全

news2025/1/17 1:06:30

目录

线程安全

1.1 概述 

1.2 案例分析 

1.3 解决线程安全

1.3.1 synchronized关键字

1.3.1.1 同步代码块 

1.3.1.2 同步方法

1.3.2 使用Lock锁

1.3.2.1 概述 

代码示例

1.4 线程安全的类

1.4.1 非线程安全集合转换成线程安全集合 


线程安全

1.1 概述 

        指如果有多个线程在同时运行,而这些线程可能会同时运行某段代码,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全。

1.2 案例分析 

那什么情况下会导致【线程不安全】呢?看如下案例:

假设:有三家电影院,卖票形式分别为以下A、B、C三种。

思考:哪一种卖票形式会出现问题呢?

  • 第1种:开一个窗口,卖100张票,不会出现问题,单线程程序不存在线程安全问题。
  • 第2种:开三个窗口,但是每个窗口票的号码不冲突,也不会出现问题,属于线程安全。
  • 第3种:开三个窗口,但是每个窗口票的号码一样。如果1,2,3三个窗口访问同一张票,那进入的结果和返回的结果很有可能不一致。这就出现了线程安全问题。

结论:

        买票出现了线程安全问题,可能会出现重复的票和不存在的票,但是线程安全问题是不允许出现的。 

1.3 解决线程安全问题

那怎么解决线程安全问题呢?

        我们可以让一个线程在访问共享数据的时候,无论是否失去了CPU的执行权,让其他的线程只能等待,等待当前线程买完票,其他线程在进行买票。保证同时只有一个线程在买票

1.3.1 使用synchronized关键字

        在Java中,synchronized是一个关键字,用于控制多个线程对 对象或方法 的访问。当一个代码块被标记为synchronized时,只允许一个线程在同一时间执行该代码块。这样做是为了防止并发访问和潜在的数据损坏或不一致。该关键字可以使用在同步代码块或者同步方法用来解决线程安全问题。

1.3.1.1 同步代码块 

         一个同步代码块一次只允许一个线程进入,并确保它完成执行后其他线程才能进入。这是通过使用与同步代码块关联的对象的内在锁(或监视器)来实现的。

格式:
       synchronized(锁对象){
           可能会出现线程安全问题的代码(访问了共享数据的代码)
       }

        

注意事项:

        1.同步代码块中的锁对象,可以使用任意的对象。

        2.必须保证多个线程使用的锁对象是同一个。

        3.锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。

代码示例: 

RunnableImpl.java:多线程的实现类

package com.zhy.multiplethread;

public class RunnableImpl implements Runnable{
    /**
     * 共享票数
     */
    private int ticket = 10;

    /**
     * 设置线程任务:卖票
     */
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true){
            //同步代码块,保证每次只有一个线程占用锁对象
            synchronized (this){
                //当存在余票时,进行卖票操作
                if (ticket > 0){
                    //为了表示卖票需要时间,暂停10毫秒
                    try {
                        Thread.sleep(10);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 正在卖第 " + ticket + "张票");
                    ticket--;
                }
            }//出了同步代码块,归还锁对象,供线程重新抢占
        }
    }
}

TestThread.java:线程测试类

package com.zhy.multiplethread;

public class TestThread {
    public static void main(String[] args) {
        RunnableImpl impl = new RunnableImpl();
        Thread t1 = new Thread(impl);
        Thread t2 = new Thread(impl);
        Thread t3 = new Thread(impl);
        //开启3个线程一起抢夺CPU的执行权,谁抢到谁执行
        t1.start();
        t2.start();
        t3.start();
    }
}

输出结果:多个线程共同抢占CPU进行卖票操作,不会出现线程安全问题。

1.3.1.2 同步方法

        当一个方法被声明为synchronized时,即使有多个线程同时访问该方法,也只允许一个线程执行。在这种情况下使用的锁是调用该方法的对象实例。  

格式:
        修饰符 synchronized 返回值类型 方法名(参数列表){
                可能会出现线程安全问题的代码(访问了共享数据的代码)
        }

        

使用步骤:

        1.把访问了共享数据的代码抽取出来,放到一个方法中。

        2.在方法上添加synchronized修饰符

代码示例:

RunnableImpl.java:多线程的实现类 

package com.zhy.multiplethread;

public class RunnableImpl implements Runnable{
    /**
     * 共享票数
     */
    private int ticket = 10;

    /**
     * 设置线程任务:卖票
     */
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true){
            payTicket();
        }
    }

    /**
     * 同步方法:卖票
     */
    public synchronized void payTicket(){
        //当存在余票时,进行卖票操作
        if (ticket > 0){
            //为了表示卖票需要时间,暂停10毫秒
            try {
                Thread.sleep(10);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 正在卖第 " + ticket + "张票");
            ticket--;
        }
    }
}

结论:

        通过使用synchronized关键字,我们可以确保在多线程环境中共享资源的安全访问。

1.3.2 使用Lock锁

1.3.2.1 概述 

        java.util.concurrent.locks.Lock接口:实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。 

Lock接口中的方法:
       void lock():获取锁
       void unlock():释放锁

        

实现类:

        java.util.concurrent.locks.ReentrantLock implements Lock接口

        

使用步骤:

        1.在成员位置创建一个ReentrantLock对象。

        2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁。

        3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁。一般放在finally里面执行。

代码示例:

RunnableImpl.java:多线程实现类

package com.zhy.multiplethread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class RunnableImpl implements Runnable{
    /**
     * 共享票数
     */
    private int ticket = 10;

    Lock l = new ReentrantLock();

    /**
     * 设置线程任务:卖票
     */
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while (true){
            //获取锁:当存在余票时,进行卖票操作
            l.lock();
            try {
                if (ticket > 0) {
                    //为了表示卖票需要时间,暂停10毫秒
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + " 正在卖第 " + ticket + "张票");
                    ticket--;
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                //释放锁:为了避免忘记释放或者出现异常,造成死锁,该操作放在finally中执行
                l.unlock();
            }
        }
    }
}

结论:

        同步保证了只能有一个线程在同步中执行共享数据,保证率安全,但是程序频繁的判断锁、获取锁、释放锁、程序的效率会降低。

1.4 线程安全的类

        如果一个类,所有的方法都是有synchronized修饰的,那么该类就叫做线程安全的类。保证同一时间,只有一个线程能够进入 这种类的一个实例 的去修改数据,进而保证了这个实例中的数据的安全,不会同时被多个线程修改而变成脏数据。

  • 操作集合的线程安全的类:Vector,Hashtable
  • 操作字符串的线程安全的类:StringBuffer

1.4.1 非线程安全集合转换成线程安全集合 

        ArrayList是非线程安全的,如果多个线程可以同时进入一个ArrayList对象的add/remove方法。那会造成什么后果呢,我们先看一个案例。 

场景:

        定义一个List集合,初始化5个元素。定义一个增加线程(往集合的头部持续插入1000个元素)和减少线程(从集合的头部持续移除1000个元素)同时操作该集合,我们最终想要的效果是:增加和减少的次数一致,最终集合内的元素仍然是初始化的元素。

代码示例:

package com.zhy.multiplethread;

import com.zhy.thread.RunnableImpl;

import java.util.ArrayList;
import java.util.List;

public class TestThread {
    public static void main(String[] args) {
        //初始化List集合
        List<Integer> nonThreadSafeList = new ArrayList<Integer>();
        for (int i = 0; i < 5; i++){
            nonThreadSafeList.add(i + 3);
        }
        System.out.println("初始化List集合:" + nonThreadSafeList);

        //验证:使用两个线程同时往集合中插入1000个元素,在删除1000个元素
        int n = 1000;
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];

        //将所有 增加线程 加入到addThreads数组中
        for (int i = 0; i < n; i++){
            Thread addThread = new Thread(){
                @Override
                public void run() {
                    nonThreadSafeList.add(0,1);
                    try {
                        //暂停1000毫秒,给其他线程抢占CPU的时间
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            };
            addThread.start();
            addThreads[i] = addThread;
        }

        //将所有 减少线程 加入到reduceThreads数组中
        for (int i = 0; i < n; i++){
            Thread reduceThread = new Thread(new RunnableImpl(){
                @Override
                public void run() {
                    if (nonThreadSafeList.size() > 0){
                        nonThreadSafeList.remove(0);
                    }
                    try {
                        //暂停1000毫秒,给其他线程抢占CPU的时间
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            });
            reduceThread.start();
            reduceThreads[i] = reduceThread;
        }

        //等待所有增加线程执行完成
        for (Thread addThread : addThreads){
            try {
                //将 增加线程 加入到主线程中
                addThread.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        //等待所有 减少线程 执行完成
        for (Thread reduceThread : reduceThreads){
            try {
                //将 减少线程 加入到主线程中
                reduceThread.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        //所有增加线程 和 减少线程 执行完毕后,List集合中的数据:正确应该为初始数据
        System.out.println("所有增加线程 和 减少线程 执行完毕后,List集合中的数据:" + nonThreadSafeList);
    }
}

输出结果:

        使用非线程安全的集合进行多线程处理,很显然最终的结果并不是我们想要的,出现了null的元素,且集合内的元素也不是初始化的元素。 

注:并不是每一次执行都会出现错误的结果,多执行几次,会发现执行结果并不一致。 

那如何把非线程安全的集合转换成线程安全的呢?  

        以ArrayList为例,使用Collections工具类中的synchronizedList,可以把ArrayList转换为线程安全的List。 

源码:

        public static <T> List<T> synchronizedList(List<T> list) ;

        

使用:Collections.synchronizedList(list);

        改造上述代码,变成线程安全,只需加入如下代码,然后将多线程中操作的集合换成转换后的集合即可:

        //将List转换成线程安全的类
        List<Integer> threadSafeList = Collections.synchronizedList(nonThreadSafeList);

 最终的执行结果如下,执行多次,结果一致。


        与此类似的,还有HashSet,LinkedList,HashMap等等非线程安全的类,具体类型如下,都可以通过Collections工具类转换为线程安全的

 


1.5 总结

        在多线程中,线程安全问题是不允许被出现的。所以我们在使用多线程时,对于共享数据,可以通过synchronized关键字和Lock锁来处理,保证线程安全。 synchronized使用简单但灵活性较差;而Lock是一个更灵活的同步方式,可以实现更复杂的同步需求,但需要手动管理锁的获取和释放。在实际开发中,可以根据具体需求进行选择。

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

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

相关文章

外贸企业邮箱怎么注册?五款外贸企业邮箱注册步骤

专业的公司邮箱不但能提高企业形象&#xff0c;也能保证商业沟通的流畅与信息安全性。本文将详解五种常见的外贸企业邮箱——Zoho Mail公司邮箱、腾讯公司邮箱、阿里公司邮箱、网易公司邮箱以及Gmail公司邮箱的注册流程&#xff0c;协助外贸企业轻松完成邮箱注册&#xff0c;打…

安装Nox夜神模拟器关闭了HyperV后Docker运行不了怎么办?

1.背景 为了模拟真机&#xff0c;尝试安装了Nox夜神模拟器&#xff0c; 安装过程要求关闭Hyper-V。当时只是在程序安装卸载中关闭了系统服务。以为到时勾选上就好了。操作路径&#xff1a;控制面板\所有控制面板项\程序和功能\启用或关闭Windows功能\Hyper-V。 后来卸载掉了夜神…

代码随想录第四十七天|打家劫舍、打家劫舍Ⅱ、打家劫舍Ⅲ

题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 代码如下&#xff1a; 打家劫舍问题需要注意相邻房子不能偷&#xff0c;因此在偷与不偷的选择上可以归类为&#xff1a; 偷当前&#xff08;i&#xff09;的房子&#xff0c;则子问题变为前i-2个房子的偷取金额…

VMware虚拟机提示内存不足

VMware虚拟机&#xff0c;k8s集群搭建内存不足的问题 疑问&#xff1a;我的电脑是8G8G双通道的内存&#xff0c;当我在搭建k8s集群时给master-2G内存&#xff0c;node1-3G内存&#xff0c;node2-3G内存&#xff1b; 当依次打开虚拟机到node2时VM提示“物理内存不足&#xff0c;…

【第14章】spring-mvc之ajax

文章目录 前言一、准备二、单个值1.前端2.后端3. 结果 三、对象1.前端2.后端3. 结果 四、JSON对象1.前端2.后端3. 结果 五、JSON数组1.前端2.后端3. 结果 总结 前言 AJAX&#xff08;Asynchronous JavaScript and XML&#xff09;是一种用于创建快速动态网页的技术&#xff0c…

大模型微调实战之强化学习 贝尔曼方程及价值函数(一)

大模型微调实战之强化学习 贝尔曼方程及价值函数 强化学习&#xff08;RL&#xff09;是机器学习中一个话题&#xff0c;不仅在人工智能方面。它解决问题的方式与人类类似&#xff0c;我们每天都在学习并在生活中变得更好。 作为一名大模型学习者&#xff0c;当开始深入研究强…

2024蓝桥杯网络安全部分赛题wp

爬虫协议 题目给了提示访问robots.txt 会出三个目录 访问最后一个 点进去就flag{22560c15-577c-4c8b-9944-815473758bad} packet 下载附件&#xff0c;这个是流量包 放wireshark流量分析 搜http协议 发现有cat flag命令&#xff0c;直接看他返回的流量 最后base64解码即可…

Web服务器和Tomcat

Web介绍 对于http协议操作进行封装、简化web程序开发 部署web项目&#xff0c;对外提供上网信息浏览 Tomcat介绍 一个轻量级的web服务器 也称为web容器 Tomcat的文件夹介绍 下载地址&#xff1a;Apache Tomcat - Apache Tomcat 9 Software Downloads 安装&#xff1a;直…

嵌入式数据库SQLite 3配置使用详细笔记教程

0、惨痛教训 随着管理开发的项目体积越来越庞大&#xff0c;产品系统涉及的数据量也越来越多&#xff0c;并且伴随着项目不久就要交付给甲方了。如果项目的数据信息没有被妥善管理&#xff0c;后期设备的运行状态、操作状况等数据流信息不能被溯源&#xff0c;当出现了一些特殊…

typescript的入门到吐槽:看了typescript,发现前端真的卷,

typescript TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集&#xff0c;而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。 TypeScript 与 JavaScript 的区别 其实就是对JavaScript的封装&#xff0c;把一个弱类型语言封…

“王翦五讨赏地,萧何三贬其身”的背后,正是智者安身的处世之道

冯子曰&#xff1a;智者&#xff0c;术所以生也&#xff1b;术者&#xff0c;智所以转也。 智慧的人&#xff0c;从不蛮行横性&#xff0c;而是懂得如何在世道和自我之间谋得最佳的处境。 01、王翦五讨赏地 战国时期&#xff0c;秦始皇派王翦率六十万大军攻打楚国&#xff0…

【SpringBoot】 什么是springboot(一)?如何搭建springboot项目?

文章目录 SpringBoot第一章1、什么是springboot1、回顾ssm项目搭建流程2、springboot项目的优点2、搭建springboot项目方式1:方式2:第二章1、基本配置1、热部署2、注解3、端口配置application.properties特点application.yml特点注意4、环境配置springboot中的配置文件要求5、…

深度学习——前馈全连接神经网络

前馈全连接神经网络 1.导入需要的工具包2.数据导入与数据观察&#xff08;1&#xff09;读取csv的文件信息&#xff1a;&#xff08;2&#xff09;训练数据前5行&#xff08;3&#xff09;打印第一个图&#xff08;4&#xff09;观察数据中的信息&#xff08;5&#xff09;查看…

JAVA版本的ATM编程问题记录

前段时间用C语言写了个银行ATM系统&#xff0c;还写了一篇文章记录了一些&#xff0c;C语言的ATM文章。后来又用IDEA写了一个JAVA版本的银行ATM。有人就会问为啥浪费这个时间写ATM呢&#xff1f;&#x1f9d0;其实是我本科代码没学好&#xff0c;所以现在想利用比较熟悉的ATM系…

第 8 章 电机测速(自学二刷笔记)

重要参考&#xff1a; 课程链接:https://www.bilibili.com/video/BV1Ci4y1L7ZZ 讲义链接:Introduction Autolabor-ROS机器人入门课程《ROS理论与实践》零基础教程 8.3.3 电机测速01_理论 测速实现是调速实现的前提&#xff0c;本节主要介绍AB相增量式编码器测速原理。 1.概…

faad2交叉编译——aac解码为pcm,解决faad单通道转双通道问题

FAAD是比较成熟高效的开源AAC解码库&#xff0c;这里用于解码AAC生成PCM数据&#xff0c;用于音频播放。这里因为faad库&#xff0c;会将单通道转化为双通道踩了些坑&#xff0c;所以记录一下。 我使用的是2.11.0版本&#xff0c;貌似往前的版本没有使用CMake&#xff0c;需要c…

如何在电脑中使用微信?pc版微信和手机版有什么区别

微信简介 微信是一个社交和商业强国&#xff0c;自成立以来一直在持续增长。该应用程序在2023年拥有13亿月活跃用户&#xff0c;尽管在某些国家对其使用存在一些阻力。 使用微信网页版可以获得的好处 尽管微信为iOS和Android智能手机提供了出色的应用程序&#xff0c;但仍有时…

面向初学者:什么是图数据库

当数据成为关键生产要素&#xff0c;许多企业开始面临利用海量数据辅助企业复杂决策的现实难题。而在数据爆发式增长&#xff0c;关联复杂度激增的趋势下&#xff0c;图数据库成为企业加工关联数据、挖掘隐藏价值、智能决策升级的关键技术之一&#xff0c;在全球范围内开始被使…

sql优化思路

sql的优化经验 这里解释一下SQL语句的优化的原理 1.指明字段名称&#xff0c;可以尽量使用覆盖索引&#xff0c;避免回表查询&#xff0c;因此可以提高效率 2.字面意思&#xff0c;无需过多赘述。索引就是为了提高查询效率的。 3.图中两条sql直接可以使用union all 或者 uni…

一文盘点 Partisia Blockchain 生态 4 月市场进展

Partisia Blockchain 是一个以高迸发、隐私、高度可互操作性、可拓展为特性的 Layer1 网络。通过将 MPC 技术方案引入到区块链系统中&#xff0c;以零知识证明&#xff08;ZK&#xff09;技术和多方计算&#xff08;MPC&#xff09;为基础&#xff0c;共同保障在不影响网络完整…