本文从以下介绍线程安全与锁优化
一、线程安全
二、锁优化
一、线程安全
1、线程安全的定义
Brian Goetz对线程安全的定义:
当多个线程访问一个对象的时候,如果不用考虑这些线程在运行时环境下的调度与交替执行,也不需要进行额外的同步,或者调用者也不需要做任何的其他协调操作,调用该对象的行为都可以获取到正确的结果,该对象就是线程安全的。
线程安全代码具备的特征:
代码本身封装了正确性的保障手段(如互斥同步),调用者无需关注多线程的问题,也无需实现任何措施来保证多线程的正确调用。
2、Java语言中的线程安全
线程的安全程度,由强到弱,Java语言各种操作共享数据分五类:不可变,绝对线程安全、相对线程安全、线程兼容、线程对立
(1)不可变
不可变的对象一定是线程安全的,无论是对象的方法实现还是调用者调用该方法,都不需要任何的线程安全措施保障。
java语言,共享数据为一个基本数据类型,只要将其用final修饰可以保证其不可变;
如果共享数据为一个对象,就需要对象的行为对其状态(属性)没有任何影响,则可保证不可变,如:String,substring()方法,不会改变原来的字符串,方法执行后会返回一个新的值;
保证对象行为不影响自己的状态方式很多:比如:将对象中带有状态的变量声明为final,如:Integer中的构造函数,用final声明了一个成员变量
private final int value;
public Integer(int value){
this.value = value;
}
(2)绝对线程安全
绝对的线程安全,是符合Brain Goetz给出的线程安全的定义的,Java API标注的自己是线程安全的类大多数不是绝对的线程安全类,如Vector
package net.oschina.tkj.jvmstu.thread; import java.util.Vector; public class TestVector { private static final Vector v = new Vector(); public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < v.size(); i++) { v.remove(i); } } }); Thread t1 = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < v.size(); i++) { System.out.println(v.get(i)); } } }); t.start(); t1.start(); while (Thread.activeCount() > 20) ; while (true) { for (int i = 0; i < 10; i++) { v.add(i); } } } }
出现异常问题:
解决办法在run()方法内加synchronized。
因此,java中声明线程安全的api并非是绝对的线程安全的。
(3)相对线程安全
对某个对象单独操作是线程安全的,如果多线程调用时,需要使用额外的线程安全手段来保证调用的正确性。如:上述的Vector,Hashtable等。
(4)线程兼容
线程兼容对象本身不是线程安全,可以通过使用同步的操作来保证线程安全。
(5)线程对立
不管调用者是否采用同步措施,都无法在多线程的环境中并发使用代码。
3、线程安全的实现
线程安全问题产生的原因:存在多个线程,并且多个线程有共享数据。
线程安全问题的解决办法:
(1)互斥同步(Mutual Exclusion &Synchronized)
Java里互斥同步的手段就是同步关注字Synchronized。
1>同步关键字Synchronized
①同步关键字在编译后,会在同步块前后生成monitorenter、monitorexit两个字节码指令,这两个字节码指令都需要一个reference类型的参数来指定锁定和解锁的对象;
②JVM规范中指出,在执行monitorenter指令时,首先要尝试获取对象的锁。如果该对象没有被锁定,或者当前的线程已经拥有了那个对象的锁,把锁计数器加1。相应的在执行monitorexit指令时会把锁计数器减1,当计数器的值为0,则锁被释放。如果获取对象的锁失败,那当前线程处于阻塞等待状态,直到对象的锁被占用的线程释放为止;
③JVM规范描述中,关于monitorenter,monitorexist指令注意点:
《1》synchronized对一个线程来说可重入,不会出现自己把自己锁死的情况;
《2》同步块在已进入的线程执行之前,会阻塞后面的其他线程的进入。
④java的线程要映射到操作系统的原生线程之上,因此,线程的阻塞和唤醒,都需要操作系统参与,这就需要从用户态转入到核心态,因此状态的转换需要花费处理器的大量时间,所以,Synchronized为java语言一个重量级的操作;
(2)java.util.concurrent.ReentrantLock实现同步
ReentrantLocak与synchronizd的操作类似,不过ReentrantLock为显式声明锁定与解锁操作(lock与unlock方法配合try,finally语句块一起使用)
ReentrantLock比synchronizd增加的功能如下:
《1》等待可中断:持有锁的线程长时间不释放锁,等待的线程可以选择放弃等待处理其他事情,对执行时间过长的同步块有用;
《2》公平锁:多线程等待同一个锁时,按照申请锁的先后顺序依次获得锁;而非公平锁,不保证这一点,任何一个等待锁的线程都有机会获得锁;synchronized为非公平锁,ReentrantLock默认为非公平锁,可通过构造方法的布尔值转为公平锁;
《3》锁可以绑定多个条件:指一个ReentrantLock对象可以绑定多个Condition对象。
(3)非阻塞同步
互斥同步最主要的问题是,进行线程的阻塞和唤醒时性能的问题,因此这种同步也称为阻塞同步。
悲观并发策略:该种策略认为,如果不去做正确的同步操作,那就会出现问题。无论线程是否真的竞争共享数据都需要加锁的操作。
乐观并发策略:基于冲突检测的乐观并发策略,简单讲就是先进行操作,如果没有线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,就进行其他补偿措施,这种乐观的并发策略都不需要将线程挂起,因此也称为非阻塞同步(Non-blocking synchronization)
二、锁优化
高并发JDK1.6重要主题,实现锁优化的技术:适应性自旋,锁消除,锁粗化,轻量级锁,偏向锁,这些技术旨为解决线程间高效共享数据,解决竞争问题,提高程序执行效率。
1、自旋锁与自适应锁
同步互斥对性能最大的影响是阻塞实现,挂起线程以及恢复线程都需要转入内核态来完成,这给操作系统的并发性能带来很大压力。
自旋锁:物理机器有一个以上处理器,可以同时处理多个线程并发,为了实现请求锁的线程等待,而不放弃处理器的执行时间。只需等待锁的线程执行一个忙循环(自旋),该技术为自旋锁。
自旋锁优点:避免了线程状态转化的开销
自旋锁缺点:需要占用处理器的时间,如果持有锁的线程持有锁时间过长,则会造成等待锁的线程持有处理器时间过长,资源浪费。所以给自旋锁设置自旋的次数默认为10。如果超过该次数未获取锁,则用传统的手段将该线程挂起。
自适应自旋:自旋的时间不固定,由前一次在同一个锁上的自旋时间及suo的拥有者的状态决定。
2、锁消除
jvm的即时编译器在运行时,对一些代码上要求同步,但是当检测到不可能存在共享数据的争用情况则对锁进行消除。
3、锁粗化
正常情况下,同步块使用时都是在尽量小的范围内使用,这样等待的锁的线程可以尽快拿到锁。
特殊情况下:一个操作反复的出现加锁解锁的操作,甚至锁的操作在循环中,即使不出现竞争数据,也会造成性能的不必要浪费。因此,此时会报加锁的范围扩大到(粗化)整个操作的外部,这样保证只加一次锁的操作。如:StringBuffer的appen()就是锁粗化操作。多个append(),加锁放到第一个append()之前,解锁放到最后一个append()之后。
4、轻量级锁
相对于使用系统的互斥量的传统锁而言。
轻量级锁提升同步性能依据:对于大部分锁而言,整个同步周期内都是不存在竞争的。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,如果存在竞争,除了互斥量的开销外,还额外的进行CAS操作,因此有竞争的情况轻量级锁更慢。
5、偏向锁
消除数据无竞争的情况下的同步,提高程序的性能。
轻量级锁在无竞争数据的情况下通过CAS消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步都消除掉,CAS的操作也无需执行。