【Java】线程安全问题

这篇具有很好参考价值的文章主要介绍了【Java】线程安全问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【Java】线程安全问题,java,安全,python
在之前的文章中,已经介绍了关于线程的基础知识。

我的主页: 🍆🍆🍆爱吃南瓜的北瓜
欢迎各位大佬来到我的主页进行指点
一同进步!!!

🍇一、观察如下代码

我们创建两个线程t1和t2,对静态变量count执行++操作各50000次。
我们的预期结果是100000。但是当两个线程分别执行++操作时最后的结果是否为100000呢?

看这样一段代码

 static  int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(count);
    }
运行三次 结果如下

【Java】线程安全问题,java,安全,python
【Java】线程安全问题,java,安全,python
【Java】线程安全问题,java,安全,python

上述结果和预期差距很大
我们将线程写作串行执行
【Java】线程安全问题,java,安全,python
运行结果如下

【Java】线程安全问题,java,安全,python

第一次的代码运行没有达到我们的预期结果,是因为两个线程同时操作一个共享变量,涉及到线程安全问题。

🍇二、线程安全概念

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。

🍇三、线程不安全的原因

  1. 线程在系统中是随机调度的,是抢占式执行的

这是系统决定的

  1. 多个线程对同一个变量进行修改

如果没有抢占式执行,一个线程接着一个线程逐个完成自己的任务,那么也就不会担心这样的情况

  1. 线程针对变量的操作,不是“原子”操作

原子操作:是指不可再细分的操作
而文章开头举的例子中n++这一操作,在系统中其实被细分成三步:
第一步:把内存中count的值加载到CPU寄存器中
第二步:把寄存器中的值+1,还是继续保存在寄存器中
第三步:把寄存器的值写回到内存中的count。

  1. 内存的可见性问题,引起的线程不安全
  2. 指令重排序引起的线程不安全

如下是对文章开头的案例做出的解释

【Java】线程安全问题,java,安全,python

🍇四、解决线程不安全

线程的随机调度这是系统决定的。无法干预
通过修改代码结构,来避免多个线程对同一个变量的修改。
解决原因三,我们就引入锁的概念

1.锁synchronizatied

 锁本质上是操作系统提供的功能,由JVM封装提供api供我们使用。

锁后面带上()
()里面写的就是“锁对象”
锁对象的用途,有且只有一个,那就是用来区分,两个线程是否针对同一个对象加锁。
如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待。
如果不是,就不会出现锁竞争,也就不会阻塞等待。

至于这个对象是什么类型,没有关系。

在Java中,synchronized进入{ 就自动上锁,出 } 就自动解锁
免去了加锁解锁

锁的特性
1) 互斥

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

• 进⼊ synchronized 修饰的代码块, 相当于 加锁
• 退出 synchronized 修饰的代码块, 相当于 解锁
2)可重⼊
synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;
可重入实例

 public static int count = 0;
    public static void main(String[] args) {
        Object locker = new Object();
        Thread thread = new Thread(()->{
           synchronized (locker){
               synchronized(locker){
                   for (int i = 0; i < 50000; i++) {
                       count++;
                   }
               }
           }
        });
    }

在可重⼊锁的内部, 包含了 “线程持有者”“计数器” 两个信息.

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

synchronized
修饰普通方法,相当于对this加锁
修饰静态方法,相当于对类加锁

死锁

如下就是一段典型的死锁代码

public class main2 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Object lock2 = new Object();

        Thread t1 = new Thread(()->{
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2){
                    for (int i = 0; i < 5000; i++) {
                        count++;
                    }
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock1){
                    for (int i = 0; i < 5000; i++) {
                        count++;
                    }
                }
            }
        });

        t2.start();
        t1.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

线程t1拿到了锁lock1,并上锁,进入sleep等待
此时线程t2拿到了CPU的执行权,拿到了锁lock2,并上锁,进入sleep等待
t1又拿到CPU的执行权,此时想要拿到锁lock2,但是此时锁lock2被线程t2占用着,
此时线程t1拿不到锁lock2,不能	进行之后的操作,想要跳出这个代码块,
但是想要开启锁lock1,就必须拿到锁lock2,此时就进入了死锁的状态。

