导读:题目中提到的几个关键字,分别是解决并发问题中,加锁所使用到的几个关键字,每个关键字代表的锁的粒度 不同,本篇博客,主要是从概念定义上,区分这几个关键字的应用场景。(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天,就废在这几个关键字上,我勒个去。然后还有关于信号量的问题还没有解决,好多好多。。。。。。


posted on 2017-01-07 16:47  何红霞  阅读(453)  评论(0编辑  收藏  举报