Java并发安所有分知识点总结 —— 8月20日学习分享

很是感谢洋哥的本周知识分享,灰常精辟~!洋哥的知识串起来了线程安全的大部分知识,我也根据个人知识储备及网络搜寻,整理了一份我本身当前的理解。html

一.  线程安全性的知识准备java

  1.1  知识准备a:JVM 内存模型 与 线程安全git

    线程安全,就是经过多个线程对某个资源进行有序访问或者修改,这里的某项资源对应的底层便是一个个的 JVM 内存模型。缓存

    因此,针对 线程安全 来谈的 JVM 内存模型,想要实现线程安全,那么就要实现两点:可见性及有序性,前者保证某项线程修改某共享变量以后能够被其它线程感知,后者保证非原子操做的线程安全。安全

    什么是 可见性?网络

    

    普通状况下,当线程须要对某一共享变量进行修改时,一般会进行以下的过程:多线程

    a  从主内存中拷贝变量的一份副本,并装载到工做内存中;并发

    b  在工做内存中执行代码,修改副本的值;ide

    c  用工做内存中的副本值更新主存中的相关变量值。函数

    当一个共享变量在多个线程的工做内存中都有副本时,若是一个线程修改了这个共享变量,那么其余线程应该可以看到这个被修改后的值,这就是多线程的可见性问题。    

    什么是 有序性?

    非原子操做时,因为各线程的操做都是针对 Work-Memory 的,若是多个线程时无序操做的,那么颇有可能出如今 a,b 线程同时读取一项共享数据,而后分别作了修改,这样就会出现线程安全问题,举例以下:

假设有一个共享变量x,线程a执行x=x+1。从上面的描述中能够知道x=x+1并非一个原子操做,它的执行过程以下:
1 从主存中读取变量x副本到工做内存
2 给x加1
3 将x加1后的值写回主 存
若是另一个线程b执行x=x-1,执行过程以下:
1 从主存中读取变量x副本到工做内存
2 给x减1
3 将x减1后的值写回主存 
那么显然,最终的x的值是不可靠的。假设x如今为10,线程a加1,线程b减1,从表面上看,彷佛最终x仍是为10,可是多线程状况下会有这种状况发生:
1:线程a从主存读取x副本到工做内存,工做内存中x值为10
2:线程b从主存读取x副本到工做内存,工做内存中x值为10
3:线程a将工做内存中x加1,工做内存中x值为11
4:线程a将x提交主存中,主存中x为11
5:线程b将工做内存中x值减1,工做内存中x值为9
6:线程b将x提交到中主存中,主存中x为9 
一样,x有可能为11,若是x是一个银行帐户,线程a存款,线程b扣款,显然这样是有严重问题

    1.2  volatile 关键字

      volatile是java提供的一种轻量级同步,该关键字只能保证 JVM 内存模型的可见性,volatile共享变量进行写操做的时候,会多出一条lock前缀的指令,即告知JVM:它所修饰的域的原子操做都不须要通过线程的工做内存,而直接在主内存中进行修改。这样就保证了线程从主内存中读取(read)它的值的时候,老是最新的,同时另外一方面,volatile 并不能保证有序性。

      可是问题在于,Java 中原子操做较少,volatile 只能保证赋值操做(原子操做)的安全性,而不能保证非原子操做的安全性。

      补充:lock 操做的底层含义

lock指令在多核处理器下会引起两件事:
1. 将当前处理器缓存行(cache line)的数据写会到系统内存
2. 这个写会内存操做会使其它CPU缓存了该内存地址的数据无效

缓冲行:缓存中能够分配最小存储单位。

为了提升处理速度,处理器不直接与内存进行通讯。而是先将系统内存的数据读到内部缓存(L1,L2或者其余)后再作操做。但操做完不知道什么时候回写到内存。

若是对生命了volatile的变量进行写操做,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。可是,就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算就会有问题。因此,在多处理器下,为了保证各个处理器缓存的值是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操做的时候,会从新从系统内存中把数据读处处理器缓存。
lock

 

二.  线程安全性基础

  2.1  什么样的数据会出现线程安全问题?

  a.  共享的

  b.  可变的

  补充知识:线程安全性有哪几类?