产生死锁的必要条件

  1. 锁具有互斥特性
    这是锁的基本特点,一个线程拿到锁之后,其他线程就只能阻塞等待。
  2. 锁不可抢占(不可被剥夺)
    一个线程拿到锁之后,除非它自己释放锁,否则别人抢不走
  3. 请求和保持
    一个线程拿到一把锁之后,不释放这个锁的前提下,在尝试获取其他锁
  4. 循环等待
    多个线程获取锁的过程中,出现了循环等待,A等待B,B又等待A。

以上四点缺一不可,缺少一个都构成不了死锁

如何避免死锁

第一点和第二点是synchronized的基本特性,这是更改不了的。

所以只能从第三点第四点开始入手

第三点的解决方案是

在书写代码时,尽量避免锁的循环嵌套,可以有效避免死锁的发生

第四点的解决方案是

约定好加锁的书顺序让所有的线程都按照规定的顺序来加锁

2.内存可见性问题‘

观察如下代码

public class main3 {
   static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1  = new Thread(()->{
            while (count == 0){
                  //循环体中什么都没有
            }
            System.out.println("线程t1结束");
        });
       Thread t2 = new Thread(()->{
           Scanner scanner = new Scanner(System.in);
           System.out.println("请输入整数count");
           count = scanner.nextInt();
       });

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

       t1.join();
       t2.join();


    }

}

运行如下

【Java】线程安全问题,java,安全,python

我们预期看到的应该是输入1后,程序结束。
但结果与预期并不相同,这是为什么呢?

问题主要出在循环体这里

【Java】线程安全问题,java,安全,python
我们可以看到循环体内什么都没有
站在指令的角度来刨析问题
1) load操作,从内存中将count的值读取到CPU寄存器中。
2)cmp操作 (比较 同时跳转)
条件成立,继续顺序执行
条件不成立,跳转到另一个地址来执行。

由于当前循环体是空的,循环体旋转速度很快
短时间内出现大量的load和cmp操作反复执行的效果
load执行消耗的时间会比cmp多很多(相差几个数量级)
这时JVM发现,load速度非常慢且每次load执行的结果都是一样的(t2未更改时)此时,JVM就会把load操作给优化掉,当然这是JVM的一个优化的bug

那么如何解决呢?

在循环内书写IO操作或阻塞操作(sleep),就会使旋转体的速度降低了。
 while (count == 0){
                System.out.println("hello t1");
            }

IO操作使循环速度减低
如果循环操作中存在IO操作,就没有优化load操作的必要了
IO操作不能被优化!!!

这就是内存可见性问题’

一个线程针对一个变量进行读操作,另一个线程针对这个变量进行修改,此时读的线程,不一定能感知到这个变量被改了

解决方案

引入volatile关键字
这个关键字从字面意思上理解是 “易变的,不稳定的”,如果给变量加上这个关键字,仿佛在告诉 JVM/编译器,这个变量很不稳定,极有可能发生变化,从而不让编译器优化!

格式如下

volatile static int count = 0;

但是volatile不能保证原子性
实例如下

public class main4 {
    public volatile static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count);
    }

    //第一次执行9979
    //第二次执行9190
    //第三次执行9715
}

把光标放置到count++这条语句上,可以发现如下报错。
对易变字段 ‘count’ 的非原子操作
【Java】线程安全问题,java,安全,python
对 count 这个易变字段执行了一个非原子操作,这可能会导致在多线程环境下数据的不一致或不可预测的行为。在实际编程中,如果你需要对某个字段进行复合操作(比如先读后写或先比较后更新),并且这个操作需要在多线程环境中是安全的,那么仅仅使用 volatile 是不够的,你还需要使用其他同步机制(如 synchronized 块或 java.util.concurrent.atomic 包中的原子类)来确保操作的原子性。

小结
使用synchronized可以保证原子性
使用volatile可以保证内存可见性
如果后面写代码的时候,既要考虑原子性,又要考虑内存可见性,直接把 synchronized 和 volatile 都加上即可。

以上就是本文所有内容,如果对你有帮助的话,点赞收藏支持一下吧!💞💞💞文章来源地址https://www.toymoban.com/news/detail-846221.html

