线程安全与锁优化
线程安全
当多线程访问一个对象时,如果不用考虑这些下线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的(代码本身封装了所有必要的正确性保障手段).
Java的线程安全
可以讲Java语言中的各种操作共享数据分为以下5类:不可变,绝对线程安全,相对线程安全,线程兼容,线程对立
不可变:保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量声明为final
绝对线程安全:不管运行环境如何,调用者都不需要额外的同步措施.达到这个要求需要付出很大,甚至有时候不切实际的代价.例如:Vector的方法都是线程安全的,但是在高并发下,调用方不做额外的同步措施,依旧会出现安全问题
相对线程安全:就是我们通常意义上的线程安全,需要保证对这个对象单独的操作都是线程安全的.在调用的时候不需要额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性.Vector,HashTable,Collections的synchronizedCollection()
线程兼容:对象本身不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用.
线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码.这种情况要避免.Thread的suspend()和resume(),如果两个线程同时持有一个线程对象,一个尝试中断线程,一个尝试恢复线程,如果并发进行,无论调用者是否进行了同步,目标线程都存在死锁的风险,如果suspend()中断的线程就是即将要执行resume()的那个线程,那就肯定要产生死锁.常见的对立线程还有System.setIn(),Synstem.setOut()和System.runFinalizersOnExit()
显示锁需要我们手动加锁和解锁,来防止死锁和锁中断,而sync会自旋等待.CAS锁在竞争激烈的时候,CPU会消耗严重,一般都是锁竞争不激烈的时候使用,但是也存在ABA的问题.
线程安全的其他解决方案:
栈封闭:Service层,一般没有共享遍历
线程安全的实现方法
与代码编写和虚拟机提供的同步和锁机制有关
互斥同步:悲观的并发策略.互斥是因,同步是果,互斥是方法,同步是目的;Java中,最基本的互斥就是synchronized关键字,synchronized同步块对同一线程来说是可重入的,不会出现把自己锁死的行为,其次,同步块在已进入的线程执行止之前,会阻塞后面其他线程的进入.如果要阻塞或唤醒一个线程,都是需要操作系统来帮忙的,也就是需要切换到核心态中(因为Java的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统),也就是线程阻塞和唤醒所带来的性能问题.
实现互斥同步的主要方式有:临界区,互斥量,信号量
ReentrantLock与synchronized很相似,都具备一样的线程重入特性,只是代码写法上有点区别,一个是API层面的互斥锁,一个是原生语法层面的互斥锁.不过相对于synchronized,ReentrantLock实现了一些高级特性
(1)等待可中断:当持有锁的线程长期不释放锁的时候,正在对等待的线程可以选择放弃等待,改为处理其他的事情.
(2)可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁.synchronized是非公平锁,ReentrantLock默认是非公平锁
(3)锁绑定多个条件:ReentrantLock锁可以绑定多个Condition对象,而synchronized如果要和多于一个条件关联的时候,就得额外添加一个锁
JDK1.5的时候,ReentrantLock效率高于synchronized,JDK1.6后,两个锁性能上基本上持平
非阻塞同步:乐观的并发策略.基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享资源,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他补偿措施(不断重试,直至成功),这种并发策略不需要把线程挂起.
需要"硬件指令集的发展",因为我们需要操作和冲突检测这两个步骤具备原子性,这里再使用互斥同步就失去了意义,所以只能依靠硬件来完成这件事.
无同步方案:线程安全和同步没有因果关系,只有涉及到共享数据,才有安全直说
(1)可重入代码:也叫纯代码,不依赖存储在堆上的数据和公用的系统资源,用到的状态量都是由参数中传入,不调用非可重入的方法等.
所有的可重入代码都是线程安全的,但是并非所有线程安全的代码都是可重入的.
(2)线程本地存储:将共享数据的代码保证在同一线程中执行.每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值得K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口.
例:消费队列的架构模式,Web中的一个请求对应一个服务器线程
锁优化
自旋锁与自适应自旋:后面的线程"稍等一下",但不放弃处理器的执行时间,为了让线程等待,我们只需让线程执行一个慢循环(自旋),这就是所谓的自旋锁.避免了线程切换的开销,但是占用了处理器的时间,在锁被占用的时间过长的时候,就会耗费处理器资源.
JDK1.6引用自适应自旋锁,即自旋时间不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定.
锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步 ,但是被检测到不可能存在共享数据竞争的锁进行消除,其主要判断依据是逃逸分析的数据(需要使用数据分析流分析)支持,如果一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁就无需进行.
例如:StringBuffer的连续append();
锁粗化:如果虚拟机探测到一串的零碎操作都在对同一个对象加锁,就会把加锁的范围扩展(粗化)到整个操作序列的外部.
例如:StringBuffer的连续append();
轻量级锁:JDK1.6加入的新型锁机制,相对于使用操作系统互斥量来实现的传统锁而言的.本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗.
同步依据:绝大部分锁,在整个同步周期内都是不存在竞争的
偏向锁:JDK1.6引入,目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能.可以提高带有同步但无竞争的程序性能.但是如果程序中的大多数锁总是被多个不同线程访问,那么偏向模式就是多余的.