Bloch 给出了描述五类线程安全性的分类方法:不可变、线程安全、有条件线程安全、线程兼容和线程对立。
1. 不可变 不可变的对象必定是线程安全的,而且永远也不须要额外的同步[1] 。由于一个不可变的对象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。
Java 类库中大多数基本数值类如 Integer 、 String 和 BigInteger 都是不可变的。 须要注意的是,对于Integer,该类不提供add方法,加法是使用
+来直接操做。而+操做是不具线程安全的。这是提供原子操做类AtomicInteger的原缘由。 2. 线程安全 线程安全的对象具备在上面“线程安全”一节中描述的属性;
由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,无论运行时环境如何排线程都不须要任何额外的同步。这种线程安全性保证是很严格的;
许多类,如 Hashtable 或者 Vector 都不能知足这种严格的定义。 3. 有条件的 有条件的线程安全类对于单独的操做能够是线程安全的,可是某些操做序列可能须要外部同步。
条件线程安全的最多见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器,
由这些类返回的 fail-fast 迭代器假定在迭代器进行遍历的时候底层集合不会有变化。
为了保证其余线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集合以实现遍历的完整性。一般,独占性的访问是由对锁的同步保证的,
而且类的文档应该说明是哪一个锁(一般是对象的内部监视器(intrinsic monitor))。 若是对一个有条件线程安全类进行记录,那么您应该不只要记录它是有条件线程安全的,并且还要记录必须防止哪些操做序列的并发访问。
用户能够合理地假设其余操做序列不须要任何额外的同步。
4. 线程兼容 线程兼容类不是线程安全的,可是能够经过正确使用同步而在并发环境中安全地使用。
这可能意味着用一个
synchronized 块包围每个方法调用,或者建立一个包装器对象,其中每个方法都是同步的(就像 Collections.synchronizedList() 同样)。
也可能意味着用 synchronized 块包围某些操做序列。为了最大程度地利用线程兼容类,若是全部调用都使用同一个块,那么就不该该要求调用者对该块同步。
这样作会使线程兼容的对象做为变量实例包含在其余线程安全的对象中,从而能够利用其全部者对象的同步。 许多常见的类是线程兼容的,如集合类 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或者 JDBC 类 Connection 和 ResultSet 。
5. 线程对立 线程对立类是那些无论是否调用了外部同步都不能在并发使用时安全地呈现的类。
线程对立不多见,当类修改静态数据,而静态数据会影响在其余线程中执行的其余类的行为,这时一般会出现线程对立。
线程对立类的一个例子是调用 System.setOut() 的类

  2.2  如何避免线程安全问题?

    1.  不可变(有final关键字修饰且已被赋值)

    2.  不共享(局部变量,ThreadLocal等)

    

    3.  加锁(synchronized、lock、ReentrantLock等)

    

三.  TreadLocal 定义及使用

   网上查了一部分关于 Threadlocal 的资料,主要是从 线程安全 解析的居多,但按照做者的原意,ThreadLocal 类的设计不只用于多线程环境下的各线程维护独立于其它线程以外的的变量,同时也能用于关联线程的上下文。

This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable. {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).

  根据winwill2012的中文翻译:ThreadLocal的做用是提供线程内的局部变量,这种变量在线程的生命周期内起做用,减小同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

  其实再翻译下也就是说:某个不便于一直传递的,同时是该线程所特有的资源,该资源就能够放置在 ThreadLocal 中。

  3.1  实现原理

    每一个 Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例自己,value 是真正须要存储的 Object。

    JDK 1.8 中的 get 方法源码:    

public T get() {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
      return setInitialValue();
  }
get

    getMap 方法的源码:

1 ThreadLocalMap getMap(Thread t) {
2     return t.threadLocals;
3 }
getMap

    setInitialValue函数的源码:

 1 private T setInitialValue() {
 2     T value = initialValue();
 3     Thread t = Thread.currentThread();
 4     ThreadLocalMap map = getMap(t);
 5     if (map != null)
 6         map.set(this, value);
 7     else
 8         createMap(t, value);
 9     return value;
10 }
setInitialValue

    createMap函数的源码:

1 void createMap(Thread t, T firstValue) {
2     t.threadLocals = new ThreadLocalMap(this, firstValue);
3 }
createMap

    从以上的代码中,咱们能够大体总结出 get 方法的流程:

    1.  获取当前线程;

    2.  根据当前线程获取一个 Map;

    3.  若是 Map 不为空,根据当前线程 ThreadLocal 的饮用做为 key ,获取 entry e;

    4.  若是 e 不为空,返回 e.value,不然转到5;

    5.  Map 为空或者 e 为空,则经过 initialValue 函数获取初始值 value,而后用 ThreadLocal 的引用和 value 做为 firstKey 和 firstValue 建立一个新的 Map

  3.2  ThreadLocalMap 的回收问题

    补充知识:Java 中的强引用,软引用,弱引用,虚引用