到了这里,关于【Java】线程安全问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • java线程安全问题及解决

    当我们使用多个线程访问 同一资源 (可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程 只有读操作 ,那么不会发生线程安全问题。但是如果多个线程中对资源有 读和写 的操作,就容易出现线程安全问题。 案例: 火车站要卖票,我们模拟火车站的卖票

    2024年02月15日
    浏览(38)
  • java 线程安全问题 三种线程同步方案 线程通信(了解)

    线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。 下面代码演示上述问题,先定义一个共享的账户类: 在定义一个取钱的线程类 最后,再写一个测试类,在测试类中创建两个线程对象 某个执行结果: 为了解决前面的线程安全问题,

    2024年02月09日
    浏览(45)
  • Java多线程【状态与安全问题】

    线程状态 说明 NEW 安排了工作, 还未开始行动 RUNNABLE 可工作的. 又可以分成正在工作中和即将开始工作 BLOCKED 这几个都表示排队等着其他事情 WAITING 这几个都表示排队等着其他事情 TIMED_WAITING 这几个都表示排队等着其他事情 TERMINATED 工作完成了 1.NEW状态:安排了工作, 还未开始

    2023年04月09日
    浏览(50)
  • Java多线程基础-8:单例模式及其线程安全问题

    单例模式是经典的设计模式之一。什么是设计模式?代码的设计模式类似于棋谱,棋谱就是一些下棋的固定套路,是前人总结出来的一些固定的打法。依照棋谱来下棋,不说能下得非常好,但至少是有迹可循,不会下得很糟糕。代码的设计模式也是一样。 设计模式,就是软件

    2024年02月05日
    浏览(50)
  • Java中SimpleDateFormat的线程安全性问题

    在日常开发中,我们经常会用到时间,我们有很多办法在Java代码中获取时间。但不同的方法获取到的时间格式不尽相同,这时就需要一种格式化工具,把时间显示成我们需要的格式,最常用的方法就是使用SImpleDateFormat类。这是一个看上去功能比较简单的类,但使用不当,也

    2024年01月25日
    浏览(46)
  • 【Java|多线程与高并发】线程安全问题以及synchronized使用实例

    Java多线程环境下,多个线程同时访问共享资源时可能出现的数据竞争和不一致的情况。 线程安全一直都是一个令人头疼的问题.为了解决这个问题,Java为我们提供了很多方式. synchronized、ReentrantLock类等。 使用线程安全的数据结构,例如ConcurrentHashMap、ConcurrentLinkedQueue等

    2024年02月09日
    浏览(47)
  • 针对java中list.parallelStream()的多线程数据安全问题我们采用什么方法最好呢?

    当使用List.parallelStream()方法进行多线程处理时,可能会涉及到数据安全问题。下面是一些常见的方法来处理parallelStream()的多线程数据安全问题: 1. 使用线程安全的集合:Java中提供了线程安全的集合类,如CopyOnWriteArrayList和synchronizedList等。可以将原始的List转换为线程安全的集

    2024年02月10日
    浏览(41)
  • Java 多线程之线程安全集合

    集合关系图 本文主要关注线程安全的集合,如 List、Set、Queue、Map 等接口的线程安全的实现方式,有关集合基础知识请转到这里。所谓线程安全集合,就是在多线程环境中使用集合不会导致数据不一致和数据异常的集合。在 Java 中线程安全集现在基本都使用 java.util.concurrent

    2024年02月05日
    浏览(49)
  • java多线程之线程安全(重点,难点)

    由于操作系统中,线程的调度是抢占式执行的,或者说是随机的,这就造成线程调度执行时,线程的执行顺序是不确定的,虽然有一些代码在这种执行顺序不同的情况下也不会运行出错,但是还有一部分代码会因为执行顺序发生改变而受到影响,这就会造成程序出现Bug,对于多线程并发

    2024年01月25日
    浏览(52)
  • 【Java】Java中线程安全有哪些实现思路?

    在 Java 多线程编程中,线程安全是一个非常重要的概念。 线程安全通常指程序在多线程并发执行时,仍然能够保持正确的行为。 Java 提供了很多实现线程安全的方法,本文将介绍几种常见的实现思路。 synchronized 是 Java 中最基本的解决线程安全问题的方法,它可以确保

    2024年02月04日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包