线程

1、线程安全性

定义:

  • 可以在多个线程中调用,并且在线程之间不会出现错误的交互

  • 可以同时被多个线程调用,而调用者无须执行额外的动作

  • 如果某个类可以在多个线程中安全地使用,那么它就是一个线程安全的类

在线程安全性的定义中,最核心的概念就是正确性

正确性:

  • 某个类的行为与其规范完全一致

  • 所见即所知(we know it when we see it)

  • 当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的

原子性:

  • 一组语句作为一个不可分割的单元被执行

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

“无状态对象一定是线程安全的“

2、内置锁(synchronized)

Java的内置锁(synchronized)相当于一个互斥体,这意味着最多只有一个线程能持有这种锁。

当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等待下去。

由于每次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

Volatile变量

Java提供的一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。

当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile类型的变量时总会返回最新写入的值。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性

使用volatile变量的条件:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

  • 该变量不会与其他状态变量一起纳入不变性条件中

  • 在访问变量时不需要加锁

发布与逸出

发布(publish)

“发布(Publish)“一个对象的意思是指,使得对象能够在当前作用域之外的代码中使用。

例如:

  • 将一个指向该对象的引用保存到其他代码可以访问的地方

  • 或者在某一个非私有的方法中返回该引用

  • 或者将引用传递到其他类的方法中

“发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件“

发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中:

/**
 * TODO 发布对象
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/2/25 14:50
 */
public class PublishObj {
    // 发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中
    public static Set<Secrets> knownSecrets;

    public void initialize(){
        knownSecrets = new HashSet<Secrets>();
    }
}

使用工厂方法来防止this引用在构造过程中逸出:

/**
 * TODO 防止this引用在构造过程中逸出
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/2/25 14:57
 */
public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event event) {
                System.out.println(event);
            }
        };
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.addListener(safe.listener);
        return safe;
    }

}

线程封闭

当访问共享的可变数据时,通常需要使用同步。

一种避免使用同步的方式就是不共享数据(如果仅在单线程内访问数据,就不需要同步),这种技术被称为线程封闭(Thread Confinement)

当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

线程封闭技术常见应用是JDBC(Java Database Connectivity)的Connection对象。在典型的服务器应用中,线程从连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。

  • Ad-hoc线程封闭

    • 维护线程封闭性的职责完全由程序实现来承担
  • 栈封闭

    • 栈封闭式线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象

    • 局部变量的固有属性之一就是封闭在执行线程中

    • 栈封闭:线程内部使用、线程局部使用

  • ThreadLocal类

    • 维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来

    • ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值

    • ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享

不变性

满足同步需求的另一种方法是使用不可变对象(Immutable Object)

如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象

  • 不可变对象一定是线程安全的。

  • final

  • volatile

常用策略

  • 线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。

  • 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象

  • 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步

  • 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象