多线程之七:锁优化

整理自炼数成金
源码连接:
1. 锁优化的思路和方

    1.1 减少锁持有时间

        

        如果需要同步的代码只是其中小部分,最好用同步块代替同步方法,并且尽可能减少同步块里的代码量。

    1.2减小锁粒度

     将大对象,拆成小对象,大大增加并行度,降低锁竞争

     偏向锁,轻量级锁成功率提

        ConcurrentHashMap的实现

            – 若干个Segment Segment[] segments

            – Segment中维护HashEntry

            – put操作时

                • 先定位到Segment,锁定一个Segment,执行put

     在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进

    1.3锁分离

     根据功能进行锁分

        合理使用ReadWriteLock

     读多写少的情况,可以提高性

     读写分离思想可以延伸,只要操作互不影响,锁就可以分

    1.4锁粗化

     通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完 公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行 任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗 系统宝贵的资源,反而不利于性能的优化。

        即在一个方法内有多个同步块,并且都是用同一个锁,而每个同步块之间的非同步代码执行的是耗时短的操作,应该将多个同步块合并优化成一个同步块。

        

    1.5锁消

     在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

[java]  view plain  copy
  1. public static void main(String args[]) throws InterruptedException {   
  2.     long start = System.currentTimeMillis();   
  3.     for (int i = 0; i < CIRCLE; i++) {   
  4.         craeteStringBuffer("JVM""Diagnosis");   
  5.     }   
  6.     long bufferCost = System.currentTimeMillis() - start;   
  7.     System.out.println("craeteStringBuffer: " + bufferCost + " ms");   
  8. }   
  9. public static String craeteStringBuffer(String s1, String s2) {   
  10.     StringBuffer sb = new StringBuffer();   
  11.     sb.append(s1);   
  12.     sb.append(s2);   
  13.     return sb.toString();   
  14. }  

        

        

2. 虚拟机内的锁优

    对象头Mark

        Mark Word,对象头的标记,32

        描述对象的hash、锁信息,垃圾回收标记,年龄

                – 指向锁记录的指针 – 指向monitor的指针

                – GC标记

                – 偏向锁线程ID

    偏向锁

        设计:

        每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

 

        大部分情况是没有竞争的,所以可以通过偏向来提高性能。

        所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程。

        将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark。

        只要没有竞争,获得偏向锁的线程,将来进入同步块不需要做同步

        当其他线程请求相同的锁时,偏向模式结束在多争用的场景下,如果另外一个线程争用偏向对象,拥有者需要释放偏向锁,而释放的过程会带来一些性能开销,但总体说来偏向锁带来的好处还是大于CAS代价的

        -XX:+UseBiasedLocking

            – 默认启用

        在竞争激烈的场合,偏向锁会增加系统负

[java]  view plain  copy
  1. public static List<Integer> numberList =new Vector<Integer>();  
  2. public static void main(String[] args) throws InterruptedException {  
  3.     long begin=System.currentTimeMillis();  
  4.     int count=0;  
  5.     int startnum=0;  
  6.     while(count<10000000){  
  7.         numberList.add(startnum);  
  8.         startnum+=2;  
  9.         count++;  
  10.     }  
  11.     long end=System.currentTimeMillis();  
  12.     System.out.println(end-begin);  
  13. }  

            

        本例中,使用偏 向锁,可以获得 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. 一个错误使用锁的案

[java]  view plain  copy
  1. public class IntegerLock {  
  2.     static Integer i=0;  
  3.     public static class AddThread extends Thread{  
  4.         public void run(){  
  5.             for(int k=0;k<100000;k++){  
  6.                 synchronized(i){  
  7.                     i++;  
  8.                 }  
  9.             }  
  10.         }  
  11.     }  
  12.     public static void main(String[] args) throws InterruptedException {  
  13.         AddThread t1=new AddThread();  
  14.         AddThread t2=new AddThread();  
  15.         t1.start();t2.start();  
  16.         t1.join();t2.join();  
  17.         System.out.println(i);  
  18.     }  
  19. }  

    代码中使用变量i作为锁对象,会导致该同步是没意义的。

    因为变量i的类型是Integer,而i++操作实际上是先+1,然后再赋值给I,通过new Integer()的方式,所以每次i++操作之后,i的对象都被改变了,同步块中的锁对象永远都不会相同。


5. ThreadLocal

[java]  view plain  copy
  1. private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  2. public static class ParseDate implements Runnable{  
  3.     int i=0;  
  4.     public ParseDate(int i){this.i=i;}  
  5.     public void run() {  
  6.         try {  
  7.             Date t=sdf.parse("2015-03-29 19:29:"+i%60);  
  8.             System.out.println(i+":"+t);  
  9.         } catch (ParseException e) {  
  10.             e.printStackTrace();  
  11.         }  
  12.     }  
  13. }  
  14. public static void main(String[] args) {  
  15.     ExecutorService es=Executors.newFixedThreadPool(10);  
  16.     for(int i=0;i<1000;i++){  
  17.     es.execute(new ParseDate(i));  
  18.     }  
  19. }  

该代码是为每一个线程分配一个实例,但是SimpleDateFormat是线程不安全的,因为SimpleDateFormat里有个Calendar用来保存和处理日期信息,在多线程中可能会出现A线程调用parse()format()时返回结果是线程B处理 的日期。

解决方法:

为每一个线程分配一个实

[java]  view plain  copy
  1. static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();  
  2. public static class ParseDate implements Runnable{  
  3.     int i=0;  
  4.     public ParseDate(int i){this.i=i;}  
  5.     public void run() {  
  6.         try {  
  7.             if(tl.get()==null){  
  8.                 tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));  
  9.             }  
  10.             Date t=tl.get().parse("2015-03-29 19:29:"+i%60);  
  11.             System.out.println(i+":"+t);  
  12.         } catch (ParseException e) {  
  13.             e.printStackTrace();  
  14.         }  
  15.     }  
  16. }  
  17. public static void main(String[] args) {  
  18.     ExecutorService es=Executors.newFixedThreadPool(10);  
  19.     for(int i=0;i<1000;i++){  
  20.         es.execute(new ParseDate(i));  
  21.     }  
  22. }  

    需要注意的是,这里的ThreadLocal.set(SimpleDateFormat)的时候不能传入SimpleDateFormat的静态对象,否则每个线程都是保存了一个对象而已。造成了ThreadLocal没意义。

猜你喜欢

转载自blog.csdn.net/qq_21508727/article/details/80617674