线程安全简述

线程安全性是指多线程环境下,一个函数、对象或系统的行为是否可以正确地处理多个线程同时访问或修改共享的数据而不会产生不确定的结果或导致数据损坏。 在并发编程中,线程安全性是一个非常重要的概念,因为多线程同时操作共享资源时可能引发竞态条件(race conditions)和其他并发问题。

如果要实现线程安全性,就要保证我们的类是线程安全的的。在《 Java 并发 编程实战》 中, 定义"类是线程安全的"如下:

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

理解

原子性(Atomicity)

指一个操作是不可中断的。一个原子操作要么完全执行成功,要么完全不执行,不会出现中间状态。在并发环境中,如果多个线程同时执行某个原子操作,那么该操作的执行结果应当与线程的执行顺序无关,保证数据的一致性。

可见性(Visibility)

指一个线程对共享数据的修改对其他线程是可见的。当一个线程修改了共享数据时,其他线程应当能够立即看到最新的修改结果,而不是看到过期或无效的数据。

实现线程安全

线程封闭

线程封闭就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。

线程封闭是一种简单有效的并发编程技术,适用于某些场景下的数据隔离需求。它能够减少线程间的竞争和同步开销,提高并发程序的性能和可靠性。然而,需要注意线程封闭可能带来的局限性,如线程安全性和数据一致性的保证,以及对并发性能的影响等。因此,在使用线程封闭时需要仔细评估场景和需求,确保其适用性和正确性。

栈封闭(Stack Confinement)

将数据保存在线程栈的局部变量中。由于局部变量的作用域仅限于所属线程的执行上下文,其他线程无法访问到该数据,因此可以避免并发访问的问题。

ThreadLocal

使用Java中的ThreadLocal类,可以将数据与当前线程关联起来,使得每个线程都有自己的数据副本。ThreadLocal提供了线程级别的数据隔离,每个线程对数据的访问都是独立的,从而避免了并发访问的问题。

单线程模型

某些情况下,可以将任务或资源限制在单个线程中进行处理,从而避免了并发访问的问题。例如,使用单个线程的事件驱动模型,或者使用单个线程的线程池来处理任务。

无状态的类

无状态的类是指不包含任何实例变量(或称为状态)的类,其行为仅依赖于传入的参数。换句话说,无状态类的方法不会受到类级别的状态影响,每次调用方法时,都只关注输入参数和方法的逻辑,而不依赖于类内部的状态信息。

java 复制代码
public class StringUtils {

    public static String concatenate(String str1, String str2) {
        return str1 + str2;
    }

    // 私有构造函数,防止实例化
    private StringUtils() {
        // 空实现
    }
}

在这个示例中,StringUtils 类是一个无状态类。它只包含一个静态方法 concatenate,该方法接受两个字符串作为参数,并返回它们的拼接结果。这个类没有实例变量,也没有实例方法,因此没有状态。 由于 concatenate 方法只依赖于传入的参数,它的行为不受类级别的状态影响。每次调用 concatenate 方法时,都只关注传入的参数和方法内部的逻辑,而不需要担心类级别的状态。 这个类可以被多个线程并发调用,因为它没有共享的实例变量。它的方法是线程安全的,不会出现线程间的竞争条件。

类不可变

类不可变指一旦创建后就不能被修改的类。在这种类中,其内部状态(成员变量)是只读的,不能被修改,因此对象的状态在创建后是不变的。

java 复制代码
public final class ImmutableClass {
    private final int value;
    private final String name;

    public ImmutableClass(int value, String name) {
        this.value = value;
        this.name = name;
    }

    public int getValue() {
        return value;
    }

    public String getName() {
        return name;
    }
}

在这个示例中,ImmutableClass 是一个不可变类。它的成员变量 value 和 name 被声明为 final,并在构造函数中进行初始化。这样一来,在对象创建后,它们的值就无法再被修改。 由于该类的状态不可变,因此它是线程安全的。多个线程可以同时访问对象的方法,而不需要担心并发访问导致的数据不一致性。

注意

