Java中的多线程——线程安全问题

news2024/11/16 13:31:39

作者:~小明学编程 

文章专栏:JavaEE

格言:热爱编程的,终将被编程所厚爱。
在这里插入图片描述

目录

多线程所带来的不安全问题

什么是线程安全

线程不安全的原因

修改共享数据

修改操作不是原子的

内存可见性对线程的影响

指令重排序

解决线程不安全的问题

synchronized关键字

互斥

刷新内存

可重入

synchronized 的几种用法

直接修饰普通方法:

修饰静态方法

修饰代码块

锁类对象

volatile

Java 标准库中的线程安全类

死锁

什么是死锁

死锁的情况

死锁的必要条件

wait 和 notify

wait()

notify()

notifyAll()方法

wait()和sleep()的对比


多线程所带来的不安全问题

我们来看一下下面的这一段代码,代码的内容主要就是,一个变量count,我们用两个线程同时对其进行操作,每个线程都让其自增50000,但是我们最终看到的结果确是count不到100000,在50000和100000之间。

class MyClass{
    public static int count;

    public void increase() {
        count++;
    }
}
public class Demo2 {
    private static int count1;
    public static void main(String[] args) throws InterruptedException {
        MyClass myClass = new MyClass();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    count1++;
                    myClass.increase();
                }
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count1++;
                myClass.increase();
            }
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(count1);//65584
        System.out.println(MyClass.count);//65478
    }
}

这是什么原因呢?

什么是线程安全

所谓的线程安全就是:我们在多线程代码之下的运行结果是符合我们预期的并且和单线程下的运行结果一致,我们就说这是线程安全的。

上面的代码肯定不符合我们的预期也不是线程安全的。

线程不安全的原因

总体回答这个问题的话就是:

1.线程是抢占式执行的,线程之间的调度充满着随机性。

2.多个线程对同一个变量进行修改操作。

3.针对变量的操作不是原子性的。

4.内存的可见性也会影响线程的安全。

5.代码的顺序性。

修改共享数据

我们上面的代码就是属于修改共享的数据,其中我们的count是在堆上因此可以被多个线程共享。

修改操作不是原子的

所谓的原子性就是不可再分割的意思,例如我们上面的++操作其实是由三部分组成的,首先是要把数据从内存读到cpu上,然后++,最后再写回去,如果在这中间我们一个线程读到数据了,然后另外的一个线程也读到数据了,这时候两个线程++完毕返回的是同样的值,这也是我们上面产生问题的原因。

内存可见性对线程的影响

因为我们是多线程的操作所以共享同一块资源,当我们在对同一块资源下执行时候就能看到彼此。

我们的线程想要获取到内存里面的东西的话,都是先从内存中去拿然后放到寄存器里面去,然后再我们线程再去从寄存器里面去拿,当我们想要修改数据的时候就先放到寄存器再去放回内存中,这就导致了一个问题,如果我们改完了一个数据放到了寄存器还没放回内存的时候,这个时候我们另外线程从内存中拿数据就拿不到最新的数据了。

这就是内存可见性对线程的影响。

指令重排序

 指令的重排序是我们编译器对我们代码的执行顺序进行的调整,同样的目的但是顺序不一样我们所消耗的资源可能也不一样,我们编译器一般会保证我们执行的高效会对代码的顺序进行调整,但是当多线程的时候就不安全了,指令的重排序可能会使我们的线程发生混乱。

解决线程不安全的问题

synchronized关键字

互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待,这就解决了我们刚才不同的线程操作同一个变量的问题了,当我们一个线程去操作那个count的时候其它的线程加了锁此时别的线程就不能再去操作那个count了。

刷新内存

我们的刷新内存就是为了解决我们的共享内存的问题,我们前面说到我们拷贝内存到我们的寄存器里面再到我们的线程中,我们修改数据再原路返回,在这中间可能会有其它的线程再读这块内存,这就可能导致我们读到的数据不是最新的数据,然而加上我们的synchronized之后

1.我们首先会加锁,加锁之后别的线程就不能再去访问和读取这块内存了。

2.从内存中读取数据到寄存器和高速缓存中。

3.处理数据。

4.再将寄存器和高速缓存中的数据返回到内存中。

5.开锁,其它的线程可以读取内存中的数据了。

可重入

可重入是我们 synchronized 可以让我们的程序避免产生自己将自己锁住的关键。

