保障线程安全的设计技术
1、尽量使用局部变量代替“实例变量和静态变量”。
堆空间和方法区是线程可以共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题。 栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固定的线程安全性。
2、如果必须是实例变量,那么可以考虑创建多个对象,每个线程持有自己的对象,这样实例变量的内存就不共享了。
3、不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
锁的优化及注意事项
有助于提高锁性能的几点建议
1、减少锁持有时间
对于使用锁进行并发控制的应用程序来说,如果单个线程特有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能。在程序中需要尽可能减少线程对锁的持有时间,如下面代码:
public synchronized void syncMethod(){ othercode1(); mutexMethod(); othercode(); }
在syncMethod同步方法中,假设只有mutexMethod()方法是需要同 步的, othercode1()方法与 othercode2()方法不需要进行同步. 如othercode1 与 othercode2 这两个方法需要花费较长的 CPU 时间,在并发量较大的情况下,这种同步方案会导致等待线程的大量增加。一个较好的优化方案是,只在必要时进行同步,可以减少锁的持有时间,提高系统的吞吐量,如把上面的代码改为:
public void syncMethod(){ othercode1(); synchronized (this) { mutexMethod(); } othercode(); }
只对 mutexMethod()方法进行同步,这种减少锁持有时间有助于降低锁冲突的可能性,提升系统的并发能力。
2、减小锁的粒度
一个锁保护的共享数据的数量大小称为锁的粒度。如果一个锁保护的共享数据的数量大就称该锁的粒度粗,否则称该锁的粒度细。锁的粒度过粗会导致线程在申请锁时需要进行不必要的等待。减少锁粒度是一种削弱多线程锁竞争的一种手段,可以提高系统的并发性。
3、使用读写分离锁代替独占锁
使用ReadWriteLock读写分离锁可以提高系统性能, 使用读写分离锁也是减小锁粒度的一种特殊情况。第二条建议是能分割数据结构实现减小锁的粒度,那么读写锁是对系统功能点的分割。
在多数情况下都允许多个线程同时读,在写的使用采用独占锁,在读多写少的情况下,使用读写锁可以大大提高系统的并发能力。
4、锁分离
将读写锁的思想进一步延伸就是锁分离。读写锁是根据读写操作功能上的不同进行了锁分离。根据应用程序功能的特点,也可以对独占锁进行分离。如 java.util.concurrent.LinkedBlockingQueue 类中 take()与 put()方法分别从队头取数据,把数据添加到队尾。虽然这两个方法都是对队列进行修改操作,由于操作的主体是链表,take()操作的是链表的头部,put()操作的是链表的尾部,两者并不冲突。如果采用独占锁的话,这两个操作不能同时并发,在该类中就采用锁分离,take()取数据时有取锁, put()添加数据时有自己的添加锁,这样 take()与 put()相互独立实现了并发。
5、粗锁化
为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短。但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和 放,也会消耗系统资源。如:
public void method1(){ synchronized( lock ){ //同步代码块 1 } synchronized( lock ){ //同步代码块 2 } }
JVM 在遇到一连串不断对同一个锁进行请求和释放操作时,会把所 有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫锁的粗化,如上一段代码会整合为:
public void method1(){ synchronized( lock ){ //同步代码块 1 //同步代码块 2 } }
在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其在循环体内请求锁时,如:
for(int i = 0 ; i< 100; i++){ synchronized(lock){} }
这种情况下,意味着每次循环都需要申请锁和释放锁,所以一种更合理的做法就是在循环外请求一次锁,如:
synchronized( lock ){ for(int i = 0 ; i< 100; i++){} }
JVM 对锁的优化
一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程。偏向第一个线程,这个线程在修改对象头成为偏向锁时使用 CAS 操作,将对象头中 ThreadId 改成自己的 ID,之后再访问这个对象时,只需要对比 ID 即可。 一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当第二个线程访问对象时,表示在这个对象上已经存在竞争了,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程; 如果原来的线程依然存活,则马上执行原来线程的栈,检查该对象的使用情况,如果仍然需要偏向锁,则偏向锁升级为轻量级锁。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋)另外一个线程就会释放锁。 当自旋超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时, 轻量级锁会膨胀为重量级锁, 重量级锁除了持有锁的线程外,其他的线程都阻塞。
锁的升级的目的:在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。