一、提高锁性能的几点建议
1、减少锁持有的时间
不要将不必要的,需要大量处理时间的业务一起写在临界区内,这样会增加线程的持锁时间,等待线程大量增加。只需对必要的逻辑代码进行加锁。
2、减少锁粒度
减少锁粒度就是指缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力
concurrentHashMap在JDK1.7是最好的例子。它将内部分为了若干个小段,称为段(segment),默认情况下,一个ConcurrentHashMap可以被分为16小段。当其进行put操作时,会先计算其hash值,判断应该放在哪个段中,再对该段加锁。
3、用读写分离锁替换独占锁
如果说减小锁粒度是通过分割数据结构实现的,那么读写分离锁就是对系统功能的分割。在读多写少的场合,读写锁对性能是很有好处的。
4、锁分离
将读写锁的思想进一步延伸,就是所分离。经典案例就是LInkedBlockingQueue的实现。它实现了对take()和put()分别对队列的前端和尾端进行加锁。
5、锁粗化
如果不停的对同一把锁进行请求与释放,则会浪费很多系统资源。如果可以,将一些可以合并的同步方法合并,减少锁请求次数,以降低系统资源的消耗
二、Java虚拟机对锁的优化
1、锁偏向
如果一个线程获得了锁,就进入“偏向模式”。如果这个线程再次请求锁时,就无需再做任何同步操作。对几乎没有锁竞争的场合,偏向锁有比较好的优化效果。如果锁竞争激烈,则不适合使用偏向锁。使用虚拟机参数-XX:+UseBiasedLocking 可以开启偏向锁
2、轻量级锁
如果偏向锁失败,那么这个线程会转化为轻量级锁。它将对象头作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果获取轻量锁成功,则可以正常进入临界区,如果轻量级锁加锁失败,则表示有其他线程抢到了锁,此时锁会膨胀为重量级锁。
3、自旋锁
锁膨胀后,为了避免系统层面的挂起,线程会进行自旋(空循环),经过几次循环后,如果还不能顺利获取到锁,线程才会真正的挂起。
4、锁消除
Java虚拟机在JIT编译时,可以通过上下文的扫描,取出一些不可能存在共享资源的竞争的锁,可以节省毫无意义的请求锁时间。
例如:在单线程的环境下使用Vector集合。众所周知,Vector中的操作都是加锁的。在这种情况下,虚拟机会将这种无用的锁去除。
锁消除涉及的关键技术为逃逸分析。即观察一个变量是否会逃出某一个作用域。
三、ThreadLocal
1、ThreadLocal的简单使用
从名字上可以看出,这是一个线程的局部变量,也就是说,只有当前线程可以访问,是线程安全的。
案例:
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;public class ParseDate implements Runnable {static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();int i = 0;public ParseDate(int i) {this.i = i;}@Overridepublic void run() {if(tl.get()==null){tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));}Date t = null;try {t = tl.get().parse("2015-03-29 19:29:"+i%60);} catch (ParseException e) {e.printStackTrace();}System.out.println(i+":"+t);}}
如果当前线程不持有SimpleDateFormat对象实例,那么就建一个新的设置到当前线程中。
2、ThreadLocal的实现原理
ThreadLocal的set()方法:
public void set(T value) {Thread t = Thread.currentThread(); // 当前线程对象ThreadLocalMap map = getMap(t); // 获取线程的ThreadLocalMapif (map != null)map.set(this, value); // 将值存入其中,key为此ThreadLcoal对象elsecreateMap(t, value);}
Thread类中:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
设置到ThreadLocal的数据,正是写入了ThreadLocal的这个Map。
get()方法:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}
由于ThreadLocalMap维护在Thread类中,所以只要线程不退出,对象的引用将一直存在。如果使用线程池,有些线程就不会退出。这样可能会导致内存泄漏。如果我们不使用了,希望及时回收对象,最好使用:ThreadLocal.remove()方法将其移除。
值得注意的是,我们可以使用:tl = null 加速对他的回收。这是因为Thread.ThreadLocalMap内部是由Entry组成,而每一个Entry是WeakReference
3、对性能的帮助
在一些场景下,共享对象对于竞争的处理容易引起损失。所以我们还是应该考虑使用ThreadLocal。经典案例:Random。
在多线程下,产生随机数。经过测试ThreadLocal模式下的处理速度更快。
四、无锁
无锁是一种乐观的策略,它假设对资源的访问是不冲突的。采用比较交换(CAS,Compare And Swap)的技术才甄别冲突。如果有冲突,就重试,直到没有冲突为止。
1、比较交换
CAS的过程是:包含三个参数CAS(V,E,N),V是要更新的变量,E是预期值,N是新值。仅当V值等于E值,才会将V的值设为N。
在硬件层面,大部分的现代处理器已经支持原子化的CAS指令,在JDK5以后,虚拟机便可以使用这个指令来实现并发操作个并发数据结构。
2、无锁的线程安全整数:AtomicInteger
JDK并发包中有一个atomic包,其中实现了一些使用CAS操作的线程安全的类型。
比较常用的是AtomicInteger,其主要的一些主要方法为:
其核心字段:private volatile int value; 前实际取值
private static final long valueOffset; 保存着value字段在AtomicInteger对象中的偏移量。
3、Java中的指针:Unsafe类
让我们来看一下incrementAndGet()方法中的compareAndSet()方法的实现。
public final boolean compareAndSet(int expect,int update){return unsafe.compareAndSwapInt(this,valueOffset,expect,update);}
Unsafe类就是封装了一些类似指针的操作。compareAndSwapInt()方法是一个navtive方法。它的几个参数含义如下:public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
第一个为给定的对象,offset为对象内的偏移量(其实是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),expected表示期望值,x表示要设置的值,如果指定的字段的值为expected,就把它设置为x。
Unsafe类还提供了一些方法:
ConcurrentLinkedQueue类中的一些操作也是用CAS去实现的
我们无法通过调用unsafe的工厂方法去获取其实例:
public static Unsafe getUnsafe() {Class var0 = Reflection.getCallerClass();if (!VM.isSystemDomainLoader(var0.getClassLoader())) {throw new SecurityException("Unsafe"); // 抛出异常} else {return theUnsafe;}}
4、无锁的对象引用:AtomicReference
AtomicReference与AtomicInteger不同的地方在于:AtomicReference是对普通对象引用进行封装。在对一个对象进行修改时,可能会出现ABA的问题。
ABA问题:就是对象的值被修改了两次,而修改后,对象的值又恢复了原值,这样CAS无法判断其是否被修改过。
案例场景:
有一家蛋糕店搞活动, 决定在vip卡中余额小于20的客户赠送20元。每个客户只能被赠送一次。
这时可能会出现这种情况:若干个线程,不断的扫描数据,为满足条件的客户充值。如果在充值的同时进行消费,如我本来有19元,被赠送20元后,进行消费20元,这时又满足条件,可能会再次赠送。(ABA问题)
AtomicStampedReference可以帮我们很好地解决这个问题
5、带有时间戳的对象引用:AtomicStampedReference
AtomicStampedReference 不仅维护了对象值,还维护了一个时间戳(其实是一个任意整数可以表示状态值)。在其修改对象值时,还会更新时间戳。
部分API:
创建:AtomicStampedReference asr = new AtomicStampedReference<Integer>(19,0);
6、数组也能无锁:AtomicIntegerArray
当前可用的原子数组有:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray。分别表示整数数组,long型数组,普通对象数组。
以下为AtomicIntegerArray的部分API:
7、让普通变量也可以进行原子操作:AtomicIntegerFieldUpdater
在我们程序设计之处,可能会因为考虑不周,在后期可能导致一些变量有线程安全的问题。但如果我们直接去修改,工程量不说,也违背了开闭原则。
我们可以使用AtomicIntegerFieldUpdater 在不改动或极少改动源代码的基础上,让普通变量享受原子操作。
Updater有三种,分别是:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater。
使用案例:
public static class Candidate{int id;volatile int score;public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");}
注意事项:
- Updater只能修改它可见范围内的变量,因为它是使用反射得到这个变量的。
- 为了保证其正确被读取,变量必须是volatile类型的。
- 由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此它不支持static字段。(Unsafe.objectFieldOffset)方法不支持静态变量。
8、无锁的Vector实现
是amino并发包中的LockFreeVector。详情查看书中P196。
9、线程之间的互帮互助:SynchronousQueue的实现
在线程池一章提到,这是一个容量为0的队列,任何一个读操作都要等待一个写操作,反之亦然。
实际上synchronousQueue内部也大量使用了无锁工具。
它的put()与take()方法抽象为一个共同的方法Transferer.transfer(),完整的签名如下:Object transfer(Object e,boolean timed, long nanos)
当e为非空时,表示当前操作传递一个消费者,如果为空,则表示当前操作需要请求一个数据。timed表示是否存在超时时间,nanos则是超时时长。如果返回值为非空,则表示数据已经接受或正常提供;如果为空,则表示失败。
synchronousQueue内部维护了一个线程等待队列。等待线程中会维护保存等待线程及相关数据信息。
Transferer.transfer()函数大体分为:
1、如果等待队列为空,或队列中节点类型和本次操作是一致的,那么将当前操作压入队列等待。
2、如果等待队列中的元素与本次操作是互补的(一读一写),那么就插入一个”完成”状态的节点,并让它”匹配”到一个等待节点上,接着弹出这两个节点,并且对这两个线程继续执行。
3、如果发现等待队列的节点是完成节点,那么帮助这个节点完成任务,与2一致。
详细的实现代码可以自行研究。
五、有关死锁的问题
死锁:就是两个或多个线程互相等待对方的资源,而都不进行释放,从而无限等待。只能通过外力接入进行消除。
如果遇到死锁,可以通过jps来得到java进程的ID,再用jstack命令得到线程的线程堆栈。(或者使用jconsole查看)