所谓的自己将自己给锁住就是我们想要给同一块的代码重复的上锁,而且必须重复上锁才能继续的运行下去,如果我们不能重复上锁的话,我们就要等待该锁解除才能继续的上锁,但是要想解除该所就必须得执行重复上锁的代码,这就矛盾了,也就产生了死锁(下面详细介绍)。

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
    }
    synchronized void increase2() {
        increase();
    }
}

例如上面这段代码,我们调用increase2()的时候会对当前的对象加锁,然后我们再去调用increase()就又对当前的对象加了一次锁,这里不会产生错误是因为我们支持重复加锁,

在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息:
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取
到锁, 并让计数器自增.解锁的时候计数器递减为 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) {
                }
            }
        }

volatile

volatile可以保证我们的数据是从内存中读取的,防止优化而导致的线程不安全的问题。

我们的线程在操作内存的时候会先把内存里的数据放到寄存器中然后再从寄存器中拿到数据,但是从内存中拿数据是一个很慢的操作,所以有些时候进行一些优化然后就会直接从寄存器中拿数据,这个时候如果其它的线程更改了数据,这个时候我们拿到的就是旧的了。

我们的volatile就可以保证我们的内存可见性,保证我们拿到的数据都是从内存中拿到的,而不是工作内存(寄存器,缓存)中偷懒拿到。

当然我们的synchronized()也能保证我们内存的可见性,但是我们不能无脑的频繁使用synchronized(),因为其使用多了可能会造成线程阻塞等问题大大降低了我们的性能,解决内存可见性的问题的时候使用synchronized()所要付出的代码往往更高。

Java 标准库中的线程安全类

我们Java标准库中有很多的线程不安全的类常见的有

ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
因为这些类里面的代码都没有加锁,所以我们在使用的时候要格外的注意,为了解决部分的问题我们也提供了一些线程安全的类。

Vector
HashTable
ConcurrentHashMap
StringBuffer
这些类里面的关键方法都加了锁,所以在进行多线程的时候不用担心线程安全的问题。

其中我们的String类也是线程安全的虽然没有加锁但是,其本身的特性不可变让其具有线程安全。

死锁

什么是死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 

在我们的Java多线程操作的时候各个线程去争夺同一资源也会陷入到僵局这个时候也会产生死锁。

比如说我们前面的synchronized这个方法可以重复加锁,如果不能重复加锁的话,那么我们就会产生死锁,也就是上面说的不可重入性而导致的死锁。

死锁的情况

1.一个线程一把锁上面自己锁自己的情况。

2.两个线程两把锁,我们两个线程对两个对象分别上了锁,然后刚好这两个线程又要去操作两个对象,因为都对彼此上锁了,都到等对方结束,但是不执行又不能结束这就有产生了死锁。

2.n个线程m把锁。

死锁的必要条件

1.互斥使用:一个锁被一个线程占用以后,其它的线程就用不了了。

2.不可抢占:一个锁被一个线程占用以后,其它的线程不能抢占。

3.请求和保持:当一个线程占据多把锁的时候,除非显示的释放锁否则,否则这些锁始终都被占用。

4.环路等待,各个线程之间互相等待彼此解锁。

wait 和 notify

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

注意:

wait, notify, notifyAll 都是 Object 类的方法。

wait()

我们的wait()方法主要是为了让我们当前所在的线程进入到一个等待的状态,其工作原理分为三个步骤:

1.让我们当前所在的线程进入到一个等待的状态。

2.释放当前线程所在的锁。(所以我们在用wait()方法之前一定要有锁才行)。

3.等待条件被唤醒。

结束等待条件:

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("等待后");
        }
    }

 执行代码我们会发现我们一直处于等待的状态。

notify()

notify()方法是用来通知在等待被wait()等待的线程,这个线程已经失去了锁,我们别的线程通知wait()所在的线程后继续执行当前的代码,执行完毕之后退出当前的线程,然后wait所在的线程重新获得锁接着执行后面的代码。当我们有多个线程都在等待一个对象的锁的时候我们notify()会随机的释放一个线程。

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread thread = new Thread(()->{
            System.out.println("thread等待前");
            synchronized (object) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread等待后");
        });
        thread.start();
        Thread.sleep(3000);//主线程休眠
        Thread thread1 = new Thread(()->{
            System.out.println("thread1通知前");
            synchronized (object) {
                object.notify();
            }
            System.out.println("thread1通知后");
        });
        thread1.start();
    }