一旦类的成员变量中有对象, 上述的 final 关键字保证不可变 并不能保证类的安全性。 因为在多线程下,虽然对象的引用不可变,但是 对象在堆上的实例是有可能被多个线程同时修改的,没有正确处理的情况下,对象实例在堆中的数据是不可预知的。

java 复制代码
public final class ImmutableClass {
    private final int value;
    private final String name;
    // 不安全的
    private final UserVo userVo;

    public ImmutableClass(int value, String name, UserVo userVo) {
        this.value = value;
        this.name = name;
        this.userVo = userVo;
    }

    public int getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    public UserVo getUserVo() {
        return userVo;
    }
}

synchronized

synchronized 关键字是 Java 提供的一种同步机制,用于实现线程安全。它通过对代码块或方法加锁,保证同一时间只有一个线程可以执行被锁定的代码,从而避免多线程并发访问共享资源时可能出现的竞争条件和数据不一致性。

实现方式

synchronized 代码块
java 复制代码
synchronized (lockObject) {
    // 需要同步的代码块
}

在这种方式中,需要指定一个锁对象(可以是任意对象),该对象用于对代码块进行加锁。只有获得了锁对象的线程才能执行被锁定的代码块,其他线程需要等待锁的释放。

synchronized 方法
java 复制代码
public synchronized void synchronizedMethod() {
    // 需要同步的方法体
}

在这种方式中,可以直接在方法声明中使用 synchronized 关键字,表示整个方法是同步的。对于非静态方法,锁对象是当前对象实例(即 this);对于静态方法,锁对象是该方法所在的类对象。

如何保证线程安全

  • 原子性

synchronized 代码块或方法在同一时间只允许一个线程执行,保证了操作的原子性。

  • 可见性

当一个线程释放锁时,会将对共享变量的修改刷新到主内存中,使得其他线程可以看到最新的值,保证了可见性。

  • 有序性

synchronized 保证了代码的有序性,即对同一锁的代码块或方法的执行顺序与其在程序中的顺序保持一致。

死锁问题

死锁(Deadlock)是指在多线程并发编程中,两个或多个线程被永久地阻塞,彼此互相等待对方持有的资源而无法继续执行的情况。

