「JVM 高效并发」线程安全

  • 面向过程编程,把数据和过程分别作为独立的部分考虑,数据代表问题空间中的客体,程序代码则用于处理这些数据;
  • 面向对象编程,把数据和行为都看做对象的一部分,以符合现实世界的思维方式来编写和组织程序;

对象在一项工作进行期间会不停的中断和切换线程,对象的数据(数据)可能会在中断期间被修改和变脏,这将使的并发变得不安全;

  • 线程安全,当多个线程同时访问同一对象,若不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行其他协调操作,调用这个对象的行为都可以获得正确的结果,则称这个对象是线程安全的;(代码本身封装了所有必要的正确性保障手段,调用者不需要关心多线程的调用问题);

1. Java 语言中的线程安全

线程安全的五个级别

  • 不可变Immutable),不可变对象一定是线程安全的,最直接、最纯粹的安全;一旦不可变对象被正确的构建出来,它永远不会在多线程中处于不一致的状态;

若多线程共享的数据是一个基本数据类型,只要限定为 final 类型,则它是不可变的;

若多线程共享的数据是一个对象,由于 Java 语言暂时没有值类型,需要对象自行保证其行为不会对其状态产生任何影响(如 String 类型,它的 subString()、replace()、concat() 都不会影响它原来的值,而是返回一个新构造的对象);最简单的方式是将对象中带有状态的变量都声明为 final;

Java 类库 API 中不可变对象还有 java.lang.Number 的部分子类(Long、Double、BigInteger、BigDecimal 等),AotmicInteger、AtomicLong 是可变类型;

  • 绝对线程安全,完全满足上文线程安全定义;不管运行时环境如何,调用者都不需要任何额外的同步措施;
private static Vector<Integer> vector = new Vector<Integer>();

public static void main(String[] args) {
    while (true) {
        for (int i = 0; i < 10; i++) {
            vector.add(i);
        }

        Thread removeThread = new Thread(() -> {
            for (int i = 0; i < vector.size(); i++) {
                vector.remove(i);
            }
        });
        Thread printThread = new Thread(() -> {
            for (int i = 0; i < vector.size(); i++) {
                System.out.println((vector.get(i)));
            }
        });
        removeThread.start();
        printThread.start();
        // 不要同时产生过多的线程,否则会导致操作系统假死
        while (Thread.activeCount() > 20) ;
    }
}

运行结果

Exception in thread "Thread-24207" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 9
	at java.util.Vector.get(Vector.java:753)
	at edu.aurelius.jvm.concurrent.VectorTest.lambda$main$1(VectorTest.java:25)
	at java.lang.Thread.run(Thread.java:750)

Vector 是一个线程安全的容器,但并非绝对线程安全,其 add()、get()、size() 等方法都被 synchronized 修饰了,但在多线程环境下若不对调用端做额外同步限制,这段代码仍不安全;

线程安全的写法

Thread removeThread = new Thread(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.remove(i);
        }
    }
});

Thread printThread = new Thread(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            System.out.println((vector.get(i)));
        }
    }
});
  • 相对线程安全,通常意义上的线程安全,保障单次操作的线程安全,但不保证一些特定顺序的联系调用安全(见上例),如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等;

  • 线程兼容,对象本身不是线程安全的,到哪可以通过在调用端正确的使用同步手段保证对象在并发环境中的安全(通常说的不是线程安全的类型,如集合类 ArrayList、HashMap 等);

  • 线程对立,不管调用端使用采用同步措施,都无法在多线程环境中并发使用的代码(如同一个 Thread 类对象的 suspend() 和 resume() 同时在两个线程中调用,一个尝试中断线程,一个尝试恢复线程,无论是否进行了同步,线程都存在死锁的分析;还有 System.setIn、System.setOut、System.runFinalizersOnExit);

2. 线程安全的实现方法

只要明白了 JVM 线程安全措施的原理与运作过程,如何编写并发安全的代码便不再困难;

  • 互斥同步Mutual Exclusion & Synchronization),在多线程并发访问共享数据时,保障共享数据在同一时刻只被一条(或者一些,当使用信号量时)线程使用,互斥可以通过临界区Critical Selection)、互斥量Mutex)、信号量Semaphore)等实现;

synchronized 关键字

synchronized 经过 javac 编译,会在同步快的前后分别形成 monitorenter 和 monitorexit 两个字节指令,这两个字节指令通过一个 reference 类型的参数指明要锁定和解锁的对象;若没有指定 reference 对象,synchronized 修饰的是实例方法则锁定方法所在类的实例,synchronized 修饰的是静态方法则锁定方法所在类的 Class 对象;

《Java 虚拟机规范》要求执行 monitorenter 指令时,首先要尝试获取对象所,如果这个对象没有被锁定,或者当前线程已经获得了这个对象的所,则把锁的计数器的值加 1,执行 monitorexit 时将锁的计数器值减 1;一旦计数器值为 0,锁随机被释放;若获取锁失败,则当前线程被阻塞,等待其他线程释放;