notifyAll()方法

相比于notify()方法,notifyAll()的方法在对多个线程同时等待的情况下会将会唤醒所有等待的线程,但是这个线程回去竞争当前的锁,竞争到然后去执行自己剩下的代码。

wait()和sleep()的对比

1.我们的sleep()是休眠我们当前的线程,而wait()是用于线程通信的。

2.wait()要搭配synchronized 使用. sleep ()不需要。

3.wait()是Object的方法而sleep()是Thread 的静态方法。

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

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

相关文章

一个高性能、无侵入的Java性能监控和统计工具,有点东西!

背景 随着所在公司的发展&#xff0c;应用服务的规模不断扩大&#xff0c;原有的垂直应用架构已无法满足产品的发展&#xff0c;几十个工程师在一个项目里并行开发不同的功能&#xff0c;开发效率不断降低。 于是公司开始全面推进服务化进程&#xff0c;把团队内的大部分工程…

你造Python中的上下文管理器是啥吗

上下文管理器(Context managers)让我们在需要的时候可以准确地分配或释放资源 Python中最常用的上下文管理例子就是with语句了&#xff0c;一般是在操作文件的时候&#xff0c;比如&#xff1a; 有PY基础的小伙伴都知道&#xff0c;上面的代码等价于&#xff1a; 对比两个例子…

python机器学习及深度学习在空间模拟与时间预测领域中的应用

了解机器学习的发展历史、计算原理、基本定义&#xff0c;熟悉机器学习方法的分类&#xff0c;常用机器学习方法&#xff0c;以及模型的评估与选择&#xff1b;熟悉数据预处理的流程&#xff0c;掌握python程序包的使用&#xff1b;理解机器学习在生态水文中的应用&#xff0c;…

使用Python绘制圣诞树教程(附源代码)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

Qt Creator 运行LVGL模拟器

windows下用Qt Creator运行LVGL 8.2 背景 最近在学习LVGL&#xff0c;手头又没有硬件&#xff0c;好多网上资料介绍了如何在PC端模拟&#xff0c;纯C语言实现的库模拟的话也不会复杂&#xff0c;恰巧本人熟悉Qt Creator&#xff0c;那就用这个环境模拟吧&#xff0c;网上搜索…

为什么企业传统网络访问海外应用程序不稳定、速度慢?怎么解决?

外贸、游戏等行业经常会有跨网数据访问的需求&#xff0c;并且访问慢、卡是常见的问题。这其中主要的原因是网络访问速度与物理距离有直接关系。刨除距离原因&#xff0c;还有哪些因素影响了我们的访问呢?那么访问国站慢的影响因素有哪些? 在中国的国内用户&#xff0c;使用应…

下载安装npm,配置环境变量详细教程

要在本地运行项目&#xff0c;就需要安装npm&#xff0c;其次还需要配置项目依赖node-modules。今天我们就先安装npm npm下载安装一、安装1、下载2、勾选同意&#xff0c;下一步3、默认的存储位置即可&#xff0c;next4、不用勾选&#xff0c;next5、点击install6、等待安装完成…

通过 Mito 在 Python 中使用电子表格

在本文中,您将学习如何使用这个强大的库,该库能够自动执行某些 Pandas 任务并以非常快速的方式执行数据分析。 Mitosheet是什么? Mitosheet 是 Python 中可用的众多库之一,它实际上是 Python 和电子表格之间的融合,大大加快了数据分析过程。 安装过程 您可以安装在分析…

《Python多人游戏项目实战》第五节 断线重连

目录 5.1 模拟弱网状态 5.2 断线重连 5.3 优化玩家名称显示 5.4 完整代码下载地址 导致客户端和服务端断开连接的原因可能有以下三种&#xff1a; 服务端主动关闭连接。客户端窗口关闭&#xff0c;玩家退出游戏。客户端所在网络不给力&#xff08;也叫做弱网&#xff09;&…

Redis - Redis持久化:AOF和RDB

1. 为什么要持久化 Redis是内从数据库&#xff0c;宕机后数据会丢失&#xff1b;Redis重启后&#xff0c;为了快速恢复数据&#xff0c;提供了持久化机制&#xff1b;Redis有两种持久化方式&#xff1a;RDB和AOF&#xff0c;这也是Redis无畏宕机与快速恢复数据的杀手锏。 注意…