1.强引用
     之前咱们使用的大部分引用实际上都是强引用,这是使用最广泛的引用。若是一个对象具备强引用,那就相似于必不可少的生活用品,垃圾回收器毫不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具备强引用的对象来解决内存不足问题。

2.软引用(SoftReference)
      若是一个对象只具备软引用,那就相似于可有可物的生活用品。若是内存空间足够,垃圾回收器就不会回收它,若是内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就能够被程序使用。软引用可用来实现内存敏感的高速缓存。
      软引用能够和一个引用队列(ReferenceQueue)联合使用,若是软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)
    若是一个对象只具备弱引用,那就相似于可有可物的生活用品。弱引用与软引用的区别在于:只具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程当中,一旦发现了只具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程, 所以不必定会很快发现那些只具备弱引用的对象。 
    弱引用能够和一个引用队列(ReferenceQueue)联合使用,若是弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)
    "虚引用"顾名思义,就是形同虚设,与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收。
    虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,若是发现它还有虚引用,就会在回收对象的内存以前,把这个虚引用加入到与之关联的引用队列中。程序能够经过判断引用队列中是否已经加入了虚引用,来了解。
    被引用的对象是否将要被垃圾回收。程序若是发现某个虚引用已经被加入到引用队列,那么就能够在所引用的对象的内存被回收以前采起必要的行动。

    特别注意,在实际程序设计中通常不多使用弱引用与虚引用,使用软用的状况较多,这是由于软引用能够加速JVM对垃圾内存的回收速度,能够维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

软引用代码示例:
import java.lang.ref.SoftReference;  
public class Test {  
      
    public static void main(String[] args){  
        System.out.println("开始");  
          
        A a = new A();  
          
        SoftReference<A> sr = new SoftReference<A>(a);  
        a = null;  
        if(sr!=null){  
            a = sr.get();  
        }  
        else{  
            a = new A();  
            sr = new SoftReference<A>(a);  
        }  
          
        System.out.println("结束");     
    }  
      
}  
  
class A{  
    int[] a ;  
    public A(){  
        a = new int[100000000];  
    }  
} 
四种引用

    而在 ThreadLocal 中,ThreadLocalMap是使用ThreadLocal的弱引用做为Key的:

 1 static class ThreadLocalMap {
 2         /**
 3          * The entries in this hash map extend WeakReference, using
 4          * its main ref field as the key (which is always a
 5          * ThreadLocal object).  Note that null keys (i.e. entry.get()
 6          * == null) mean that the key is no longer referenced, so the
 7          * entry can be expunged from table.  Such entries are referred to
 8          * as "stale entries" in the code that follows.
 9          */
10         static class Entry extends WeakReference<ThreadLocal<?>> {
11             /** The value associated with this ThreadLocal. */
12             Object value;
13             Entry(ThreadLocal<?> k, Object v) {
14                 super(k);
15                 value = v;
16             }
17         }
18         ...
19         ...
20 }
ThreadLocalMap

    用图例表示为(实线为强引用,虚线为弱引用):

    

    由图可知,若是不作任何处理,可能会发生如下状况:

    ThreadLocalMap 使用 ThreadLocal 的弱引用做为 key,若是一个 ThreadLocal 没有外部强引用引用他,那么系统 gc 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,若是当前线程再迟迟不结束的话,这些key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远没法回收,形成内存泄露。

    因此为了不这种状况,在设计中已经作了一些防御措施:

    ThreadLocalMap的getEntry方法的源码:

