1.锁优化的思路和方法
1.1 减少锁持有时间
如果需要同步的代码只是其中小部分,最好用同步块代替同步方法,并且尽可能减少同步块里的代码量。
1.2减小锁粒度
将大对象,拆成小对象,大大增加并行度,降低锁竞争
偏向锁,轻量级锁成功率提高
ConcurrentHashMap的实现
– 若干个Segment :Segment[] segments
– Segment中维护HashEntry
– put操作时
• 先定位到Segment,锁定一个Segment,执行put
在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入
1.3锁分离
根据功能进行锁分离
合理使用ReadWriteLock
读多写少的情况,可以提高性能
读写分离思想可以延伸,只要操作互不影响,锁就可以分离
1.4锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完 公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行 任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗 系统宝贵的资源,反而不利于性能的优化。
即在一个方法内有多个同步块,并且都是用同一个锁,而每个同步块之间的非同步代码执行的是耗时短的操作,应该将多个同步块合并优化成一个同步块。
1.5锁消除
在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
public static void main(String args[]) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < CIRCLE; i++) {
craeteStringBuffer("JVM", "Diagnosis");
}
long bufferCost = System.currentTimeMillis() - start;
System.out.println("craeteStringBuffer: " + bufferCost + " ms");
}
public static String craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
2. 虚拟机内的锁优化
对象头Mark
Mark Word,对象头的标记,32位
描述对象的hash、锁信息,垃圾回收标记,年龄
– 指向锁记录的指针 – 指向monitor的指针
– GC标记
– 偏向锁线程ID
偏向锁
设计:
每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。
大部分情况是没有竞争的,所以可以通过偏向来提高性能。
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程。
将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark。
只要没有竞争,获得偏向锁的线程,将来进入同步块不需要做同步
当其他线程请求相同的锁时,偏向模式结束,在多争用的场景下,如果另外一个线程争用偏向对象,拥有者需要释放偏向锁,而释放的过程会带来一些性能开销,但总体说来偏向锁带来的好处还是大于CAS代价的
-XX:+UseBiasedLocking
– 默认启用
在竞争激烈的场合,偏向锁会增加系统负担
public static List<Integer> numberList =new Vector<Integer>();
public static void main(String[] args) throws InterruptedException {
long begin=System.currentTimeMillis();
int count=0;
int startnum=0;
while(count<10000000){
numberList.add(startnum);
startnum+=2;
count++;
}
long end=System.currentTimeMillis();
System.out.println(end-begin);
}
本例中,使用偏 向锁,可以获得 5%以上的性能 提升
轻量级锁
BasicObjectLock
– 嵌入在线程栈中的对象
普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
如果对象没有被锁定
– 将对象头的Mark指针保存到锁对象中
– 将对象头设置为指向锁的指针(在线程栈空间中)
如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)
在没有锁竞争前提下,减少传统锁使用OS互斥量产生的性能损耗
在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降
自旋锁
原理:
当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋),在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。
何时使用了自旋锁:
在线程进入ContentionList时,也即第一步操作前。线程在进入等待队列时首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了Ready线程的锁。自旋锁由每个监视对象维护,每个监视对象一个。
JDK1.6中-XX:+UseSpinning开启
JDK1.7中,去掉此参数,改为内置实现
如果同步块很长,自旋失败,会降低系统性能
如果同步块很短,自旋成功,节省线程挂起切换时间,提升性能
偏向锁,轻量级锁,自旋锁总结
不是Java语言层面的锁优化方法
内置于JVM中的获取锁的优化方法和获取锁的步骤
每一个线程在准备获取共享资源时:
第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”
第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,
之前线程将Markword的内容置为空。
第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,
把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord,
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败
第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
– 偏向锁可用会先尝试偏向锁
– 轻量级锁可用会先尝试轻量级锁
– 以上都失败,尝试自旋锁
– 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起
4. 一个错误使用锁的案例
public class IntegerLock {
static Integer i=0;
public static class AddThread extends Thread{
public void run(){
for(int k=0;k<100000;k++){
synchronized(i){
i++;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
AddThread t1=new AddThread();
AddThread t2=new AddThread();
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
代码中使用变量i作为锁对象,会导致该同步是没意义的。
因为变量i的类型是Integer,而i++操作实际上是先+1,然后再赋值给I,通过new Integer()的方式,所以每次i++操作之后,i的对象都被改变了,同步块中的锁对象永远都不会相同。
5. ThreadLocal
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i){this.i=i;}
public void run() {
try {
Date t=sdf.parse("2015-03-29 19:29:"+i%60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es=Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
该代码是为每一个线程分配一个实例,但是SimpleDateFormat是线程不安全的,因为SimpleDateFormat里有个Calendar用来保存和处理日期信息,在多线程中可能会出现A线程调用parse()或format()时返回结果是线程B处理 的日期。
解决方法:
为每一个线程分配一个实例
static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i){this.i=i;}
public void run() {
try {
if(tl.get()==null){
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t=tl.get().parse("2015-03-29 19:29:"+i%60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es=Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
需要注意的是,这里的ThreadLocal.set(SimpleDateFormat)的时候不能传入SimpleDateFormat的静态对象,否则每个线程都是保存了一个对象而已。造成了ThreadLocal没意义。