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

目录

线程安全

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.mfbz.cn/a/604801.html

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

相关文章

JavaEE企业级开发中常用的JDK7和JDK8的时间类

JDK7时间类 全世界的时间有一个统一的计算标准 在同一条经线上的时间是一样的 格林威治时间 简称GMT 计算核心 地球自转一天是24小时 太阳直射正好是12小时 但是误差太大 现在用原子钟来代替 用铯原子震动的频率来计算时间&#xff0c;作为世界的标准时间UTC 中国标准时间…

Dockerfile实践java项目

目的&#xff1a;用java项目测试dockerfil部署&#xff08;前提是安装好了docker&#xff09; 部署准备文件如下 1. java项目 java项目demo地址 https://gitee.com/xiaoqu_12/dockerfileDemo.git 或者百度网盘直接下载打包好的jar包 链接&#xff1a;https://pan.baidu.com/s/…

Ansible---inventory 主机清单

一、inventory 主机清单 1.1、inventory介绍 hosts配置文件位置&#xff1a;/etc/ansible/hosts Inventory支持对主机进行分组&#xff0c;每个组内可以定义多个主机&#xff0c;每个主机都可以定义在任何一个或多个主机组内。 1.2、inventory中的变量 Inventory变量名含义…

数值计算方法——大题题型总结

目录 一、绝对误差限、相对误差限 1.1 例题 1.2 解题套路 1.3 题解 二、敛散性、收敛速度 2.1 例题 2.2 解题套路 2.3 题解 三、牛顿迭代法 3.1 例题 3.2 解题套路 3.3 题解 四、割线法 4.1 例题 4.2 解题套路 ​4.3 题解 五、列主元素消去法 5.1 例题 5.…

新版Idea配置仓库教程

这里模拟的是自己搭建的本地仓库环境&#xff0c;基于虚拟机搭建利用gogs创建的仓库 1、Git环境 你需要准备好git和仓库可以使用github 、gitee等 1.1 拉取代码 本项目使用 Git 进行版本控制&#xff0c;在 gogs 上创建一个个人使用的 git 仓库&#xff1a; http://192.168.…

【Linux】项目自动化构建工具make/makefile的简单使用

使用步骤 1) 编写 创建 makefile 文件 vim makefile用 vim 打开名为 makefile 的文件,存在该文件则打开编辑,不存在则创建并打开.在 makefile 文件中编写需要编译的文件 test:test.cppg -o test test.cpp第一行: 冒号左侧为编译后的可执行文件名,可以随便取. 冒号右侧为依赖…

vue2项目升级到vue3经历分享4

后端重构&#xff0c;如果接口做好抽象封装&#xff0c;只需要考虑jar之间的兼容性问题&#xff0c;jdk版本不变&#xff0c;基本不用做太大的调整&#xff0c;但是前端就不一样&#xff0c;除了vue框架本身&#xff0c;css的调整&#xff0c;改起来更是让人头疼。前面写了vue2…

Linux与windows网络管理

文章目录 一、TCP/IP1.1、TCP/IP概念TCP/IP是什么TCP/IP的作用TCP/IP的特点TCP/IP的工作原理 1.2、TCP/IP网络发展史1.3、OSI网络模型1.4、TCP/IP网络模型1.5、linux中配置网络网络配置文件位置DNS配置文件主机名配置文件常用网络查看命令 1.6、windows中配置网络CMD中网络常用…

C++进阶之路:深入理解编程范式,从面向过程到面向对象(类与对象_上篇)

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

Mysql 基础 - 常见 子句

算数运算符 > < > < !/<> 逻辑运算符 3i in is null is not null 2l limit like 2o or 、order by 1a and ib between and 1n not and、or 、not、 in、 orderby、 limit、 like、 between...and、 is null 、is not null

我独自升级崛起怎么下载 游戏下载教程分享

