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

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

作者:~小明学编程 

文章专栏:JavaEE

格言:热爱编程的,终将被编程所厚爱。
Java中的多线程——线程安全问题

目录

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

什么是线程安全

线程不安全的原因

修改共享数据

修改操作不是原子的

内存可见性对线程的影响

指令重排序

解决线程不安全的问题

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上,然后++,最后再写回去,如果在这中间我们一个线程读到数据了,然后另外的一个线程也读到数据了,这时候两个线程++完毕返回的是同样的值,这也是我们上面产生问题的原因。

内存可见性对线程的影响

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

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

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

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

指令重排序

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

解决线程不安全的问题

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

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

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

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();
    }

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

notifyAll()方法

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

wait()和sleep()的对比

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

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

3.wait()是Object的方法而sleep()是Thread 的静态方法。文章来源地址https://www.toymoban.com/news/detail-434798.html

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

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

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

相关文章

  • 编程小白的自学笔记八(python中的多线程)

     编程小白的自学笔记七(python中类的继承) 编程小白的自学笔记六(python中类的静态方法和动态方法)  编程小白的自学笔记五(Python类的方法)  编程小白的自学笔记四(正则表达式模块search函数)  编程小白的自学笔记三(Python正则表达式)  目录 系列文章目录 前言

    2024年02月16日
    浏览(7)
  • “深入理解Java的多线程编程“

    多线程编程是指在一个程序中同时运行多个线程,以提高程序的并发性和性能。Java是一门支持多线程编程的强大编程语言,提供了丰富的多线程相关类和接口。 在Java中,可以通过以下方式实现多线程编程: 继承Thread类:创建一个继承自Thread类的子类,并重写run()方法,在

    2024年02月13日
    浏览(11)
  • 深入浅出Java的多线程编程——第二篇

    深入浅出Java的多线程编程——第二篇

    目录 前情回顾 1. 中断一个线程 1.1 中断的API 1.2 小结 2. 等待一个线程  2.1 等待的API 3. 线程的状态 3.1 贯彻线程的所有状态 3.2 线程状态和状态转移的意义 4. 多线程带来的的风险-线程安全 (重点) 4.1 观察线程不安全 4.2 线程安全的概念 4.3 线程不安全的原因 4.3.1 修改共享数据

    2024年02月07日
    浏览(18)
  • 【JavaEE】Java中的多线程 (Thread类)

    【JavaEE】Java中的多线程 (Thread类)

    作者主页: paper jie_博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文录入于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将基础知识一网打尽,希望可以帮到读者们哦。 其他专栏:《MySQL》《

    2024年02月05日
    浏览(7)
  • Java 8并发集合:安全高效的多线程集合

    Java 8并发集合:安全高效的多线程集合

    在多线程环境中,使用线程安全的数据结构非常重要,以避免竞态条件和数据不一致的问题。Java 8引入了一些并发集合类,提供了安全高效的多线程集合操作。本教程将介绍Java 8中的并发集合类,包括ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentSkipListSet和CopyOnWriteArrayList。 Conc

    2024年02月04日
    浏览(9)
  • 多媒体库SDL以及实时音视频库WebRTC中的多线程问题实战详解

    目录 1、概述 2、开源跨平台多媒体库SDL介绍 3、开源音视频实时通信库WebRTC介绍

    2024年02月08日
    浏览(12)
  • Qt的多线程编程

    Qt的多线程编程

    并发 当有多个线程在操作时,如果系统 只有一个CPU ,则它根本不可能真正同时进行一个以上的线程,它只能把 CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其他线程处于挂起状态。 虽然看起来所有 线程都是一起执行

    2024年02月08日
    浏览(6)
  • Java多线程编程中的线程同步

    Java多线程编程中的线程同步

    基本概念: ​ 线程同步是多线程编程中的一个重要概念,用于控制多个线程对共享资源的访问,以防止数据的不一致性和并发问题。 在多线程环境下,多个线程同时访问共享资源可能导致数据的竞争和不正确的结果。 是确保多个线程按照特定的顺序和规则访问共享资源,以

    2024年02月13日
    浏览(8)
  • Java多线程编程中的线程死锁

    Java多线程编程中的线程死锁

    ​ 在多线程编程中,线程死锁是一种常见的问题,它发生在两个或多个线程互相等待对方释放资源的情况下,导致程序无法继续执行 。本文将介绍线程死锁的概念、产生原因、示例以及如何预防和解决线程死锁问题。 线程死锁的概念 ​ 线程死锁是指两个或多个线程被阻塞

    2024年02月12日
    浏览(11)
  • 【并发编程】多线程安全问题,如何避免死锁

    【并发编程】多线程安全问题,如何避免死锁

    从今天开始阿Q将陆续更新 java并发编程专栏 ,期待您的订阅。 在系统学习线程之前,我们先来了解一下它的概念,与经常提到的进程做个对比,方便记忆。 线程和进程是操作系统中的两个重要概念,它们都代表了程序运行时的执行单位,它们的出现是为了更好地管理计算机

    2024年02月11日
    浏览(8)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包