多线程线程安全

线程安全

概念:

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境下应该的结果,则说这个程序是线程安全的

要考虑多个线程并发并行执行:多个线程之间的代码都是随机执行的 =》任何情况下如果存在不符合预期结果的,都是不安全的

public class Test {
    static int n = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for(int i=0;i<10000;i++)
                    n++;
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++)
                    n++;
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(n);
    }
}

结果显示是随机的,为什么不安全?涉及到多个线程对n的修改

什么情况产生线程不安全?

多个线程执行共享变量的操作:

  1.  都是读:使用值进行判断,打印等(不存在线程安全)
  2.  至少一个线程写:线程不安全

线程不安全的原因?

  • 原子性:多行指令是不可拆分的最小执行单位,就具有原子性

注意:Java中,某些特殊的代码,Java代码只有一行,但编译为class字节码,或jvm翻译字节码为机器码,可能会有多行;

典型:

n++,n--,++n,--n =》 1. 从内存读取到cpu的寄存器 2.修改 3.写回内存

new操作 等

  • 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到

Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型。

目的:屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 "工作内存" (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存

因为每个线程有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的副本。所以当线程1的工作内存中的值修改时,线程2工作内存的值不一定回及时变化

为什么有这么多的内存?

这里的内存,为Java的抽象叫法;

  • 主内存才是硬件角度的内存,
  • 工作内存指cpu的寄存器和缓存器

为什么要来回拷贝?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍)

  • 代码顺序性(有序性):执行字节码指令或执行机器码指令,都可能为了提高执行效率(不能重排序有前后依赖关系的指令),使用指令重排序的方式来执行

如何解决线程不安全问题?

设计多线程代码原则:满足线程安全(底线)的前提下,尽可能的提高执行效率

  • 某个线程对共享变量的写操作,可以加锁来保证线程安全;

加锁:解决以上不安全三个原因:原子性,可见性,有序性;

Java中两种方式:

    1. synchronized 关键字:申请对给定的Java对象,对象头加锁
    2. Lock:这是一个锁的接口,它的实现类提供了锁这样的对象,可以调用方法来加锁/释放锁
  • 若某个线程,对共享变量是读操作,使用volatile 关键字保证线程安全

volatile :修饰变量(变量的读操作,本身具有原子性)

只保证可见性,有序性,结合后满足线程安全

如果对共享变量的写操作,不依赖任何共享变量,也可以使用volatile保证线程安全

synchronized 关键字-监视器锁(monitor lock)

synchronized 的特性

原理:多个线程对同一个对象进行加锁操作,具有同步互斥的作用/效果;(基于对象头加锁操作,满足原子,可见,有序)

  • 互斥(synchronized的底层是使用操作系统的mutex lock实现的.)原子性

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

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

Java对象头:

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 "锁定" 状态(类似于厕 所的 "有人/无人").

如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.

如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队

理解 "阻塞等待":

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁.

注意:

上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也 就是操作系统线程调度的一部分工作.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能 获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

  • 刷新内存 可见性

synchronized 的工作过程:

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

所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.

  • 有序性:某个线程执行一段同步代码,不论如何重排序,过程中不可能有其他线程执行指令。

  • 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题

理解 "把自己锁死"

一个线程没有释放锁, 然后又尝试再次加锁.

//第一次加锁成功
lock();
//第二次加锁,锁已被占用,阻塞等待
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 =二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会 死锁.

这样的锁称为 不可重入锁

原理:在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

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

synchronized 使用

原理:多个线程对同一个对象进行加锁操作,具有同步互斥的作用/效果;(基于对象头加锁操作,满足原子,可见,有序)

synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.

语法一:同步代码块

语法二:同步实例方法

语法三:同步静态方法

package 线程安全;

public class SynchronizedUse {
    //语法二:
    public synchronized void t1(){
        //代码;
    }
    //等同于:
    public void t1_2(){
        //this:当前线程(谁(当前类的一个实例对象)调用该实例方法,this就是谁)
        synchronized (this){
            //整个方法内部代码代码;
        }
    }

