线程安全与锁优化

Posted 法海你懂不

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全与锁优化相关的知识,希望对你有一定的参考价值。

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

Java语言中的线程安全

按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

不可变

不可变的对象一定是线程安全的。
保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final。
每次对对象的操作都会返回一个新的对象,而不会去改变旧的对象,这样可以保证对象的线程安全。

绝对线程安全

绝对线程安全完全满足Brian Goetz给出的线程安全的定义。事实上,Java API标注自己是线程安全的类大部分分都不是绝对的线程安全。

相对线程安全

相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

线程兼容

指对象本身并不是线程安全的,但是通过使用同步手段来保证对象在并发环境中可以安全的使用。我们平时说的一个类不是线程安全的,绝大多数都是指这种情况。

线程独立

指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。

线程安全的实现方法

互斥同步

互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此在这4个字里面

互斥是因,同步是果;
互斥是方法,同步是目的。

Java中,最基本的互斥同步手段就是synchronized关键字。通过锁计数器+-1,实现对锁的加锁和释放。

非阻塞同步

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。从处理问题的方式上说,互斥同步属于一种悲观的并发策略。随着硬件指令集的发展,我们可以采用基于冲突检查的乐观并发策略,通俗地说,就是先行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。

乐观并发策略需要“硬件指令集”的支持,来保证操作和冲突检测这两个步骤的原子性。

无同步方案

要保证线程安全,并不是一定就要同步,两者并没有因果关系。同步只是保证共享数据争用时的正确手段,如果一个方法本来就不涉及共享数据,它自然就无需任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。

可重入代码(Reentry code)

也叫纯代码(Pure code)是一种允许多个进程同时访问的代码。程序在执行的任何时刻被打断,转而去执行另外一段代码(包括体归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入是更基本的特性,它可以保证线程安全,即所有的可重入代码都是线程安全的,但是并非所有的线程安全代码都是可重入的。

线程本地存储(Thread Local Storage)

如果一段代码中所需要的数据必须与其他代码共享,那就要看这些共享数据的代码是否能够保证再同一个线程中执行?如果能保证,就可以把共享数据的可见范围限制在同一个线程之内,这样无须同步也能保证线程之间不再出现数据争用的问题。

锁优化

自旋锁与自适应自旋

为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

引入自旋锁的原因是互斥同步对性能最大的影响是阻塞的实现,管钱线程和恢复线程的操作都需要转入内核态中完成,给并发带来很大压力。自旋锁让物理机器有一个以上的处理器的时候,能让两个或以上的线程同时并行执行。我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自旋锁的自旋次数默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行清除。

锁清除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无需进行。

锁粗化

另一种线程优化方式是锁粗化(或合并,merging)。

当多个彼此靠近的同步块可以合并到一起,形成一个同步块的时候,就会进行锁粗化。该方法还有一种变体,可以把多个同步方法合并为一个方法。如果所有方法都用一个锁对象,就可以尝试这种方法。

轻量级锁

轻量级锁并不是用来代替重量级锁的,它的本意是再没有多线程竞争的条件下,减少传统的重量级锁使用操作系统互斥量而产生的性能消耗。

偏向锁

它的目的是消除数据在无竞争条件情况下的同步原语,进一步提高程序的运行性能。

如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那么偏向锁就是在无竞争的条件下把整个同步都消除掉,连CAS都不做了。

以上是关于线程安全与锁优化的主要内容,如果未能解决你的问题,请参考以下文章

JVM线程安全与锁优化

jvm(13)-线程安全与锁优化(转)

线程安全与锁优化

Java并发编程学习:线程安全与锁优化

线程安全与锁优化

线程安全与锁优化