全球代表供应商!腾讯安全NDR再获Gartner认可

近日&#xff0c;国际研究机构Gartner发布了2022年《Market Guide for Network Detection and Response》&#xff08;《网络检测和响应&#xff08;NDR&#xff09;市场指南》&#xff09;&#xff08;以下简称《报告》&#xff09;&#xff0c;腾讯安全被Gartner列为全球NDR市…

非零基础自学Golang 第17章 HTTP编程(上) 17.3 爬虫框架gocolly 17.3.1 gocolly简介

非零基础自学Golang 文章目录非零基础自学Golang第17章 HTTP编程(上)17.3 爬虫框架gocolly17.3.1 gocolly简介第17章 HTTP编程(上) 17.3 爬虫框架gocolly 我们在之前学习了如何使用标准库实现HTTP爬虫【其实也不算&#xff0c;就实现了简单的请求&#xff0c;但是爬虫不就是这…

别乱用了,用新的。Go SliceHeader 和 StringHeader 将会被废弃!

大家好&#xff0c;我是煎鱼。Go 语言中有个很经典的 (Slice|String)Header&#xff0c;经常出现在大家视野中&#xff0c;为此我写了《Go SliceHeader 和 StringHeader&#xff0c;你知道吗&#xff1f;》给大家介绍&#xff0c;避免被面试官卷到。以重点来讲&#xff0c;Slic…

Alibaba送给开发人员的“礼物”:Java架构成长笔记,深入内核,拒绝蒙圈

提起阿里&#xff0c;行外人联想到的关键词无非是“交易”、“淘宝”、“支付宝”&#xff0c;但对于程序员来说&#xff0c;阿里庞大的技术体系才是最吸引人的。实际上阿里作为国内一线互联网公司的头把交椅&#xff0c;内部的技术体系和发展都是备受关注的&#xff0c;对于程…

【javaScript总结归纳】字符串常用方法总结

前言 在js中我们对字符串进行一部分截取&#xff0c;可以使用slice()函数截取&#xff0c;也可以直接用substring()函数来截取&#xff0c;但是截取也有可能出bug const str小&#x20bb7;和小&#x27a01;今天吃了50块钱的KFC console.log(str.slice(0,5)); 可以在控制台看…

评估EtherCAT从站节点解决方案

本系列博客文章的第1部分介绍了用于C2000™微控制器的EtherCAT从站堆栈解决方案的市场机遇&#xff0c;以及从站堆栈开发快速入门的三个阶段指南。第2部分详细说明了TIC2000 MCU EtherCAT实施的特点和优势。第3部分分别介绍了使用EtherCAT从站和C2000 Delfino MCU controlCARD套…

Linux【windows使用xshell连接本地虚拟机】【Mac使用terminal连接本地虚拟机】

文章目录对于本地虚拟机的配置使用Mac的terminal的ssh连接本地虚拟机windows使用xshell连接本地虚拟机对于本地虚拟机的配置 IP地址和子网掩码。 在虚拟机中使用ping命令判断虚拟机到宿主机是否是连通的。&#xff08;不通的话&#xff0c;关闭Windows防火墙&#xff0c;再试一…

python+pyhyper实现识别图片中的车牌号

背景 最近领导给布置了一个基于图片识别车牌号的工具开发任务&#xff0c;然后就去研究实现逻辑&#xff0c;自己根据opencv写了一个小demo&#xff0c;发现不仅速度慢而且成功率极低。然后&#xff0c;就找到了Hyperlpr开源项目。 环境搭建 排雷1&#xff1a;有教程说在git…

动态内存管理易错点+分析例题

复习一下&#xff1a; 常见的错误&#xff1a; 1.可能返回的是空指针 2.对动态开辟的内存越界访问 3.非动态开辟内存却用free释放 4.使用free释放动态开辟内存的一部分 5.free多次释放 如果加了pNULL的话 free&#xff08;p&#xff09;相当于啥也不干 程序起码不会崩掉 6.动…

RabbitMQ、RocketMQ、Kafka延迟队列实现

延迟队列在实际项目中有非常多的应用场景&#xff0c;最常见的比如订单未支付&#xff0c;超时取消订单&#xff0c;在创建订单的时候发送一条延迟消息&#xff0c;达到延迟时间之后消费者收到消息&#xff0c;如果订单没有支付的话&#xff0c;那么就取消订单。 那么&#xf…