    public static void main(String[] args) {
        //任意对象,包括类对象
        //语法一:同步代码块
        Object 任意对象 = new Object();
        synchronized (任意对象){
            //代码;
        }
        synchronized (SynchronizedUse.class){
        }
        //语法二:同步实例方法

        //语法三:静态同步方法
    }
    
    //语法三:
    public static synchronized void t2(){
        
    }
    //等同于
    public static void t2_2(){
        synchronized (类对象.class){
        }
    }
}

注意:如果不同的线程,申请不同对象的锁,没有同步互斥的作用

Java标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

  • ArrayList
  • LinkedList(区别)
  • HashMap(理解)
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder(区别)

但是还有一些是线程安全的. 使用了一些锁机制来控制.

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

不加锁,但安全(不涉及修改)

  • String

volatile 关键字

语法:修饰某个变量(实例变量,静态变量)

private static volatile boolean flag = false;

作用:

  • 保证可见性:多个线程对同一变量的操作,具有可见性

代码在写入 volatile 修饰的变量的时候,

  • 改变 线程工作内存 中 volatile变量副本 的值
  • 将改变后的 副本的值 从 工作内存 刷新到 主内存

代码在读取 volatile 修饰的变量的时候,

  • 从 主内存 中 读取 volatile变量 的最新值 到线程的工作内存中
  • 从 工作内存 中 读取 volatile变量 的副本

volatile,强制读写内存,速度慢了,但数据变得准确

示例:

在这个代码中

  • 创建两个线程 t1 和 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束
public class VolatileUse {
    static class Counter{
        //没有 volatile 预期效果无法达到
        public volatile int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            while(counter.flag==0){
                //System.out.println("20");
            }
            System.out.println("结束");
        });
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("input a number : ");
            counter.flag = sc.nextInt();
        });

        t1.start();
        t2.start();
    }
}
  • 禁止指令重排序,建立内存屏障

结合单例双重校验锁理解

注意:不保证原子性(例如++,--)

使用场景:共享变量的读操作,及常量赋值操作(这些操作本身具有原子性)

总结:保证线程安全的思路:

  1. 使用没有共享资源的模型
  2. 适用共享资源只读,不写的模型
    1. 不需要写共享资源的模型
    2. 使用不可变对象
  3. 直面线程安全
    1. 保证原子性
    2. 保证可见性
    3. 保证有序性

最难不过坚持!!!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Python中的多线程编程可以使用`threading`模块来实现。然而,需要注意的是,由于全局解释器锁(GIL)的存在,Python中的多线程并不能真正实现并行执行,而只是通过在不同的时间片中切换线程来模拟并发执行。 在多线程编程中,线程安全是一个很重要的概念。线程安全指的是当多个线程同时访问共享资源时,不会导致数据的不一致或者损坏。在Python中,有几种方法可以保证线程安全: 1. 使用互斥锁(Lock):通过互斥锁来控制对共享资源的访问,同一时间只允许一个线程访问共享资源。可以使用`threading.Lock()`来创建一个互斥锁对象,并使用`acquire()`和`release()`方法来获取和释放锁。 2. 使用条件变量(Condition):通过条件变量来控制线程的执行顺序。条件变量是一个包含了等待(wait)、通知(notify)和广播(broadcast)操作的对象。可以使用`threading.Condition()`来创建一个条件变量对象,并使用`wait()`、`notify()`和`notifyAll()`方法来实现线程之间的同步。 3. 使用信号量(Semaphore):信号量是一种计数器,用于控制对共享资源的访问。可以使用`threading.Semaphore()`来创建一个信号量对象,并使用`acquire()`和`release()`方法来获取和释放信号量。 需要注意的是,使用这些线程同步机制会增加程序的复杂性和开销,所以在实际应用中,需要根据具体情况权衡使用。此外,还可以考虑使用线程安全的数据结构或者避免共享资源的方式来减少线程安全问题的发生。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值