多线程与高并发(二)--synchronized,volatile,CAS

synchronized底层

  • JDK早期的实现是重量级锁(向操作系统申请锁)。
  • Hotspot实现(锁升级)
    1. 首先在锁的markword上记录第一个访问的线程ID,并没有上锁(偏向锁)。
    2. 如果有线程争用,升级为自旋锁。
    3. 默认旋10次之后升级为重量级锁,未获得锁的线程进入等待队列。
    4. 可参考http://ifeve.com/java-synchronized/

  引申:

    • 在Hotspot锁只能升级,不能降级。
    • 自旋锁只占用CPU,不访问操作系统。在用户态解决锁问题,不经过内核态,加解锁效率比重量级锁效率高。
    • 执行时间长以及线程多的时候尽量使用重量级锁。

synchronized 优化

  1. 锁细化:同步代码块中的语句越少越好。
  2. 锁粗化:如果一个方法中加了很多锁,这时把整个方法上锁性能会提高很多。
  3. 锁对象要加final

Volatile

  1. 保证线程可见性
    • 所有线程共享堆内存,另外每个线程都有自己的工作内存。当线程要访问堆内存中的数据时,会把堆内存中的数据copy到线程的工作内存,对该变量的改变是在工作内存的进行改变。线程对变量的更改,没有及时的反应到其他线程,导致线程之间数据不可见。
    • 变量加上volatile关键字之后,能够保证一个线程修改变量之后,其他线程及时可以发现。
    • volatile本质上使用了CPU的缓存一致性协议(MESI)。
  2. 禁止指令重排序
    • CPU为了提高执行效率,会并发的执行指令。使用volatile之后,CPU不会进行重排序。
      • DCL单例(Double Check Lock)
        package designPatterns;
        
        public class DCLSingleTon {
        
            private volatile static DCLSingleTon dclSingleTon;
        
            private DCLSingleTon() {
            }
        
            public static DCLSingleTon getInstance() {
                // 外层的判断不能去掉,可以显著提高效率
                if (dclSingleTon == null) {
                    synchronized (DCLSingleTon.class) {
                        if (dclSingleTon == null) {
                            /**
                             * 经过编译之后分为三步
                             * 1.分配内存,给变量赋默认值
                             * 2.成员变量初始化
                             * 3.把内存赋值给dclSingleTon
                             * 如果发生指令重排序,可能会发生2和3交换
                             * 这时dclSingleTon已经不是null,新来的线程就会直接获得未初始化成员变量的instance
                             */
                            dclSingleTon = new DCLSingleTon();
                        }
                    }
                }
                return dclSingleTon;
            }
        }
  3. 不能保证原子性

 CAS(Compare And Swap)

  • cas(V, Expected, NewValue)
    • if V == Expected

      V = NewValue

      otherwise try again or fail

    • concurrent包中的cas是CPU原语支持,执行过程不会被打断
  • ABA问题
    • 当前线程的CAS操作无法分辨当前V值是否发生过变化。比如在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。

    • 可以通过加版本号解决。
  • LongAdder与AtomicLog比较
    • 两者都是使用的CAS操作来进行增减
    • 不同点在于LongAdder内部是分段锁。
posted @ 2021-02-13 09:45  January01  阅读(49)  评论(0)    收藏  举报