zoukankan      html  css  js  c++  java
  • 【java基础 14】锁的粒度:ThreadLocal、volatile、Atomic和Synchronized

    导读:题目中提到的几个关键字,分别是解决并发问题中,加锁所使用到的几个关键字,每个关键字代表的锁的粒度 不同,本篇博客,主要是从概念定义上,区分这几个关键字的应用场景。(PS:睡梦中,依稀记得有回面试的时候,问了我一个问题:你们在加锁的时候,加多大的锁? 哇塞,当时愣了一下,压根儿就没有这个大小的概念,我真的以为都是一样的)


    话说,就像加锁日记本的锁是个很小的艺术锁,保险箱一般是密码锁(或者什么指纹人脸瞳孔识别之类的),锁铁门的一般都是那种大号锁,所以,仅从生活考虑,这个锁也是分大小的,唉,为毛自己写代码的时候,却没有注意过,不过,应该说,我至今为止,只用过final和Synchronized !好了,看下面的解析吧,话说多了都是泪!


    首先,锁的粒度支持,也就是对Load、Store的各种顺序控制,load、store两两组合为4种情况:LoadLoad、StoreStore、LoadStore、StoreLoad,他们以一种指令屏障的方式来控制顺序。绝大部分系统,都支持StoreLoad。

    备注:JVM中一些普通变量的操作指令

    1,Load操作(将本地变量推至栈顶,用来给CPU调度运算)发生在read之后(之间可以有其他的指令)

    2,普通变量的修改未必会理解发生Store操作(将栈顶的数据写入本地变量),但发生Store操作,就会发生write操作


    一、ThreadLocal

    ThreadLocal可以放一个线程级别的变量,但是它本身可以被多个线程共享使用,而且又可以达到线程安全的目的,且绝对线程安全

    理解:这就是一个线程安全的全局变量,呵呵,全局变量,就是动一个地方,到处都变了的那位,再代码设计比较乱的情况下,如果用了很多ThreadLocal,那这个系统就会慢慢的神龙见首不见尾,要是再整出一个bug,就真的呵呵了。 要点:ThreadLocal是一种对象持有的方式,每个线程都有一个ThreadLocalMap,而这个ThradLocal则相当于是一个Key值,要保持的对象作为value值。

    ThreadLocal的“坑”:

    1,ThreadLocal是一个与线程绑定的变量,所以说,如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将与线程共存,第一个坑:不知道它的作用域范围

    2,理论上说,线程结束后ThreadLcoal就会被回收,但事实可能并非如此。比如说,在线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至永远不会结束,第二个坑:ThreadLocal变量的生命周期不可预测

    3,理论上每次set数据时,使用ThreadLocal本身作为Key,相同的Key肯定会替换原来的数据,原来的数据就会被释放,但是,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次从同一个ThreadLocal中取出对象,再对内容进行操作,第三个坑:内部的集合类和复杂对象所占用的空间膨胀


    填坑tips:让ThreadLocal的入口和出口可控,用finally去remove数据,为了不破坏ThradLocal的入口,一般在使用之前,会调用get()方法判断是否为null


    为什么说ThradLocal是与线程绑定的(java7源码),在jThreadLocal类中,主要包含三个方法:set(),get(),remove()

        /**
         * Returns the value in the current thread's copy of this
         * thread-local variable.  If the variable has no value for the
         * current thread, it is first initialized to the value returned
         * by an invocation of the {@link #initialValue} method.
         *
         * @return the current thread's value of this thread-local
         */
        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null)
                    return (T)e.value;
            }
            return setInitialValue();
        }
     /**
         * Sets the current thread's copy of this thread-local variable
         * to the specified value.  Most subclasses will have no need to
         * override this method, relying solely on the {@link #initialValue}
         * method to set the values of thread-locals.
         *
         * @param value the value to be stored in the current thread's copy of
         *        this thread-local.
         */
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
        /**
         * Removes the current thread's value for this thread-local
         * variable.  If this thread-local variable is subsequently
         * {@linkplain #get read} by the current thread, its value will be
         * reinitialized by invoking its {@link #initialValue} method,
         * unless its value is {@linkplain #set set} by the current thread
         * in the interim.  This may result in multiple invocations of the
         * <tt>initialValue</tt> method in the current thread.
         *
         * @since 1.5
         */
         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }

    备注:在任何异步程序中(包括异步I / O,非阻塞I / O),ThreadLocal的参数传递都是不靠谱的,因为线程将请求发送后,就不再等待远程返回结果继续向下执行了,真正的返回结果得到后,处理的线程可能是另一个。


    二、volatile

    volatile被称为是“最轻量级的锁”,因为它只是在读这个瞬间要求一个简单的顺序,而不是一个变量上的原子读写,或者在一段代码上的同步!

    volatile要求在对变量进行读 写操作时,其前后的指令在某些情况下不允许进行重排序,这种限制主要体现在以下3中情况:

    1,如果是一条对volatile变量进行赋值操作的代码,那么在该代码前面的任何代码不能与这个赋值操作交换顺序;如果是一条读取volatile变量的代码,则正好相反

    2,普通变量的读写操作相互之间是可以重排序的,只要不影响它们之间的逻辑语义顺序就可以重排序,但是如果普通变量的读 写操作遇上了volatile变量的操作,就需要遵循前一个基本原则

    3,如果两个volatile变量的读/ 写操作都在一段代码中,则依然遵循前两个基本原则,此时不管两者之间的读 写顺序如何,都肯定不会重排序


    理解:其实说到这个volatile变量,就想到那个sql,就是说发出sql查询语句,只会获取到发出sql语句时的数据块数据,而不会获取到之后的数据。比如说,我在9点1分1秒发出了一条查询语句,然后在9点1分2秒执行了update操作,那么我拿到的数据,只会是update之前的数据。这个volatile变量,感觉就是一样的,如果我有一个线程正在读取这个变量,那么另一个写操作的线程,就必须等待我这个读操作结束,因为这个volatile不允许重排序!


    三、synchronized

    Synchronized是一把锁,始终保证临界区的访问控制。临界区:指访问这个地方最多只能有一个线程在里面!

    对于这一个,结合到自己在项目中应用,我只想问我自己一个问题:姑娘,你难道只会把Synchronized关键字加到方法块吗?我要反思的是,为什么我加锁的最小粒度是一个完整的方法?其实,很多时候,这个锁的范围真的变大了,频繁的锁征用,就进入了悲观锁的状态!在加锁的时候,应该注意一下锁的粒度问题,节省不必要的开销!


    四、Atomic

    PS:想想事务的原子性,再想想乐观锁的实现原理,这个东西秒懂

    Atomic为我们提供了一些列java原子变量的操作方法,其中,Atomic提供的原子操作类有(java 7源码):



    可分为4中类型:

    1,基本变量操作:Boolean,Integer,Long

    2,引用(reference)操作:也就是对象引用的操作,可以做到原子级别,当多个线程对同一个引用发生修改时,只会有一个成功(存在对ABA问题的处理)

    3,数组(Array)操作:这个操作并不是操作数组的对象,而是数组中的每一个元素,针对每一个元素的读写操作是线程安全的

    4,Updater:java提供了一种updater机制,可以在原有的类定义volatile变量的基础上,实现一种原子性的管理,而不需要将变量本身定义为Atomic,这样可以在不破坏原有程序逻辑的基础上实现一致性的读写


    简单说来,它的原子性实现,是基于可见性,修改前提取、修改后对比来确定是否写回到主存。请看下面的关键代码,以AtomicInteger为例(其他的几个操作类都类似,java7源码):

    /**
         * Atomically sets to the given value and returns the old value.
         *
         * @param newValue the new value
         * @return the previous value
         */
        public final int getAndSet(int newValue) {
            for (;;) {
                int current = get();
                if (compareAndSet(current, newValue))
                    return current;
            }
        }
    
        /**
         * Atomically sets the value to the given updated value
         * if the current value {@code ==} the expected value.
         *
         * @param expect the expected value
         * @param update the new value
         * @return true if successful. False return indicates that
         * the actual value was not equal to the expected value.
         */
        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
    从代码中对compareAndSet方法的调用,可以知道,它每次修改,都会有一个期待值和当前值的对比,如果一致,则写入!

    实现原子性,主要就是两种方法,一种是总线加锁,也就是我们常说的悲观锁;另外一种就是缓存加锁,对应着乐观锁

    五、总结

    关于这几个关键字的介绍就到这里了,简单说来,volatile和ThreadLocal主要是加在变量上,而Synchronized和Atomic是代码块或者更大级别的锁(Atomic可以不破坏原有程序的逻辑)

    以后自己再处理并发的时候,对于锁的概念,估计能清楚点。写这篇博客,看了书、参考了博客、还看了源码,我也真是够了!我足足写了差不多1天,就废在这几个关键字上,我勒个去。然后还有关于信号量的问题还没有解决,好多好多。。。。。。


  • 相关阅读:
    BZOJ1093: [ZJOI2007]最大半连通子图
    BZOJ4033: [HAOI2015]树上染色
    BZOJ1977: [BeiJing2010组队]次小生成树 Tree
    BZOJ4944: [Noi2017]泳池
    BZOJ1269: [AHOI2006]文本编辑器editor
    BZOJ4596: [Shoi2016]黑暗前的幻想乡
    BZOJ1815: [Shoi2006]color 有色图
    BZOJ1488: [HNOI2009]图的同构
    BZOJ3527: [Zjoi2014]力
    Salazar Slytherin's Locket CodeForces
  • 原文地址:https://www.cnblogs.com/hhx626/p/7534613.html
Copyright © 2011-2022 走看看