持有锁是一个重量级操作,阻塞或唤醒一个线程需要操作系统来完成,这会陷入用户态和核心态的转换,这需要耗费很多处理器时间;

java.util.concurrent.locks.Lock 接口

Lock 接口是 Java 的另一种互斥同步手段,用户可以以非块结构(Non-Block Structured)来实现互斥同步;

重入锁ReentrantLock)是 Lock 接口最常见的一种实现,与 synchronized 相似,只是多了一些高级功能:等待可中断、可实现公平锁、可绑定多个条件;

等待可中断,当持有锁的线程长期不释放锁,正在等待的线程可以选择放弃等待,这对处理较长时间的同步块很有帮助;

公平锁,多个线程在等待同一锁时,必须按照申请锁的时间顺序来依次获得锁,synchronized 的锁是非公平的 ReentrantLock 默认也是非公平的,公平锁会导致 ReentrantLock 的性能急剧下降,明显影响吞吐量;

锁绑定多个条件,一个 ReentrantLock 对象绑定多个 Condition 对象,在 synchronized 中,锁对象的 wait() 和它的 notify()/notifyAll() 配合可以实现一个隐含条件;ReentrantLock 对象可以多次调用 newCondition() 绑定多个条件;

synchronized vs. ReentrantLock

JDK 5 时 synchronized 有非常大的优化余地,ReentrantLock 表现更稳定;
JDK 6 时 synchronized 锁得到优化,与 ReentrantLock 的性能基本持平,性能不再试选择的关键因素;
ReentrantLock 在功能上是 synchronized 的超集,但 synchronized 是 Java 语法层面的同步,更清晰简单,且自动处理异常时的锁释放,JVM 也更容易战队 synchronized 进行优化,所以在功能皆满足情况下,推荐使用 synchronized;

  • 非阻塞同步Non-Blocking Synchronized),基于冲突检测的乐观并发策略;不管风险,先进性操作,若没有其他线程争用共享数据,则操作成功,若共享数据被争用,产生了冲突,则进行补偿操作(如不停尝试,直到没有竞争为止);这种乐观并发策略不需要阻塞挂起线程,因此称为非阻塞同步;

原子性处理器指令集

a. 测试并设置(Test-and-Set)
b. 获取并增加(Fetch-and-Increment)
c. 交换(Swap)
d. 比较并交换(Compare-and-Swap,CAS)
e. 加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC)

Java 最终暴露出来的是 CAS 操作,通过三个操作数(内存位置 V、就的预期值 A、准备设置的新值 B),当且仅当 V 符合 A 时,处理器才会用 B 更新 V;不管是否更新成功,都返回 V 的旧值;

Atomic 原子自增运算

public static AtomicInteger race = new AtomicInteger(0);

public static void increase() {
    race.incrementAndGet();
}

private static final int THREADS_COUNT = 20;

public static void main(String[] args) throws Exception {
    Thread[] threads = new Thread[THREADS_COUNT];
    for (int i = 0; i < THREADS_COUNT; i++) {
        threads[i] = new Thread(() -> {
            for (int i1 = 0; i1 < 10000; i1++) {
                increase();
            }
        });
        threads[i].start();
    }
    while (Thread.activeCount() > 1) Thread.yield();
    System.out.println(race);
}

incrementAndGet() 的 JDK 源码

/**
* Atomically increment by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

不断尝试将一个比当前值大一的新值赋给自己,若失败,则说明旧值发生了变化,再次循环操作,直到设置成功为止;

CAS 操作的 ABA 问题:当变量 V 初次读取时是 A 值,准备复制时检测它还是 A,但实际他已经被改成 B,并改回 A 了;可通过原子引用类 AtomicStampedReference 保证 CAS 的正确性(通过给 V 添加版本号),不过传统的互斥同步可能会更高效;

  • 无同步方案,若让一段代码本来就不涉及线程共享数据,那它天生就是线程安全的;

可重入代码Reentrant Code),又称纯代码Pure Code),指在多线程的上下文语境中不涉及信号量等因素,不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都是由参数传入,不调用非可重入的方法等;可重入代码线程安全代码真子集

线程本地存储Thread Local Storage),若共享数据能保证只在同一线程中共享,可将共享数据的可见范围限制在同一线程内,这样就无需同步也能保证线程不出现数据争用问题;

Java 语言中,若一个变量被多个线程访问,可使用 volatile 将之声明为易变的;若一个变量只被单线程独享,可以通过 ThreadLocal 类实现线程本地存储(每个 Thread 对象中都有一个 ThreadLocalMap 对象,以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值,ThreadLocal 对象为当前线程的 Map 的访问入口);


上一篇:「JVM 高效并发」Java 协程
下一篇:「JVM 高效并发」锁优化

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值