《我独自升级&#xff1a;崛起》这款游戏核心聚焦于激烈的战斗与角色的持续成长。新加入的玩家首要任务是熟悉基础攻击模式&#xff0c;随后深入探索技能组合策略与连贯招式的艺术&#xff0c;同时掌握防守与躲避技巧&#xff0c;这些都是战斗中不可或缺的关键。随着战斗的持续…

那个在买珠宝的年轻人

金价搭上过山车&#xff0c;今年以来价格一路飙涨。 珍珠身价同步飙升&#xff0c;晋级珠宝圈“新宠”。 文玩圈“减龄”&#xff0c;盘珠串不再只是“老头乐”。 月薪3000的年轻人&#xff0c;悄悄实现“宝石”自由。 黄金珠宝走俏&#xff0c;这届年轻人到底有着怎样的珠宝…

Baidu Comate智能编码助手 -----AI编程帮你解放双手

目录 Baidu Comate是什么&#xff1f; Baidu Comate如何安装&#xff1f; 在VSCode上安装Baidu Comate插件 Baidu Comate如何使用&#xff0c;有哪些功能&#xff1f; 1.代码解释 2.代码注释 使用感受 如何体验 Baidu Comate是什么&#xff1f; Baidu Comate智能编码助手…

网络编程入门之UDP编程

欢迎各位帅哥美女来捧场&#xff0c;本文是介绍UDP网络编程。在这里&#xff0c;你会见到最详细的教程&#xff1b;细致到每一行代码&#xff0c;每一个api的由来和使用它的目的等。 目录 1.UDP相关API 1.1.两个类 1.2.两个类中的方法 2.UDP编程 2.1.大体框架 2.2.内容构…

修改el-checkbox样式

一定要在最外层&#xff1b; //未选中框/deep/ .el-checkbox__inner{border-color: #0862a3;}//选中框/deep/ .el-checkbox__input.is-checked .el-checkbox__inner{background-color: #0862a3;border-color: #0862a3;}//未选中框时右侧文字/deep/ .el-checkbox__label{}//选中…

企业做网站,如何设计才有创意?

企业做网站&#xff0c;如何设计才有创意&#xff1f;我们都希望能打造一个有创意的网站建设&#xff0c;能在众多网站中脱颖而出&#xff0c;能够营销推广公司的产品&#xff0c;为公司带来更多的经济效益收益。广州网站建设的时候&#xff0c;记住直观的设计可以让用户体验更…

Terrain —— Nodes

目录 Convert HeightField —— 转化高度场 HeightField —— 为地形创建初始高度场或遮罩场 HeightField Blur —— 模糊高度场或遮罩场 HeightField Clip —— 限制高度场的值 HeightField Combine Layers —— 将多个volume或VDB合并为一个新的volume或VDB HeightFiel…

C++浮点数format时的舍入问题

C浮点数format时的舍入问题 首先有这样一段代码&#xff1a; #include <iostream> #include <stdio.h> using namespace std;int main() {cout << " main begin : " << endl;printf("%.0f \r\n", 1.5);printf("%.0f \r\n&…

2024副业指南:年轻人热捧的七大赚钱副业,在家就能做!做得好的月入过万了

副业&#xff0c;听起来就像是在主业之外的“小打小闹”&#xff0c;但你知道吗&#xff1f;很多人通过副业实现了财务自由&#xff0c;甚至有的人副业收入超过了主业&#xff01; 今天&#xff0c;就让我们一起探索那些适合你的副业机会&#xff0c;让你在工作之余也能成为收入…

3D模型素材有哪些常见的用途?

3D模型素材已经成为了设计、游戏开发、电影制作和建筑等领域的重要工具。它们以其独特的形式和丰富的细节&#xff0c;为这些领域的专业人士提供了无尽的创作可能性。 1.建筑和室内设计&#xff1a;在建筑设计中&#xff0c;3D模型可以帮助建筑师更直观地展示设计方案&#xff…
最新文章