1 private Entry getEntry(ThreadLocal<?> key) {
2     int i = key.threadLocalHashCode & (table.length - 1);
3     Entry e = table[i];
4     if (e != null && e.get() == key)
5         return e;
6     else
7         return getEntryAfterMiss(key, i, e);
8 }
getEntry

    getEntryAfterMiss函数的源码:

 1 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
 2      Entry[] tab = table;
 3      int len = tab.length;
 4      while (e != null) {
 5          ThreadLocal<?> k = e.get();
 6          if (k == key)
 7              return e;
 8          if (k == null)
 9              expungeStaleEntry(i);
10          else
11              i = nextIndex(i, len);
12          e = tab[i];
13      }
14      return null;
15  }
getEntryAfterMiss

    expungeStaleEntry函数的源码:

 1 private int expungeStaleEntry(int staleSlot) {
 2            Entry[] tab = table;
 3            int len = tab.length;
 4            // expunge entry at staleSlot
 5            tab[staleSlot].value = null;
 6            tab[staleSlot] = null;
 7            size--;
 8            // Rehash until we encounter null
 9            Entry e;
10            int i;
11            for (i = nextIndex(staleSlot, len);
12                 (e = tab[i]) != null;
13                 i = nextIndex(i, len)) {
14                ThreadLocal<?> k = e.get();
15                if (k == null) {
16                    e.value = null;
17                    tab[i] = null;
18                    size--;
19                } else {
20                    int h = k.threadLocalHashCode & (len - 1);
21                    if (h != i) {
22                        tab[i] = null;
23                        // Unlike Knuth 6.4 Algorithm R, we must scan until
24                        // null because multiple entries could have been stale.
25                        while (tab[h] != null)
26                            h = nextIndex(h, len);
27                        tab[h] = e;
28                    }
29                }
30            }
31            return i;
32        }
expungeStaleEntry

    根据这些源码,咱们能够分析出 getEntry 的运行流程:

     1.  首先从 ThreadLocal 的直接索引位置(经过 ThreadLocal.threadLocalHashCode & (len-1) 运算获得)获取 Entry e,若是 e 不为 null 而且 key 相同则返回 e;若是 e 为 null 或者 key 不一致则向下一个位置查询,若是下一个位置的 key 和当前须要查询的 key 相等,则返回对应的 Entry,不然,若是 key 值为 null,则擦除该位置的 Entry,不然继续向下一个位置查询。

     2.  在这个过程当中遇到的 key 为 null 的 Entry 都会被擦除,那么 Entry 内的 value 也就没有强引用链,天然会被回收。仔细研究代码能够发现,set 操做也有相似的思想,将 key 为 null 的这些 Entry 都删除,防止内存泄露。可是光这样仍是不够的,上面的设计思路依赖一个前提条件:要调用 ThreadLocalMap 的 getEntry 函数或者 set 函数。这固然是不可能任何状况都成立的,因此不少状况下须要使用者手动调用 ThreadLocal 的 remove 函数,手动删除再也不须要的 ThreadLocal ,防止内存泄露。因此 JDK 建议将 ThreadLocal 变量定义成 private static 的,这样的话 ThreadLocal 的生命周期就更长,因为一直存在 ThreadLocal 的强引用,因此 ThreadLocal 也就不会被回收,也就能保证任什么时候候都能根据 ThreadLocal 的弱引用访问到 Entry的value 值,而后 remove 它,防止内存泄露。

  3.3  ThreadLocal 总结(摘自洋哥的 PPT)

  ThreadLocal 负责管理ThreadLocalMap,包括插入,删除 等等,key就是ThreadLocal对象本身;同时,很重要的一点,就ThreadLocal把map存储在当前线程对象里面。 为何在ThreadLocalMap 中弱引用ThreadLocal对象呢?固然是从线程内存管理的角度出发的。 使用弱引用,使得ThreadLocalMap知道ThreadLocal对象是否已经失效,一旦该对象失效,也就是成为垃圾,那么它所操控的Map里的数据也就没有用处了,由于外界再也没法访问,进而决定擦除Map中相关的值对象,即:Entry对象的引用,来保证Map老是尽量的小。  总之,线程经过ThreadLocal 来给本身的map 添加值,删除值。同时一旦ThreadLocal自己成为垃圾,Map也能自动清除该ThreadLocal所操控的数据。  这样,经过设计一个代理类ThreadLocal,保证了咱们只须要往Map里面塞数据,无需担忧清除,这是普通map作不到的。

四. 同步加锁的四大方式

  4.1  volatile (前文已提到,此处再也不赘述)

  4.2  JVM 内置锁(监视器锁) synchronized

  很是久远以及久经使用的锁,也被称为重量级锁,基本用法:

    

  待续。。

 

 

参考文献:

1. 线程安全的理解 http://www.cnblogs.com/kunpengit/archive/2011/11/18/2254280.html 

2. 从JVM角度看线程安全与垃圾收集 http://blog.csdn.net/sadfishsc/article/details/10325879

3.《深刻理解java虚拟机》以内存模型与安全 http://blog.csdn.net/sjtu_chenchen/article/details/49132183

4. [Java并发包学习七]解密ThreadLocal http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/

相关文章
相关标签/搜索