Java虚拟机--线程安全和锁优化

时间:2023-12-25 17:11:43

Java虚拟机--线程安全和锁优化

线程安全

线程安全:当多线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

Java中,线程安全体现在多个线程访问同一个共享数据,如果一段代码中根本不会与其他线程共享数据,可以说不存在线程安全的问题。

线程安全的安全程度,由强至弱排序,可以分为以下5类。

不可变

不可变的对象一定是线程安全的,final关键字可以实现可见性。Java中如果共享数据时基本数据类型,加上final关键字就能保证它不可变,如果是对象,只需保证它是个不可变对象就行,比如String对象,对其的任何操作都不会改变原来的值,而是返回一个新的字符串对象。

绝对线程安全

满足一开始的线程安全定义,即:当多线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

相对线程安全

通常意义上的线程安全,需要保证这个对象单独操作是线程安全的,在调用时不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能使用额外的同步手段。Java API中大多数线程安全类都是这种类型,比如Vector、HashTable等。

线程兼容

指对象本身不是线程安全,但是可以通过使用同步手段保证在并发环境下安全使用。通常说的线程不安全就是指这个类型,Java中大多数类都是,比如ArrayList、HashMap等。

线程对立

无论是否采取了同步措施,都无法在多线程环境中并发安全使用。在Java中很少见。

线程安全的实现方法

互斥同步

同步指在多线程并发访问共享数据时,保证共享的那个数据在同一时刻只能被一个线程使用。。在Java中使用synchronized或者concurrent包中的重入锁可以实现同步。后者相比前者,有一些更高级的功能:

  • 等待可中断。当持有锁的线程长时间不释放锁,正在等待的线程可以不在放弃等待,从而可以处理其他任务。
  • 公平锁。多个线程等待同一个锁,按照先来后到的顺序依次得到锁。而不是任意一个线程都有机会在这一次能得到锁。synchronized中的锁时非公平的,重入锁ReentrantLock默认非公平,不过可以指定参数设置为公平锁。
  • 锁绑定多个条件。一个ReentrantLock可以和多个Condition对象绑定。

性能上,两者差不多。

非阻塞同步

同步会进行线程阻塞,属于悲观策略,即无论是否真的共享数据竞争,都要加锁。而CAS操作属于乐观策略,先进行操作,如果没有共享数据竞争,就操作成功;否则产生冲突,那么就不断重试,直到成功为止。

CAS操作有3个操作数,分别是内存位置V(Java中可简单理解为变量的内存地址)、旧的预期值A,和新值B,当且仅当V符合旧的预期值A时,才会用新值更新V的值,否则不更新,说明这个变量已经被别的线程修改过了

CAS有个问题:如果变量V一开始被读取到是A值,中途被修改成B,最后又被修改回A,CAS操作会误认为变量V没有被改变过,这称为CAS操作的“ABA问题”。

无同步

如果不涉及共享数据,就无需进行同步。可重入代码:可以在代码执行的任何时刻中断,转而去执行其他代码,在控制权返回后,原来的程序不会出现任何错误。可重入代码都是线程安全的

锁优化

自旋锁

如果共享数据的锁定状态只有很短的一段时间,为了这段时间去挂起和恢复线程(都需要转入内核态)并不值得,所以此时让后面请求锁的那个线程稍微等待以下,但不放弃处理器的执行时间。这里的等待其实就是执行了一个忙循环,这就是所谓的自旋

自旋等待有一定的限度,默认值是10次,如果超过这个次数,就会使用传统方式挂起线程。JDK1.6中引入了自适应的自旋锁。如果在同一个对象上,自旋等待刚刚成功获得了锁,且持有的锁正在运行中,虚拟机就认为这次自旋也会成功,进而它被允许有更长时间的自旋等待;相反,如果对于某个锁,自旋很少成功过,那在之后获取这个锁时很可能省略掉自旋过程。

锁消除

虚拟机即时编译时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。锁消除的依据来源于“逃逸分析”技术。堆上的所有数据都不会逃逸出去被其他线程访问到,就可以把它们当栈上的数据对待,认为它们是线程私有的,同步加锁就是没有必要的。

锁粗化

如果连续对某个对象反复枷锁或解锁,甚至加锁操作出现在循环中。虚拟机探测到这种情况,会把锁粗化到操作的外部。举个例子

public static int j = 0;

for (int i = 0; i < 100; i++) {
    synchronized (this) {
        j++;
    }
}

// 锁粗化
synchronized (this) {
    for (int i = 0; i < 100; i++) {
        j++;
    }
}

轻量级锁

传统的锁机制称为“重量级”锁,轻量级锁用于在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

轻量级锁的加锁和解锁都是通过CAS操作完成的,如果有两个以上的线程争用同一个锁,轻量级锁将膨胀为重量级锁

轻量级锁能提升同步性能主要因为:对于大多数锁,在整个同步周期内都是不存在竞争的。

偏向锁

轻量级锁噪无竞争的情况下使用CAS操作消除同步使用的互斥量,偏向锁是在无竞争的情况下吧整个同步都消除掉,CAS操作也没有了

偏向锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远也不需要再进行同步


by @sunhaiyu

2018.6.20