java 复制代码
public class DeadlockExample {
    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1 acquired resource 1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource2) {
                    System.out.println("Thread 1 acquired resource 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2 acquired resource 2");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource1) {
                    System.out.println("Thread 2 acquired resource 1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个示例中,有两个线程 thread1 和 thread2,它们都试图获取两个资源 resource1 和 resource2,但是它们的获取顺序是相反的。 thread1 首先获取 resource1,然后尝试获取 resource2;而 thread2 首先获取 resource2,然后尝试获取 resource1。如果两个线程同时运行,它们将相互等待对方所持有的资源,导致死锁。

总结:

  1. 死锁是必然发生在多操作者(M>=2 )争夺多个资源( N>=2个,且N<=M)才会发生这种情况;
  2. 争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁;
  3. 争夺者对拿到的资源不放手;

学术化定义

  • 互斥条件

指进程对所分配到的资源进行排它性使用, 即在一段时间内 某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待, 直至占有资源的进程用毕释放。

  • 请求和保持条件

指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

  • 不剥夺条件

指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  • 环路等待条件

指在发生死锁时,必然存在一个进程------资源的环形链,例如thread1 等待 resource2,而 thread2 等待 resource1,形成了循环等待。

死锁的危害

  • 程序停止响应

一旦发生死锁,各个线程将无法继续执行,导致程序停止响应。这可能会影响系统的正常运行,使用户无法进行交互或完成操作。

  • 资源浪费

死锁会导致一些线程持有的资源无法被其他线程使用,造成资源的浪费。这些资源可能是内存、文件句柄、数据库连接等,如果被死锁占用,其他线程将无法利用这些资源,导致系统性能下降。

  • 系统崩溃

某些情况下,死锁可能导致系统崩溃。当系统中存在大量的死锁情况时,资源消耗可能过高,系统无法正常运行,最终崩溃。

  • 无法继续进行的操作

当发生死锁时,涉及到死锁的线程无法继续执行,可能会造成一些操作的无法完成。例如,无法提交事务、无法释放锁导致其他线程无法继续执行等。

  • 难以排查和修复

死锁是一种复杂的并发问题,发生死锁时,可能会导致线程状态的混乱和数据不一致的情况。排查死锁问题可能需要深入分析和调试,并且修复死锁问题可能需要对代码进行重构和重新设计。

避免死锁的方式

  • 避免循环等待

按照固定的顺序获取锁,确保线程按照相同的顺序请求和释放资源,避免形成循环等待的情况。

  • 加锁顺序

尽量以相同的顺序获取锁,这样可以避免不同线程以不同的顺序获取资源导致死锁的产生。

  • 资源分配策略

使用资源分配策略来避免死锁,如银行家算法、资源预先分配等。通过合理分配资源,避免资源竞争和死锁的产生。

  • 超时机制

对于获取锁的操作,设置超时机制,当一定时间内无法获得锁时,放弃当前获取的资源,回滚操作,防止死锁的长时间持续。

  • 死锁检测和恢复

使用死锁检测算法来检测死锁的发生,一旦检测到死锁,采取相应的恢复措施,如终止部分线程、回滚操作等。

  • 减少锁粒度

将锁的粒度尽量缩小,锁定尽可能小的代码块,这样可以减少锁的持有时间,减少死锁的风险。

其他线程安全问题

活锁

活锁(Livelock)是一种类似于死锁的并发问题,其中线程们处于不断改变自己的状态,但无法继续前进,导致无法完成任务。

在活锁中,线程们相互响应对方的动作,但却无法向前推进,最终导致无法完成任务的情况。不同于死锁中线程被阻塞的情况,活锁中的线程处于忙等(Busy Waiting)的状态,不断重试、改变自己的状态,但无法成功。

java 复制代码
public class LivelockExample {
    private static boolean isTakingTurns = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            while (isTakingTurns) {
                System.out.println("Thread 1 is waiting politely...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                isTakingTurns = false;
            }
        });

        Thread thread2 = new Thread(() -> {
            while (isTakingTurns) {
                System.out.println("Thread 2 is waiting politely...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                isTakingTurns = false;
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个示例中,有两个线程 thread1 和 thread2,它们都试图交替执行某个操作,但由于逻辑上的冲突,导致无法顺利交替执行,最终进入了活锁状态。

产生原因

  • 响应性过度

当线程遇到冲突或竞争时,为了避免死锁,它们试图改变自己的状态或行为。然而,如果所有线程都同时响应并改变自己的行为,就可能导致活锁的发生。

  • 同步策略问题

不恰当的同步策略或竞争条件可能导致线程在忙等状态下相互响应,无法前进。

避免方式

  • 随机化

通过引入随机因素,使得线程在冲突时具有不确定性的行为,减少线程之间的同步冲突,降低活锁的可能性。

  • 退避策略

当线程遇到冲突时,可以使用退避策略,即暂停一段时间后再尝试,避免持续的忙等状态。

  • 合理的调度策略

合理的线程调度策略可以减少线程之间的竞争和冲突,降低活锁的发生概率。

  • 重试次数限制

对于一些可能导致活锁的操作,可以设置重试次数限制,超过限制则采取其他策略。

  • 分布式算法

在分布式系统中,可以采用一些分布式算法,如仲裁者、协调者等,来解决并发冲突和避免活锁的问题。

相关推荐
程序员-珍10 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin3344556627 分钟前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
bug菌1 小时前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
夜月行者3 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm
Yvemil73 小时前
RabbitMQ 入门到精通指南
开发语言·后端·ruby
sdg_advance3 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
猿java4 小时前
使用 Kafka面临的挑战
java·后端·kafka
碳苯4 小时前
【rCore OS 开源操作系统】Rust 枚举与模式匹配
开发语言·人工智能·后端·rust·操作系统·os
kylinxjd4 小时前
spring boot发送邮件
java·spring boot·后端·发送email邮件
2401_857439697 小时前
Spring Boot新闻推荐系统:用户体验优化
spring boot·后端·ux