线程安全 订阅
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。 展开全文
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
信息
外文名
thread
作    用
保证各线程正常且正确的执行
中文名
线程安全
线程安全简介
多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题大多是由全局变量及静态变量引起的,局部变量逃逸也可能导致线程安全问题。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。类要成为线程安全的,首先必须在单线程环境中有正确的行为。如果一个类实现正确(这是说它符合规格说明的另一种方式),那么没有一种对这个类的对象的操作序列(读或者写公共字段以及调用公共方法)可以让对象处于无效状态,观察到对象处于无效状态、或者违反类的任何不可变量、前置条件或者后置条件的情况。此外,一个类要成为线程安全的,在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排或者交错,它必须仍然有如上所述的正确行为,并且在调用的代码中没有任何额外的同步。其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、全局一致的顺序发生的。正确性与线程安全性之间的关系非常类似于在描述 ACID(原子性、一致性、独立性和持久性)事务时使用的一致性与独立性之间的关系:从特定线程的角度看,由不同线程所执行的对象操作是先后(虽然顺序不定)而不是并行执行的。
收起全文
精华内容
下载资源
问答
  • 线程安全
    千次阅读
    2022-01-28 17:29:26

    什么是线程安全

    多线程执行某段代码,不对这段代码进行同步处理、线程间的协调,程序运行的结果仍与预期一致,这就是线程安全。

    多线程编程的三个核心概念

    • 原子性: 同数据库事务的原子性,一些操作要么全部成功,要么全部失败,经典的例子就是银行转账。
    • 可见性:多线程并发访问共享变量时,某个线程对共享变量的更新,其他线程能立即看到这个更新。

    在java中,对象储存在主内存。每个线程都有自己的工作内存,线程从主内存读取对象到工作内存,执行更新对象操作,立即更新工作内存,但没有立即刷新到主内存,故其他线程仍然读到旧值。

    • 顺序性: 指程序顺序执行代码中的操作。

    编译器、处理器会进行指令重排序,优化代码,以提高处理速度。程序实际的执行顺序可能与代码中的不一样,但编译器、处理器保证结果是一样的。这不会影响单线程的正确性,但会影响多线程的正确性。

    要保证多线程程序正确执行,必需保证原子性、可见性、顺序性。

    synchronized、ReentrantLock

    使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、顺序性。使用synchronized、ReentrantLock获取到锁,线程更新共享变量后会立即刷新到主内存,其他线程获取到同一个锁时会将缓存失效并从主内存读取新值,保证了可见性。注意是同一个锁才能保证可见性。synchronized、ReentrantLock的性能较差,因为一个线程占有锁,其他线程会阻塞,线程的挂起和唤醒会降低处理速度。

    原子操作类

    使用AtmoicInteger、AtmoicLong、AtmoicReference来保证原子性,底层使用CAS(compare and swap)。CAS(compare and swap)是非阻塞同步的计算机指令, 它有三个操作数,内存位置、旧的预期值、新值,当内存位置的值与旧的预期值相等时才将新值存入内存位置。对于ABA问题,可使用AtomicStampedReference,通过引入版本号解决。

    volatile

    volatile能保证可见性和一定程度的顺序性。
    变量被volatile修饰时,线程对变量进行写操作时jvm会向处理器发送lock前缀指令,lock前缀指令相当于内存屏障。
    内存屏障的功能

    • 写操作修改的值会立即刷新到主内存,并设置其他线程的缓存无效,线程读取变量必需从主内存读取新值,保证了可见性。
    • 禁止指令重排序,后面的指令不能再内存屏障之前,前面的指令不能再内存屏障之后,保证一定程度的顺序性。

    ThreadLocal

    在这里插入图片描述
    每个线程Thread都有一个ThreadLocalMap的变量threadLocals。
    ThreadLocal不存储对象,对象存储于每个线程的ThreadLocalMap。ThreadLocal就是对当前线程的ThreadLocalMap进行增删改查。

    ThreadLocal中的内存泄露问题

    ThreadLocalMap的key是弱引用,这个key就是ThreadLocal对象。
    若ThreadLocal变量被设置为null后,且没有强引用指向这个ThreadLocal对象,根据垃圾回收的可达性分析算法,该ThreadLocal对象将被回收,ThreadLocalMap中某个Entry的key就会变为null,不能再使用的value无法释放内存,造成内存泄露。
    ThreadLocalMap的补救措施,调用getEntry()、set()、remove()方法会清除key为null的entry。但不调用这三个方法仍然会有内存泄露问题,因此当ThreadLocal使用完时应当remove掉。

    如何实现线程安全

    • 互斥同步: 使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、可见性、顺序性。
    • 非阻塞同步: CAS(compare and swap)是非阻塞同步的计算机指令, 它有三个操作数,内存位置、旧的预期值、新值,当内存位置的值与旧的预期值相等时才将新值存入内存位置。对于ABA问题,可使用AtomicStampedReference,通过引入版本号解决。
    • 无同步: 使用ThreadLocal将共享变量的可见性限制在线程内部,每个线程维护一个共享变量的副本,线程间相互隔离,不再争用数据。
    更多相关内容
  • Java多线程之线程安全问题

    千次阅读 多人点赞 2022-03-31 11:02:50
    本篇文章介绍的内容为Java多线程中的线程安全问题,此处的安全问题并不是指的像黑客入侵造成的安全问题,线程安全问题是指因多线程抢占式执行而导致程序出现bug的问题。

    ⭐️前面的话⭐️

    本篇文章介绍的内容为Java多线程中的线程安全问题,此处的安全问题并不是指的像黑客入侵造成的安全问题,线程安全问题是指因多线程抢占式执行而导致程序出现bug的问题。

    📒博客主页:未见花闻的博客主页
    🎉欢迎关注🔎点赞👍收藏⭐️留言📝
    📌本文由未见花闻原创,CSDN首发!
    📆首发时间:🌴2022年4月2日🌴
    ✉️坚持和努力一定能换来诗与远方!
    💭参考书籍:📚《Java编程思想》,📚《Java核心技术》
    💬参考在线编程网站:🌐牛客网🌐力扣
    博主的码云gitee,平常博主写的程序代码都在里面。
    博主的github,平常博主写的程序代码都在里面。
    🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!



    封面


    ☁️1.线程安全概述

    ❄️1.1什么是线程安全问题

    首先我们需要明白操作系统中线程的调度是抢占式执行的,或者说是随机的,这就造成线程调度执行时线程的执行顺序是不确定的,有一些代码执行顺序不同不影响程序运行的结果,但也有一些代码执行顺序发生改变了重写的运行结果会受影响,这就造成程序会出现bug,对于多线程并发时会使程序出现bug的代码称作线程不安全的代码,这就是线程安全问题。

    下面,将介绍一种典型的线程安全问题实例,整数自增问题。

    ❄️1.2一个存在线程安全问题的程序

    有一天,老师布置了这样一个问题:使用两个线程将变量count自增10万次,每个线程承担5万次的自增任务,变量count的初始值为0
    这个问题很简单,最终的结果我们也能够口算出来,答案就是10万。
    小明同学做事非常迅速,很快就写出了下面的一段代码:

    class Counter {
        private int count;
        public void increase() {
            ++this.count;
        }
        public int getCount() {
            return this.count;
        }
    }
    
    public class Main11 {
        private static final int CNT = 50000;
        private static final Counter counter = new Counter();
        public static void main(String[] args) throws InterruptedException {
    
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < CNT; i++) {
                    counter.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int j = 0; j < CNT; j++) {
                    counter.increase();
                }
            });
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(counter.getCount());
        }
    }
    

    按理来说,结果应该是10万,我们来看看运行结果:
    线程不安全程序结果
    运行的结果比10万要小,你可以试着运行该程序你会发现每次运行的结果都不一样,但绝大部分情况,结果都会比预期的值要小,下面我们就来分析分析为什么会这样。

    ☁️2.线程加锁与线程不安全的原因

    ❄️2.1案例分析

    上面我们使用多线程运行了一个程序,将一个变量值为0的变量自增10万次,但是最终实际结果比我们预期结果要小,原因就是线程调度的顺序是随机的,造成线程间自增的指令集交叉,导致运行时出现两次自增但值只自增一次的情况,所以得到的结果会偏小。

    我们知道一次自增操作可以包含以下几条指令:

    1. 将内存中变量的值加载到寄存器,不妨将该操作记为load
    2. 在寄存器中执行自增操作,不妨将该操作记为add
    3. 将寄存器的值保存至内存中,不妨将该操作记为save

    我们来画一条时间轴,来总结一下常见的几种情况:

    ⭐️情况1: 线程间指令集,无交叉,运行结果与预期相同,图中寄存器A表示线程1所用的寄存器,寄存器B表示线程2所用的寄存器,后续情况同理。
    情况1
    ⭐️情况2: 线程间指令集存在交叉,运行结果低于预期结果。
    情况2
    ⭐️情况3: 线程间指令集完全交叉,实际结果低于预期。
    情况3
    根据上面我们所列举的情况,发现线程运行时没有交叉指令的时候运行结果是正常的,但是一旦有了交叉会导致自增操作的结果会少1,综上可以得到一个结论,那就是由于自增操作不是原子性的,多个线程并发执行时很可能会导致执行的指令交叉,导致线程安全问题。

    那如何解决上述线程不安全的问题呢?当然有,那就是对对象加锁。

    ❄️2.2线程加锁

    ⚡️2.2.1什么是加锁

    为了解决由于“抢占式执行”所导致的线程安全问题,我们可以对操作的对象进行加锁,当一个线程拿到该对象的锁后,会将该对象锁起来,其他线程如果需要执行该对象的任务时,需要等待该线程运行完该对象的任务后才能执行。

    举个例子,假设要你去银行的ATM机存钱或者取款,每台ATM机一般都在一间单独的小房子里面,这个小房子有一扇门一把锁,你进去使用ATM机时,门会自动的锁上,这个时候如果有人要来取款,那它得等你使用完并出来它才能进去使用ATM,那么这里的“你”相当于线程,ATM相当于一个对象,小房子相当于一把锁,其他的人相当于其他的线程。
    ATM栗子
    你用完就跑路
    在java中最常用的加锁操作就是使用synchronized关键字进行加锁。

    ⚡️2.2.2如何加锁

    synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
    线程进入 synchronized 修饰的代码块, 相当于加锁,退出 synchronized 修饰的代码块, 相当于 解锁

    java中的加锁操作可以使用synchronized关键字来实现,它的常见使用方式如下:

    ⭐️方式1: 使用synchronized关键字修饰普通方法,这样会使方法所在的对象加上一把锁。
    例如,就以上面自增的程序为例,尝试使用synchronized关键字进行加锁,如下我对increase方法进行了加锁,实际上是对某个对象加锁,此锁的对象就是this,本质上加锁操作就是修改this对象头的标记位。

    class Counter {
        private int count;
        synchronized public void increase() {
            ++this.count;
        }
        public int getCount() {
            return this.count;
        }
    }
    

    多线程自增的main方法如下,后面会以相同的栗子介绍synchronized的其他用法,后面就不在列出这段代码了。

    public class Main11 {
        private static final int CNT = 50000;
        private static final Counter counter = new Counter();
        public static void main(String[] args) throws InterruptedException {
    
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < CNT; i++) {
                    counter.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int j = 0; j < CNT; j++) {
                    counter.increase();
                }
            });
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(counter.getCount());
        }
    }
    

    看看运行结果:
    加锁
    ⭐️方式2: 使用synchronized关键字对代码段进行加锁,但是需要显式指定加锁的对象。
    例如:

    class Counter {
        private int count;
        public void increase() {
            synchronized (this){
                ++this.count;
            }
        }
        public int getCount() {
            return this.count;
        }
    }
    

    运行结果:
    加锁
    ⭐️方式3: 使用synchronized关键字修饰静态方法,相当于对当前类的类对象进行加锁。

    class Counter {
        private static int count;
        synchronized public static void increase() {
            ++count;
        }
        public int getCount() {
            return this.count;
        }
    }
    

    运行结果:
    加锁
    常见的用法差不多就是这些,对于线程加锁(线程拿锁),如果两个线程同时拿一个对象的锁,就会产生锁竞争,两个线程同时拿两个不同对象的锁不会产生锁竞争。
    对于synchronized这个关键字,它的英文意思是同步,但是同步在计算机中是存在多种意思的,比如在多线程中,这里同步的意思是“互斥”;而在IO或网络编程中同步指的是“异步”,与多线程没有半点的关系。

    ⭐️synchronized 的工作过程:

    1. 获得互斥锁lock
    2. 从主内存拷贝变量的最新副本到工作的内存
    3. 执行代码
    4. 将更改后的共享变量的值刷新到主内存
    5. 释放互斥锁unlock

    synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,即死锁问题,关于死锁后续文章再做介绍。

    综上,synchronized关键字加锁有如下性质:互斥性,刷新内存性,可重入性。

    synchronized关键字也相当于一把监视器锁monitor lock,如果不加锁,直接使用wait方法(一种线程等待的方法,后面细说),会抛出非法监视器异常,引发这个异常的原因就是没有加锁。

    ⚡️2.2.3再析案例

    对自增那个代码上锁后,我们再来分析一下为什么加上了所就线程安全了,先列代码:

    class Counter {
        private int count;
        synchronized public void increase() {
            ++this.count;
        }
        public int getCount() {
            return this.count;
        }
    }
    
    public class Main11 {
        private static final int CNT = 50000;
        private static final Counter counter = new Counter();
        public static void main(String[] args) throws InterruptedException {
    
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < CNT; i++) {
                    counter.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int j = 0; j < CNT; j++) {
                    counter.increase();
                }
            });
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(counter.getCount());
        }
    }
    
    

    多线程并发执行时,上一次就分析过没有指令集交叉就不会出现问题,因此这里我们只讨论指令交叉后,加锁操作是如何保证线程安全的,不妨记加锁为lock,解锁为unlock,两个线程运行过程如下:
    线程1首先拿到目标对象的锁,对对象进行加锁,处于lock状态,当线程2来执行自增操作时会发生阻塞,直到线程1的自增操作完毕,处于unlock状态,线程2才会就绪取执行线程2的自增操作。
    线程安全时间图
    加锁后线程就是串行执行,与单线程其实没有很大的区别,那多线程是不是没有用了呢?但是对方法加锁后,线程运行该方法才会加锁,运行完该方法就会自动解锁,况且大部分操作并发执行是不会造成线程安全的,只有少部分的修改操作才会有可能导致线程安全问题,因此整体上多线程运行效率还是比单线程高得多。

    ❄️2.3线程不安全的原因

    首先,线程不安全根源是线程间的调度充满随机性,导致原有的逻辑被改变,造成线程不安全,这个问题无法解决,无可奈何。

    多个线程针对同一资源进行写(修改)操作,并且针对资源的修改操作不是原子性的,可能会导致线程不安全问题,类似于数据库的事务。

    由于编译器的优化,内存可见性无法保证,就是当线程频繁地对同一个变量进行读操作时,会直接从寄存器上读值,不会从内存上读值,这样内存的值修改时,线程就感知不到该变量已经修改,会导致线程安全问题(这是编译器优化的结果,现代的编译器都有类似的优化不止于Java),因为相比于寄存器,从内容中读取数据的效率要小的多,所以编译器会尽可能地在逻辑不变的情况下对代码进行优化,单线程情况下是不会翻车的,但是多线程就不一定了,比如下面一段代码:

    import java.util.Scanner;
    
    public class Main12 {
        private static int isQuit;
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                while (isQuit == 0) {
    
                }
                System.out.println("线程thread执行完毕!");
            });
            thread.start();
    
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
            isQuit = sc.nextInt();
            System.out.println("main线程执行完毕!");
        }
    }
    

    运行结果:
    内存可见性问题
    我们从运行结果可以知道,输入isQuit后,线程thread没有停止,这就是编译器优化导致线程感知不到内存可见性,从而导致线程不安全。
    我们可以使用volatile关键字保证内存可见性。
    我们可以使用volatile关键字修饰isQuit来保证内存可见性。

    import java.util.Scanner;
    
    public class Main12 {
        volatile private static int isQuit;
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                while (isQuit == 0) {
    
                }
                System.out.println("线程thread执行完毕!");
            });
            thread.start();
    
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
            isQuit = sc.nextInt();
            System.out.println("main线程执行完毕!");
        }
    }
    

    运行结果:
    volatile

    ⭐️synchronized与volatile关键字的区别:
    synchronized关键字能保证原子性,但是是否能够保证内存可见性要看情况(上面这个栗子是不行的),而volatile关键字只能保证内存可见性不能保证原子性。
    保证内存可见性就是禁止编译器做出如上的优化而已。

    import java.util.Scanner;
    
    public class Main12 {
        private static int isQuit;
        //锁对象
        private static final Object lock = new Object();
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                    synchronized (lock) {
                        while (isQuit == 0) {
    
                        }
                        System.out.println("线程thread执行完毕!");
                    }
            });
            thread.start();
    
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
            isQuit = sc.nextInt();
            System.out.println("main线程执行完毕!");
        }
    }
    

    运行结果:
    synchronized内存可见性

    编译器优化除了导致内存可见性感知不到的问题,还有指令重排序也会导致线程安全问题,指令重排序也是编译器优化之一,就是编译器会智能地(保证原有逻辑不变的情况下)调整代码执行顺序,从而提高程序运行的效率,单线程没问题,但是多线程可能会翻车,这个原因了解即可。

    ☁️3.线程安全的标准类

    Java 标准库中很多都是线程不安全的。这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施。例如,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder。
    但是还有一些是线程安全的,使用了一些锁机制来控制,例如,Vector (不推荐使用),HashTable (不推荐使用),ConcurrentHashMap (推荐),StringBuffer。
    还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的,例如String。

    在线程安全问题中可能你还会遇到JMM模型,在这里补充一下,JMM其实就是把操作系统中的寄存器,缓存和内存重新封装了一下,其中在JMM中寄存器和缓存称为工作内存,内存称为主内存。
    其中缓存分为一级缓存L1,二级缓存L2和三级缓存L3,从L1到L3空间越来越大,最大也比内存空间小,最小也比寄存器空间大,访问速度越来越慢,最慢也比内存的访问速度快,最快也没有寄存器访问快。

    ☁️4.Object类提供的线程等待方法

    除了Thread类中的能够实现线程等待的方法,如join,sleep,在Object类中也提供了相关线程等待的方法。

    序号方法说明
    1public final void wait() throws InterruptedException释放锁并使线程进入WAITING状态
    2public final native void wait(long timeout) throws InterruptedException;相比于方法1,多了一个最长等待时间
    3public final void wait(long timeout, int nanos) throws InterruptedException相比于方法2,等待的最长时间精度更大
    4public final native void notify();唤醒一个WAITING状态的线程,并加锁,搭配wait方法使用
    5public final native void notifyAll();唤醒所有处于WAITING状态的线程,并加锁(很可能产生锁竞争),搭配wait方法使用

    上面介绍synchronized关键字的时候,如果不对线程加锁会产生非法监视异常,我们来验证一下:

    public class TestDemo12 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行完毕!");
            });
    
            thread.start();
            System.out.println("wait前");
            thread.wait();
            System.out.println("wait后");
        }
    }
    

    看看运行结果:
    非法监视器异常
    果然抛出了一个IllegalMonitorStateException,因为wait方法的执行步骤为:先释放锁,再使线程等待,你现在都没有加锁,那如何释放锁呢?所以会抛出这个异常,但是执行notify是无害的。

    wait方法常常搭配notify方法搭配一起使用,前者能够释放锁,使线程等待,后者能获取锁,使线程继续执行,这套组合拳的流程图如下:
    搭配使用

    现在有两个任务由两个线程执行,假设线程2比线程1先执行,请写出一个多线程程序使任务1在任务2前面完成,其中线程1执行任务1,线程2执行任务2。
    这个需求可以使用wait/notify来实现。

    class Task{
        public void task(int i) {
            System.out.println("任务" + i + "完成!");
        }
    }
    
    public class WiteNotify {
        //锁对象
        private static final Object lock = new Object();
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(() -> {
                synchronized (lock) {
                    Task task1 = new Task();
                    task1.task(1);
                    //通知线程2线程1的任务完成
                    System.out.println("notify前");
                    lock.notify();
                    System.out.println("notify后");
                }
            });
            Thread thread2 = new Thread(() -> {
                synchronized (lock) {
                    Task task2 = new Task();
                    //等待线程1的任务1执行完毕
                    System.out.println("wait前");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    task2.task(2);
                    System.out.println("wait后");
                }
            });
            thread2.start();
            Thread.sleep(10);
            thread1.start();
        }
    }
    
    

    运行结果:
    运行结果


    本文总结了多线程中线程安全问题,下一篇文章会介绍多线程的重要实例。

    觉得文章写得不错的老铁们,点赞评论关注走一波!谢谢啦!

    1-99

    展开全文
  • 线程安全的问题

    千次阅读 多人点赞 2022-04-14 18:10:26
    线程安全则主要体现在三个方面: 原子性 可见性 有序性 接下来就对这三个特性进一步说明: 1.1 原子性 原子:分子组成的最小单位。(不可分割) 原子操作不可分割的两层含义: 访问共享变量的原子操作是不...

    目录

    1 .线程安全则主要体现在三个方面:

    1.1 原子性

    1.1.1 Java 实现原子性的两种方式:

    1.2 可见性

    1.3有序性

    1.3.1 重排序

    1.3.2 指令重排序

    1.3.3 存储子系统重排序

    1.3.4 貌似串型语义

    1.3.5 保证内存的访问顺序


    1 .线程安全则主要体现在三个方面:

    • 原子性
    • 可见性
    • 有序性

    接下来就对这三个特性进一步说明:

    1.1 原子性

    •  原子:分子组成的最小单位。(不可分割)
    • 原子操作不可分割的两层含义
      • 访问共享变量的原子操作是不能够交错的
      • 访问(读、写):某个共享变量的操作从其他线程来看。
        • 该操作要么执行完毕。
        • 要么尚未发生。
        • 例如生活中的取款,两个人同时操作一张银行卡时,不可能两个人同时成功。

    1.1.1 Java 实现原子性的两种方式:

    • 😆😆😆😆 第一种:使用锁  
      • 锁的缺点:
      • 多线程编程中 我们⼀般会使⽤各种锁来确保对共享资源的访问和操作,需要共享的数据要对它的访问串⾏化。
        修改共享数据的操作必须以原⼦操作的形式出现才能保证不被其它线程破坏相应数据。
        锁这种机制⽆法彻底避免以下⼏点问题:

        1、锁引起线程的阻塞,对于没有能占⽤到锁的线程或者进程将会⼀直等待锁的占有者释放资源后才能继续。
        2、申请和释放锁的操作增加了很多访问共享资源的消耗。
        3、锁不能很好的避免编程开发者设计实现的程序出现死锁或者活锁可能
        4、优先级反转和所护送怪现象
        5、难以调试
    • 😆😆😆😆第二种:利用处理器的CAS(Compare and Swap比较并操作)指令。
      • CAS:⽐较并操作,解决多线程并⾏情况下使⽤锁造成性能损耗的机制。

      两者比较:

    •  锁具有排它性,保证共享数据在某一时刻只能被以线程执行。
    • CAS 指令直接在硬件(处理器和内存)层次上实现,看做是硬件锁

    1.2 可见性

    在多线程环境中,一个线程对某一个共享变量进行更新之后,后续其它线程可能无法立即读到这个更新的结果这就是线程安全中的另一种形式:可见性(visibility)。

    •  如果一个线程对共享变量更新后,后续访问该变量的其它线程可以读到这个更新之后的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见。
    • 多线程程序因为可见性问题可能会导致其它线程读到了脏数据(过期的数据)。
    package stu.my.cdn.threadsafe;
    
    /**
     * 测试线程的可见性
     */
    public class Test02 {
        public static void main(String[] args) throws InterruptedException {
            MyTask myTask = new MyTask();
            new Thread(myTask).start();
    
            Thread.sleep(6000);
            // 主线程 1 秒后取消执行
            myTask.cancel();
            /*
                可能会出现以下情况:
                    在 main 线程中调用了 myTask.cancel() 方法,把myTask的 toCancel 修改为 true
                    可能存在子线程看不到 main 线程对 toCancel做的修改,在子线程中 toCancel 变量的值一直为 false
                    导致子线程看不到 main 线程对 toCancel 变量更新的原因可能为:
                        1)JIT即时编译器,可能会对 run 方法中的 while循环进行优化为:
                            if(!toCancel){
                                 while(true){
                                    if(doSomething())
                                 }
                            }
                        2) 可能与计算机的存储系统有关,假设分别有两个 cpu 内核运行 main 线程与子线程,
                          一个 cpu 内核无法立即读取另一个cpu 内核中的数据。(运行在子线程的cpu无法立即读取
                          main 线程cpu的数据)
             */
        }
    
        static class MyTask implements Runnable{
            private boolean toCancel = false;
            @Override
            public void run() {
                while(!toCancel){
                    if(doSomething()) break;
                }
                if(toCancel){
                    System.out.println("任务被取消");
                }else{
                    System.out.println("任务正常执行");
                }
            }
    
            private boolean doSomething() {
                System.out.println("执行了某个任务");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return true;
            }
    
            public void cancel(){
                toCancel = true;
                System.out.println("收到 取消线程的消息");
            }
        }
    }

    总的来说,可见性就是,一个线程修改了数据,能让其共享的线程及时的到新数据。 

    1.3有序性

    • 有序性(Ordering):是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另一个处理器运行的其它线程看起来是乱序的(Out of Order)
    • 乱序是指内存访问操作的顺序看起来发生了变化。

    1.3.1 重排序

    • 在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
      • 编译器可能会改变两个操作的先后顺序。
      • 处理器也可能不按照目标代码的顺序执行
      • 这种一个处理器上执行的多个操作,在其它处理器来看的顺序与目标代码指定的顺序可能不一样,这种现象为重排序。
      • 重排序是指编译器处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
      • 可能出现,和可见性一样,不是必然的。

      与内存操作顺序有关的几个概念:

    • 源码顺序:就是源码中指定访问的内存顺序。
    • 程序顺序:处理器上运行的目标代码所执行的内存访问顺序。
    • 执行顺序:内存访问操作在处理器上的实际执行顺序。
    • 感知顺序:给定处理器所感知到的,该处理器即及它处理器的程序访问操作的顺序。

       可以把重排序分为存储子系统重排序子系统重排序 :

    • 指令重排序:主要是由 JIT 编译器,处理器引起的,指程序顺序与执行顺序不一样。
    • 存储子系统重排序:是由高速缓存,写缓冲器引起的,感知顺序与执行顺序不一致。

      JIT 编译器 和 javac 编译器区别:

    • JIT:

      Java 代码的执行过程中,首先进行前端编译,通过编译器将 .java文件 编译成字节码,即 .class文件。
      然后,进行后端编译,将字节码编译成机器码,在此过程中,有一个编译器将热点代码编译成本地平台相关的机器码,并进行优化。 

      javac: javac的任务是将Java源代码语言 (.java文件) 先转化成JVM能够识别的一种语言,然后由JVM将JVM语言再转化成当前这个机器能够识别的机器语言,javac的作用简单来说就是通过一些列的流程之后将.java文件转换为.class文件, javac 编译器位于jdk --> bin -->javac

    • javac:

       在 Java 中提到“编译”,自然很容易想到 javac 编译器将*.java文件编译成为*.class文件的过程,这里的 javac 编译器称为前端编译器,其他的前端编译器还有诸如 Eclipse JDT 中的增量式编译器 ECJ 等。相对应的还有后端编译器,它在程序运行期间将字节码转变成机器码(现在的 Java 程序在运行时基本都是解释执行加编译执行),如 HotSpot 虚拟机自带的 JIT(Just In Time Compiler)编译器(分 Client 端和 Server 端)。另外,有时候还有可能会碰到静态提前编译器(AOT,Ahead Of Time Compiler)直接把*.java文件编译成本地机器代码,如 GCJ、Excelsior JET 等,这类编译器我们应该比较少遇到。

    1.3.2 指令重排序

    • 在源码顺序与程序顺序不一致,或者 程序顺序与执行顺序不一致时的情况下,我们就说发生了指令重排序。

    • 指令重排序是一种动作,确实对指令的顺序做了调整,重排序的对象指令。

    • javac 编译器一般不会执行指令重排序,而 JIT 编译器可能执行指令重排序。

    • 处理器也可以执行指令重排序,使得执行顺序与程序顺序不一致。

    • 指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程程序出现非预测的结果。

    1.3.3 存储子系统重排序

    • 存储子系统是指写缓冲器与高速缓存:

    • 高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配设计的一个高速缓冲

    • 写缓冲器(Store buffer,Write buffer)用来提高写高速缓冲操作的效率。

    • 即使处理器严格按照程序执行两个内存访问操作,在存储子系统的作用下,其它处理器对这两个操作的感知顺序和程序顺序一致,即使这两个操作的顺序看起来像是发生了变化,这种现象称为存储子系统重排序。

    • 存储子系统重排序并没有真正的对程序执行顺序进行了调整,而是造成一种指令执行顺序被调整的现象。

    • 存储子系统重排序对象是内存操作的结果。

    1.3.4 貌似串型语义

    • JIT 编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序,给单线程程序造成一种假象----------指令是按照源码的顺序执行的,这种假象称为貌似串型语义。并不能保证多线程环境程序的正确性。

    • 为了保证貌似串型语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的数据才会被重排序。如果两个操作(指令)访问同一个变量,其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系(Data dependency)

      • 如:

        • x=1; y=x+1; 后一条语句的操作数包含前一条语句的执行结果;

        • y = x; x = 1 ; 先读取x变量,在更新 x 变量的值。

        • x = 1; x = 2; 两条语句同时对一个变量进行写操作。

    • 如果不存在数据依赖关系则可能重排序

      • 如:

        • double price = 45.8;

        • int quantity = 10;

        • double sum = price * quantity;

    • 存在控制依赖关系的数据允许重排。一条语句(指令)的执行结果会决定另一条语句(指令)能否执行,这两条数据(指令)存在控制依赖关系(Control Dependency)。如在 if 语句中允许重排,可能存在处理器先执行 if 代码块,在判断 if 条件是否成立。

    1.3.5 保证内存的访问顺序

    • 可以使用 volatile 关键字
    • 使用 synchronized 关键字实现有序性。

    两者比较:😁😁😁😁

    volatile与sychronized的比较:

    1)volatile只能修饰变量sychronized可以修饰变量、方法,以及代码块

    2)多线程访问volatile修饰的变量时不会发生阻塞,而访问sychronized修饰的内容时,线程会发生阻塞;

    3)volatile只能保证可见性,而sychronized保证了原子性,间接保证了可见性,因为它会将本地缓存和主内存的数据同步。

    感谢阅读~~~希望对你有帮助~~~

    展开全文
  • 多线程带来的风险——线程安全

    千次阅读 多人点赞 2022-05-30 10:14:30
    文章目录多线程带来的风险——线程安全一、何为线程安全1.1 线程不安全的原因二、多线程带来的风险——线程安全2.1、 原子性2.1.1如何让线程安全?2.1.2、 synchronized (这里简单了解下,后续祥细说)2.2、内存...

    多线程带来的风险——线程安全

    一、何为线程安全

    ​ 编写多线程代码的时候,如果当前代码中因为多线程随机的调度顺序,导致程序出现了BUG,就称之为“线程不安全”,如果我们自己写的多线程代码,不管系统按照啥样的随机情况来调度,也不会导致出现 BUG,就称之为“线程安全”。

    这里的多线程安全跟黑客无关emmmm…,黑客一般都是跟 网络安全 挂钩的。

    • 实现一个经典线程不安全的案例,两个线程进行变量累加
    
    //线程不安全案列,两个线程进行变量累加
    
    class Count {
        public int count = 0;
    
        public void increase() {
            count++;
        }
    }
    
    public class Demo11 {
        private static Count count = new Count();
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    count.increase();
                }
            });
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    count.increase();
                }
            });
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            System.out.println(count.count);
        }
    }
    

    这里相加的结果应该是 10w,但是运行的结果大概率不是10w,而是在[5w,10w]之间,10w大概率很少出现。显然这就是一个bug,这种bug是因为线程调度的随机性导致的。

    那么在这个过程中 count++ 到底干了给啥???

    1.1 线程不安全的原因

    count++ 操作其实是三个步骤:

    1、把内存 中的值,读到 CPU 的寄存器中 (load)

    2、把寄存器中的 0 给进行 +1 操作 (add)

    3、把寄存器中的 1 给写回到 内存中 (save)

    其中这也是CPU的三条指令。

    如果是两个线程,同时操作这个count,此时由于线程之间的随机调度的过程就可能产生不同的结果。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LlG41j0X-1653876849286)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220524093631935.png)]

    按照上面图形的顺序才是应该正确执行步骤,但是线程调度是随机的。所以就会发生各种的情况。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J6S9TwUi-1653876849287)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220524094248859.png)]

    这些都是可能出现的情况,但是不止这三种,这些情况都是线程不安全的!!!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOijOdkz-1653876849288)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220524102622625.png)]

    由此可见,这里情况是两个线程分别自增1,一共自增2次,但是内存还是1,两个线程分别都读到了0,接下来分别自增,得到的都是1,再往内存里写的时候,就都是1,这就是明显的bug。所以这里的线程调度是不安全的。

    在操作系统中,调度线程过程中是随机的,就是使t1,t2和内存这三个操作,之间穿插多种情况。我这上面画的几种都是可能出现的情况,当然还有很多的情况我就不一一画出来了,并且那种情况各自出现多少次,这些都是随机的!

    只有两种情况是正确的:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-crO6dM0m-1653876849288)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220524103836252.png)]

    有图观察可得:上述串行执行的情况,出现的次数,就直接决定了最终的执行结果。

    极端情况,如果5w次循环,都是出现串行的情况,最终结果就是 10w

    极端清况,如果5w次循环,一次串行的情况都不出,此时最终结果就是5w

    所谓的极端情况,也是小概率事件。


    综上所示:就是修改了共享数据,上述代码不安全中,就涉及到了多个线程针对 count 变量来修改,而此时 count 也是一个多个线程共享的数据。在共享的数据中就会出现多种随机情况,就会导致线程不安全。

    ​ 正如每个人都会有隐私,想象成一段代码是一个房间,这个房间没有锁,那么任何人都可以进入,假设这是一个女孩子的房间,那么这个时候一个男孩子进入了这个女孩子的房间,这样女孩子的隐私就会被侵犯,这种是万万不可的(排除这两人是夫妻hhh…) 。

    二、多线程带来的风险——线程安全

    2.1、 原子性

    什么是原子性??

    刚才男孩闯入女孩的房间就不具备原子性,那么给房间加一把锁,女孩进入房间把门锁上,其他人进不来,这样就保证了这段代码的原子性。

    也就是说在一个操作中要么全做完,中途是不能暂停或者再次调度,要么就等另外一个操作执行完了,再进行;只要一个完整的操作不被 打扰调度暂停,那么这就是原子性。

    这种现象也可以叫做同步互斥,表示操作是互相排斥的。

    2.1.1如何让线程安全?

    ​ 如何让线程安全??根据上面的例子就是给这个房间加锁!!

    通过加锁的操作,就是把上述 “无序” 的过程 给变成 “有序” ,把上述 的 “随机” 变成 “确定”。

    2.1.2、 synchronized (这里简单了解下,后续祥细说)

    java 给线程加锁的方案有多种,其中最常用的方案,就是 synchronized (这个也是java中内置的关键字)。

    中文翻译为同步,也可以理解为“互斥”。

    class Count {
        public int count = 0;
    
        synchronized public void increase() {
            count++;
        }
    }
    

    这时候在 increase ()方法中加上 synchronized 关键字之后,相当于 进入increase()方法,就加锁,出了这个方法 就解锁。

    那么 synchronized 关键字是如何解决线程安全的问题??

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MjidHe7H-1653876849289)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220524114824902.png)]

    由上图可知:

    基本操作就是先等一个线程执行完,解锁之后,在进行下一个线程执行。可以理解成排队上厕所,厕所里面有人,门是锁着的下一个人拉不开门,只能等厕所里面的人上完厕所解锁出来,下一个人才能去上厕所。

    • 线程不安全的原因

    这就也相当于是多个线程在修改一个变量。如果是一个线程修改一个变量,可行;如果是两个线程读这个变量,可行;如果是两个线程修改两个变量,可行;(两个变量相当于两个不同的空间)。

    总结:加锁 (synchronized),进入 synchronized 修饰的方法中,就会先加锁,出了方法就会解锁,如果当前有线程占用了这把锁,有其他的线程,那么其他的线程尝试占用这把锁,此时就会出现阻塞等待。

    ❓❓❓❓提出疑问?

    实现并发编程,本来就是提高效率的,但是加锁过后,执行结果对了,但是并发性就低了,速度效率也降下来了。

    但是做成多线程这个东西还是有意义的,两个并发线程,可能各自要完成的任务有很多,也有不少工作能够并行进行的,整体来说,多线程还是有意义的。

    2.2、内存可见性 ☆

    public class Demo12 {
        private static int flg = 0;
        public static void main(String[] args) {
            Thread t = new Thread(()->{
                while(flg == 0) {
                    // 先啥也不写,无限循环。
                }
                
                System.out.println("t线程结束");
            });
            t.start();
    
            Scanner scan = new Scanner(System.in);
            System.out.println("请输入一个数");
            flg = scan.nextInt();
            System.out.println("main 线程结束");
    
        }
    }
    

    按照普通人的逻辑,这段代码在用户输入一个数字时,这个数就会赋值给flg,flg!=0,从而跳出循环。打印“t线程结束”。但是真正运行的Consequences 并不是这样。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wp5E5QyG-1653876849291)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220526195005652.png)]

    这个场景就是设置一个变量,一个线程无限循环读取这个变量的值,另一个线程会在一定时间之后,修改这个变量的值。但是这里的 t 线程并没有读到修改后变量的值。这种问题就是 内存可见性

    • 引起内存可见性的原因。
    这里就要补充一个概念,java编译器中的优化功能:
    	程序猿在编译器中写的代码,编译器并不会逐字翻译,编译器会保证原有逻辑不变的情况下,动态调整要执行的指令内容,这个调整过程,是需要保证原有逻辑不变的,从而这种调整就会提高程序运行的效率。
    
    可惜😥😥😥 得是在多线程的场景下,编译器的判定可能就会存在误差,优化的操作就可能会影响原有的逻辑
    

    编译器优化功能不仅是java,许多主流的语言也是会有编译器优化的功能,因为开发编译器的大佬并不信任我们能写出高效的代码emmmm… 所以让编译器来动态修改我们写的代码,这种优化就会让程序快得非常多,可能是倍数的 promote。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-re8H5LYM-1653876849293)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220526201705539.png)]

    这样的优化也将导致了后面进行 变量 修改的时候 t 线程感知不到,所以就会一直认为 flg == 0 ,然后一直循环,t线程就不会退出。

    所谓的内存可见性就是:一个线程改了内存里面的值,但是另一个线程看不见你改的值。

    load读的是内存,后面的比较就是在寄存里面比较,而内存的速度要比寄存器的速度要慢 3~4 个数量级。

    2.2.1、内存可见性的解决方案。(volatile)

    解决方案有两种:

    1、使用 synchronized ,加上 synchronized,编译器就会禁止在 synchronized 内部的代码产生上述的优化。

    2、还可以使用另一个关键字,volatile.

    volatile,用这个关键字修饰对应的变量就行,有了这个关键字,编译器在进行优化的时候,识别到了这个关键字,就知道会禁止进行上述的 读内存 的优化,会保证每次都重新从内存读。

    这两个方法,保证了内存可见性,禁止了编译器自己相关的优化。

    • 第一种改法
    Thread t = new Thread(()->{
                while(true) {
                    synchronized(Demo12.class) {
                        if (flg != 0) {
                            break;
                        }
    
                    }
                }
                System.out.println("t线程结束");
            });
            t.start();
    
    • 第二种改法
    private static volatile int flg = 0;
    

    2.3、指令重排序

    也是跟编译器优化存在关联(一种优化手段)。

    触发指令重排的前提也是要保证代码原逻辑不变;指令重排,就是保证原有逻辑不变,调整了程序指令的执行顺序,从而提高效率。

    单线程环境下,这里的判定比较准,

    如果是多线程环境下,这里的判定就不太准了

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4uDBIaY-1653876849295)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220526223433793.png)]

    这上面的图是单线程的情况,但是有多个线程的时候,就会判断优化失误;

    解决办法也是使用关键字 synchronized ,编译器对于 synchronized 内部的代码非常的谨慎,不会随便乱优化。

    三、synchronized 具体使用方法 ☆

    三个方面起到的效果:

    1、互斥 (核心),也就是阻塞等待

    2、保证内存可见性

    3、禁止指令重排序

    2,3 都是提醒编译器能够优化的谨慎一点

    具体的使用方法:

    1、在一个普通方法上使用synchronized,进入方法是加锁,出方法就是解锁,锁对象相当于this。

    2、加到一个代码块,需要手动的指定一个 “锁对象”。

    ​ 锁对象可以手动指定锁对象 this,也可以指定其他对象作为锁对象;java中,任何一个继承自 Object 类的对象,都可以作为锁对象。(synchronized 加锁操作,本质上是在操作 Object 对象头中的一个标志位)

    3、加到一个 static 方法上,此时相当于指定了当前的 类对象,为锁对象。

    类对象里面包含了这个类中的一些关键信息,这些关键信息就支撑了java的反射机制,类对象 和 普通对象一样,也是可以作为被加锁的对象。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wpOdbor-1653876849296)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220527201904284.png)]

    4、两个线程针对同一个对象加锁,才会产生竞争;两个线程针对不同对象加锁,不会产生竞争。

    可以看成上厕所的时候厕所里面人满了,需要排队等待,直到在锁里面的人解锁出来下一个人才能进去,此时就形成了锁锁竞争;如果厕所里面还有空的厕所间,下一个人就直接进入空的厕所间,他们就不会产生竞争关系;在同一个空间里面就是竞争,在不同空间就没有竞争。

    在加锁的时候,必须要明确当前是针对啥来上锁的!!!只有说针对指定的对象上锁之后,此时多个线程尝试操作相关的对象才会产生竞争关系!!!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7M3HF3g0-1653876849297)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220527204451395.png)]

    3.1 可重入锁

    是 synchronized 重要特性!

    如果 synchronized 没有 可重入锁,那么就会出现 “死锁” 的情况。

    class Count {
        public int count = 0;
        synchronized public void increase() {
            synchronized (this) {
                count++;
            }
        }
    }
    

    可能会经常写出这种情况,就是针对同一个对象 Count ,加锁了两次,没有可重入的功能,就会出现 “死锁”。

    按照之前的理解,第二次加锁的时候是会阻塞等待的,等待第一次加锁把锁释放到了,才能获取到第二把锁,但是释放第一把锁使用该线程里面的代码完成的,这个时候就僵住了。

    为了避免这种情况,java大佬们就设计出来 可重入锁,针对同一把锁可以多次加锁,不会有负面效果。

    锁中会持有两个信息:

    1、当前这个锁被那个线程给持有;

    2、当前这个锁被加锁了几次;

    当 t 线程加锁后,当前这个锁就是 t 持有的,后续再次进行加锁操作,并不会真正的加锁,而只是修改计数(1->2);

    后续往下执行的时候,出了 synchronized 代码块,就触发一次解锁,这里的解锁也不会真正的解锁,而是计数 -1 操作;

    在外层方法执行完了之后,再次解锁,再次计数 -1 ,计数减成0了,才真正的进行了解锁。

    但是在操作系统原生提供加锁相关的api中就是不可重入锁了。

    3.2 死锁

    死锁不仅仅只会出现我们上述的情况,针对同一把锁加锁多次,也会有另外一种经典的情况。(哲学家们就餐的问题)

    1个线程,1把锁;

    2个线程,2把锁;

    N个线程,M把锁;

    我这里是举了个差不多的栗子:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ysDaucH-1653876849298)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220527213054369.png)]

    这种僵住的情况也就是 “死锁”,N个线程,M把锁;这也是一种“死锁”的情况。


    因为多线程有很多的陷阱,同时也引出了面试中的常见问题:某个集合类是否是线程安全的??

    • ArrayList
    • LinkedList
    • HashMap
    • TreeMap
    • HashSet
    • TreeSet
    • StringBulider

    以上属于线程不安全!! 注意这些对象在多线程中谨慎使用,尤其是一个对象被多个线程修改的时候。

    • Vector
    • HashTable
    • ConcurrentHashMap
    • StringBuffer
    • String (注意)

    以上都是线程安全的,前面都带有 synchronized 关键字,更加放心的在多线程环境下使用;注意这里的 String 比较特殊,因为 String 是不可变对象,不能修改,因此就不能在多线程中修改同一个 String 了。


    ❓❓❓❓提问:

    synchronized 和 volatile 两个有啥区别?

    synchronized:原子性,内存可见性,指令重排序

    volatile:内存可见性。


    3.3 JMM

    JMM => java Memory Model java 存储模型(内存模型)。

    JMM 就是 CPU 的寄存器,以及 内存 之间这样的一套模型,java只是取了一个抽象的名字 JMM

    JMM中把CPU的寄存器这部分存储称为 “工作内存”(work memory),memory理解成存储更好,因为是CPU的寄存器。

    JMM中把正常的内存称之为 ”主内存“(main memory),这也是理解成存储会更好;

    在代码中执行一些操作,例如前面的 count++,就是把 主内存 中的数据,拷贝到 工作内存 ,然后 工作内存自增,再次拷贝到 主内存 中。这里只是说法概念不一样,但是逻辑是一样的;JMM相当于概念性的东西。

    面试中问到了 内存可见性 问题,可以从两个维度来回答 :一、CPU/ 内存 这个角度来;二、也可以从JMM 的 主内存/ 工作内存 这个角度来回答。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7IQiH7rX-1653876849299)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220528222533272.png)]

    3.3.1缓存

    缓存是属于CPU上面的:

    1、缓存的容量比内存小 1-2 个数量级

    2、缓存的容量比寄存器要大,

    3、缓存的速度比内存快

    分为L1(一级内存),L2,L3(三级内存),想对缓存而言,L1最小最快,L3最大最慢。

    为什么会出现缓存??

    CPU的工艺和性能要比内存更好,假如CPU每秒能读10条指令,但是内存每秒是能读1条指令。而CPU是要通过内存才能读取指令的,由于内存的效率低,就拉低了CPU的性能,每秒只能读一条指令。因此CPU就大才小用了!怎么解决??

    1、让内存的工艺跟CPU差不多,但是代码的后果就是内存的价格成本极高,可以理解是8内存100万…😕😕😕,pass

    2、就是在CPU和内存中间有一个缓存(Cache),而 Cache 的速度比内存快,所以,CPU在请求指令的时候同时请求的是 Cache和内存,由于 Cache 比内存快,响应的结果就会先给CPU,而 Cache 里面的东西就是先预先下载好CPU常用的指令,响应的速度更快。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXo9ykKr-1653876849299)(D:\常用文件夹\资料信息图片\Typora-image\Image\202205021806311.png)]

    缓存顾名思义也是日常生活中的缓存,其实就是把CPU常用的指令缓存下来从而提升速度,尽可能以小的代价,实现尽可能高的性价比。当然容量大,指令的命中率就越高,这里的命中率始终指的是 Cache。

    3.4 wait和notify

    多线程的调度过程是充满随机性的,系统源码是改变不了的,所以系统层面上不能解决问题。

    但是在实际开发中我们希望合理的协调多线程之间的先后顺序。

    通过 wait和notify 机制,来对多线程之间的执行顺序,做出一定的控制。

    当某个线程调用 wait 之后,就会阻塞等待。

    直到其他某个线程调用 notify 把这个线程唤醒为止。

    public class Demo14 {
        public static void main(String[] args) throws InterruptedException {
            Object o = new Object();
            
            // 这里会跳出 java.lang.IllegalMonitorStateException 的错误。
                //System.out.println("等待之前");
                //o.wait(); // 这个线程阻塞等待。
                //System.out.println("等待之后");
    
            // 正确代码
            synchronized (o){
                System.out.println("等待之前");
                o.wait(); // 这个线程阻塞等待。
                System.out.println("等待之后");
            }
        }
    }
    

    上面的这个代码 o线程 调用了 wait 就会一直等。直到notify唤醒才会继续进行。

    这里的必须要加 synchronized ,不然后报错, 出现Exception in thread "main" java.lang.IllegalMonitorStateException 的错误,Monitor -> 就是监视器,synchronized也可以叫做监视器锁。

    wait 这个方法里面会做三件事情:
    1、先针对 o 解锁。(应该加了锁才能解锁)
    2、进行等待(等待通知的到来)
    3、当通知到来之后,就会被唤醒,同时尝试重新获取到锁,然后再继续执行。
    

    正因为 wait 里面做了这几件事情,所以 wait 需要搭配 synchronized 来使用。

    public class Demo15 {
        private static Object loker = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Thread waiter = new Thread(()->{
                while(true) {
                    synchronized (loker){
                        System.out.println("wait 开始");
    
                        try {
                            loker.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("wait 结束");
                    }
                }
            });
            waiter.start();
    
            Thread.sleep(3000);
    
            Thread notifier = new Thread(()->{
                synchronized(loker) {
                    System.out.println("notify 之前");
                    loker.notify();
                    System.out.println("notify 之后");
                }
            });
            notifier.start();
        }
    }
    

    运行结果:

    wait 开始
    notify 之前
    notify 之后
    wait 结束
    wait 开始
    

    从运行结果可以看出,wait开始,进行阻塞等待,notify唤醒,线程继续,后面又循环,线程开始,直到有 notify 唤醒为止线程才继续。

    notify 也是 Object 类的方法,那个对象调用了wait,就需要那个对象调用 notify 来唤醒。

    o1.wait(), 就需要o1.notify 来唤醒。

    o1.wait(),使用 o2.notify,没效果。

    notify 同样搭配 synchronized 来使用;如果多个线程都在等待,调用一次 notify ,只能唤醒其中的一个线程,具体唤醒那个线程就是系统随机的了,如果没有任何线程等待,调用notify,不会有副作用。

    notify 也是用必要加锁的,notify 本质上也是在针对 locker 对象里面的对象头进行了修改状态,需要保证先加上锁,在进行其他修改。(属于java的特殊要求)

    总结:有了 wait 和 notify 机制,就可以针对多个线程之间的顺序进行一定的修改了。

    除此之外,java中还有个 notifyAll 的操作,一下子全部唤醒,唤醒之后,这些线程再尝试竞争这同一个锁,唤醒全部,这些线程尝试竞争锁,然后按照竞争成功的顺序,依次往下执行。notify的话就是唤醒一个,其他线程仍然在wait中阻塞。

    从而也引出面试题:wait 和 sleep 的对比
    一个是线程之间的通信,一个是让线程阻塞一段时间;
    总结:
    1.wait 需要搭配 synchronized 使用,sleep不需要;
    2、wait 是Object 的方法 sleep 是 Thread 的静态方法;
    

    这篇帖子重点介绍了线程安全的问题,面试高频考点,是整个多线程中最重要的关键点!!!也是概念居多(八股文),代码较少,下一篇会写关于多线程的案例,代码会 enhance 点。

    铁汁们,觉得笔者写的不错的可以点个赞哟❤🧡💛💚💙💜🤎🖤🤍💟,收藏关注呗,你们支持就是我写博客最大的动力!!!!

    展开全文
  • SimpleDateFormat类不是线程安全的根本原因和解决方案,冰河吐血整理,建议收藏!!
  • 一、为什么HashMap线程安全? 1、JDK1.7 扩容引发的死循环和数据丢失 (1).当前jdk1.7版本的HashMap线程安全主要是发生在扩容函数中,其中调用了HshMap的transfer()方法 //jdk 1.7的transfer方法,HashMap的扩...
  • Java之线程安全问题浅析

    千次阅读 2022-04-13 16:47:18
    在java开发中确保线程安全已成为基本要求,线程安全就是指某段代码在多线程环境下能够正确的执行,不会出现数据不一致的情况,反之就是非线程安全。 目前解决线程安全的方式有: 线程安全类,如AtomicInteger 加锁...
  • C# 线程安全

    万次阅读 2021-09-15 20:58:56
    System.Collections.Concurrent 命名空间下提供多个线程安全集合类,只要多个线程同时访问集合,就应使用这些类来代替 System.Collections 和 System.Collections.Generic 命名空间中的相应类型。 但是,不保证通过...
  • 集合线程安全

    千次阅读 2022-04-08 15:51:44
    集合线程安全 常用的集合类型如ArrayList,HashMap,HashSet等,在并发环境下修改操作都是线程不安全的,会抛出java.util.ConcurrentModificationException异常,这节主要记录如何在并发环境下安全地修改集合数据。 ...
  • Java 基础 —— 线程安全

    千次阅读 2021-05-18 23:48:34
    一、线程安全问题 线程安全 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的...
  • 线程安全问题,线程状态细化,synchronized的用法
  • 什么是线程安全?如何保证线程安全

    万次阅读 多人点赞 2019-05-27 23:22:44
    什么是线程安全 参考: 《Java并发编程实践》中对线程安全的定义: 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作...
  • 线程安全问题(面试常考)

    千次阅读 多人点赞 2022-04-03 23:09:16
    线程安全是面试中最常考的问题之一,本篇文章从n++,n--例子引入,解释了为何发生线程不安全问题,以及从多方面考虑如何解决线程不安全问题
  • 线程安全 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。 比如一个 ...
  • 单例模式中的线程安全问题

    千次阅读 多人点赞 2022-04-04 15:34:24
    单例模式是面试中设计模式里考的最多的一个,本文就单例模式中的饿汉模式和懒汉模式中的线程安全进行说明,以及分别使用synchronized和双重校验锁使懒汉模式在多线程环境下为线程安全
  • 三种线程安全的List

    万次阅读 多人点赞 2021-05-30 12:10:45
    在单线程开发环境中,我们经常使用ArrayList作容器来存储我们的数据,但它不是线程安全的,在多线程环境中使用它可能会出现意想不到的结果。 多线程中的ArrayList: 我们可以从一段代码了解并发环境下使用ArrayList...
  • 主要用于弄清楚为什么会有线程安全与非线程安全的不同语言版本,以及这种情况是不是一直存在,是否有解决统一的办法吗~
  • 线程安全的List

    千次阅读 2022-02-10 16:29:29
    获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式 读多写少的情况下,推荐使用CopyOnWriteArrayList方式 读少写多的情况下,推荐使用Collections....
  • 线程安全的问题?2.线程安全产生的根源?3.原子操作?4.线程锁---互斥锁? 1.线程安全的问题? 因为线程之间存在资源竞争的情况,也就是说一个全局变量经过多个线程的共同操作,最终的结果出出现异常情况,这就是...
  • 深耕Java多线程 - 线程安全问题

    千次阅读 2022-02-23 16:28:31
    什么是线程安全和线程不安全?2. 自增运算为什么不是线程安全的?3. 临界区资源和竞态条件 1. 什么是线程安全和线程不安全? 什么是线程安全呢?当多个线程并发访问某个Java对象时,无论系统如何调度这些线程,也...
  • ConcurrentHashMap是如何保证线程安全

    千次阅读 多人点赞 2019-09-24 09:55:40
    ConcurrentHashMap是如何保证线程安全的 之前分析过HashMap的一些实现细节,关于HashMap你需要知道的一些细节, 今天我们从源码角度来看看ConcurrentHashMap是如何实现线程安全的,其实网上这类文章分析特别多,秉着...
  • 线程安全性的基本概念

    千次阅读 2022-04-12 11:20:31
    线程安全性       我们总是说要编写线程安全的代码,有时候也会讨论某个类是不是线程安全的。那到底什么是线程安全性呢?       网上有很多说法:可以...
  • Qt-可重入性和线程安全 文章目录Qt-可重入性和线程安全一、写在前面二、可重入性三、线程安全四、注意事项 一、写在前面 ​ 在本篇文章中,术语"可重入性"和"线程安全"被用来标记类与函数,表明类与函数在多线程应用...
  • 线程 安全问题

    千次阅读 2021-12-17 09:33:11
    一、线程安全问题 ①当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。 ②临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性 ③原子操作:不可分割的多步操作,被...
  • 单例模式的线程安全

    千次阅读 2021-12-31 20:01:53
    单例对象应该允许多线程访问,确保单例对象是线程安全的十分有必要。 单例由于其生命周期特点,一般被实现为指针对象或静态对象,下面将分别讨论这两种情况的线程安全实现。 静态指针变量 下面实现一个基本的单例...
  • Spring中的线程安全

    千次阅读 2022-01-27 16:11:45
    spring线程安全
  • 多线程——保证线程安全

    千次阅读 2021-09-09 10:07:04
    目录多线程——保证线程安全含义如何保证线程安全具体方法volatile关键字保证可见性禁止重排序synchronized关键字保证原子性防止死锁原因后果检查死锁解决方法 不积跬步,无以至千里;不积小流,无以成江海。要沉下...
  • C#学习笔记之线程安全

    千次阅读 2021-11-30 09:08:23
    线程安全:就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程同步技术:是指多线程...
  • 众所周知,多线程会造成线程安全问题,那么多线程为什么会导致线程安全问题呢? 一:首先了解jvm内存的运行时数据区 1.堆区:存储对象实例(和实例变量),数组等 2.java虚拟机栈(方法·栈),存放方法声明,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,057,372
精华内容 422,948
关键字:

线程安全

友情链接: FreeMarker使用手册.rar