并发编程之ThreadLocal、Volatile、synchronized、Atomic关键字

前言

对于ThreadLocal、Volatile、synchronized、Atomic这四个关键字,我想一说起到你们确定都想到的是解决在多线程并发环境下资源的共享问题,可是要细说每个的特色、区别、应用场景、内部实现等,却可能模糊不清,说不出个因此然来,因此,本文就对这几个关键字作一些做用、特色、实现上的讲解。算法

一、Atomic

做用:安全

对于原子操做类,Java的concurrent并发包中主要为咱们提供了这么几个经常使用的: AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference。
对于原子操做类,最大的特色是在多线程并发操做同一个资源的状况下,使用 Lock-Free算法来替代锁,这样开销小、速度快,对于原子操做类是采用原子操做指令实现的,从而能够保证操做的原子性。

什么是原子性?多线程

好比一个操做i++;实际上这是三个原子操做,先把i的值读取、而后修改(+1)、最后写入给i。因此使用Atomic原子类操做数,好比:i++;那么它会在这步操做都完成状况下才容许其它线程再对它进行操做,而这个实现则是经过Lock-Free+原子操做指令来肯定的.

如:AtomicInteger类中:并发

public final int incrementAndGet() {  
     for (;;) {  
         int current = get();  
         int next = current + 1;  
         if (compareAndSet(current, next))  
         return next;  
     }  
 }

而关于Lock-Free算法,则是一种新的策略替代锁来保证资源在并发时的完整性的,Lock-Free的实现有三步:ide

  • 一、循环(for(;;)、while)
  • 二、CAS(CompareAndSet)
  • 三、回退(return、break)

用法
好比在多个线程操做一个count变量的状况下,则能够把count定义为AtomicInteger,以下:性能

public class Counter {  

    private AtomicInteger count = new AtomicInteger(); 
    
    public int getCount() {   
        return count.get();    
    }    
    
    public void increment() {  
        count.incrementAndGet();   
    }
  }

在每一个线程中经过increment()来对count进行计数增长的操做,或者其它一些操做。这样每一个线程访问到的将是安全、完整的count。this

内部实现spa

采用Lock-Free算法替代锁+原子操做指令实现并发状况下资源的安全、完整、一致性;

二、Volatile

做用线程

Volatile能够看作是一个轻量级的synchronized,它能够在多线程并发的状况下保证变量的“可见性”;

什么是可见性?设计

就是在一个线程的工做内存中修改了该变量的值,该变量的值当即能回显到主内存中,从而保证全部的线程看到这个变量的值是一致的。因此在处理同步问题上它大显做用,并且它的开销比synchronized小、使用成本更低。

举个栗子:在写单例模式中,除了用静态内部类外,还有一种写法也很是受欢迎,就是Volatile+DCL:

public class Singleton { 

 private static volatile Singleton instance;  
  
 private Singleton() {  
 }  
  
 public static Singleton getInstance() {  
     if (instance == null) {  
        synchronized (Singleton.class) {  
         if (instance == null) {  
            instance = new Singleton();  
         }  
       }  
     }  
    return instance;  
    }  
}

这样单例无论在哪一个线程中建立的,全部线程都是共享这个单例的。

虽然说这个Volatile关键字能够解决多线程环境下的同步问题,不过这也是相对的,由于它不具备操做的原子性,也就是它不适合在对该变量的写操做依赖于变量自己本身。举个最简单的栗子:在进行计数操做时count++,实际是count=count+1;,count最终的值依赖于它自己的值。因此使用volatile修饰的变量在进行这么一系列的操做的时候,就有并发的问题;

举个栗子:

  • 由于它不具备操做的原子性,有可能1号线程在即将进行写操做时count值为4;而2号线程就刚好获取了写操做以前的值4,因此1号线程在完成它的写操做后count值就为5了,而在2号线程中count的值还为4,即便2号线程已经完成了写操做count仍是为5,而咱们指望的是count最终为6,因此这样就有并发的问题。
  • 而若是count换成这样:count=num+1;假设num是同步的,那么这样count就没有并发的问题的,只要最终的值不依赖本身自己。

用法

由于volatile不具备操做的原子性,因此若是用volatile修饰的变量在进行依赖于它自身的操做时,就有并发问题,如:count,像下面这样写在并发环境中是达不到任何效果的:
public class Counter {  
 private volatile int count;  

 public int getCount(){  
 return count;  
 }  
 public void increment(){  
 count++;  
 }  
}

而要想count能在并发环境中保持数据的一致性,则能够在increment()中加synchronized同步锁修饰,改进后的为:

public class Counter {  
 private volatile int count;  
  
 public int getCount(){  
 return count;  
 }  
 public synchronized void increment(){  
 count++;  
 }  
}

三、synchronized

做用

synchronized叫作同步锁,是Lock的一个简化版本,因为是简化版本,那么性能确定是不如Lock的,不过它操做起来方便,只须要在一个方法或把须要同步的代码块包装在它内部,那么这段代码就是同步的了,全部线程对这块区域的代码访问必须先持有锁才能进入,不然则拦截在外面等待正在持有锁的线程处理完毕再获取锁进入,正由于它基于这种阻塞的策略,因此它的性能不太好,可是因为操做上的优点,只须要简单的声明一下便可,并且被它声明的代码块也是具备操做的原子性。

用法

public synchronized void increment(){  
    count++;  
 }  
  
 public void increment(){  
     //同步代码块
     synchronized (Counte.class){  
         count++;  
     }  
 }

内部实现
重入锁ReentrantLock+一个Condition,因此说是Lock的简化版本,由于一个Lock每每能够对应多个Condition;

四、ThreadLocal

做用

  • 关于ThreadLocal,这个类的出现并非用来解决在多线程并发环境下资源的共享问题的,它和其它三个关键字不同,其它三个关键字都是从线程外来保证变量的一致性,这样使得多个线程访问的变量具备一致性,能够更好的体现出资源的共享。
  • 而ThreadLocal的设计,并非解决资源共享的问题,而是用来提供线程内的局部变量,这样每一个线程都本身管理本身的局部变量,别的线程操做的数据不会对我产生影响,互不影响,因此不存在解决资源共享这么一说,若是是解决资源共享,那么其它线程操做的结果必然我须要获取到,而ThreadLocal则是本身管理本身的,至关于封装在Thread内部了,供线程本身管理。

用法

通常使用ThreadLocal,官方建议咱们定义为 private static ,至于为何要定义成静态的,这和内存泄露有关,后面再讲。
它有三个暴露的方法,set、get、remove。
public class ThreadLocalDemo {  
 
 private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){  
     @Override  
     protected String initialValue() {  
         return "hello";  
     }  
 };  
 
 static class MyRunnable implements Runnable{  
     private int num;  
     
     public MyRunnable(int num){  
         this.num = num;  
     }  
     @Override  
     public void run() {  
         threadLocal.set(String.valueOf(num));  
         System.out.println("threadLocalValue:"+threadLocal.get()); 
         //手动移除
         threadLocal.remove();
     }  
 }  
  
 public static void main(String[] args){  
     new Thread(new MyRunnable(1)).start();  
     new Thread(new MyRunnable(2)).start();  
     new Thread(new MyRunnable(3)).start();  
 }  
 
}

运行结果以下,这些ThreadLocal变量属于线程内部管理的,互不影响:

threadLocalValue:1  
threadLocalValue:2  
threadLocalValue:3

对于get方法,在ThreadLocal没有set值得状况下,默认返回null,全部若是要有一个初始值咱们能够重写initialValue()方法,在没有set值得状况下调用get则返回初始值。

值得注意的一点:ThreadLocal在线程使用完毕后,咱们应该手动调用remove方法,移除它内部的值,这样能够防止内存泄露,固然还有设为static。

内部实现

ThreadLocal内部有一个静态类ThreadLocalMap,使用到ThreadLocal的线程会与ThreadLocalMap绑定,维护着这个Map对象,而这个ThreadLocalMap的做用是映射当前ThreadLocal对应的值,它key为当前ThreadLocal的弱引用:WeakReference

内存泄露问题

对于ThreadLocal,一直涉及到内存的泄露问题,即当该线程不须要再操做某个ThreadLocal内的值时,应该手动的remove掉,为何呢?咱们来看看ThreadLocal与Thread的联系图:

其中虚线表示弱引用,从该图能够看出,一个Thread维持着一个ThreadLocalMap对象,而该Map对象的key又由提供该value的ThreadLocal对象弱引用提供,因此这就有这种状况:

若是ThreadLocal不设为static的,因为Thread的生命周期不可预知,这就致使了当系统gc时将会回收它,而ThreadLocal对象被回收了,此时它对应key一定为null,这就致使了该key对应得value拿不出来了,而value以前被Thread所引用,因此就存在key为null、value存在强引用致使这个Entry回收不了,从而致使内存泄露。

因此避免内存泄露的方法,是对于ThreadLocal要设为static静态的,除了这个,还必须在线程不使用它的值是手动remove掉该ThreadLocal的值,这样Entry就可以在系统gc的时候正常回收,而关于ThreadLocalMap的回收,会在当前Thread销毁以后进行回收。

总结

关于Volatile关键字具备可见性,但不具备操做的原子性,而synchronized比volatile对资源的消耗稍微大点,但能够保证变量操做的原子性,保证变量的一致性,最佳实践则是两者结合一块儿使用。

  • 一、对于synchronized的出现,是解决多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。同步机制是提供一份变量,让全部线程均可以访问。
  • 二、对于Atomic的出现,是经过原子操做指令+Lock-Free完成,从而实现非阻塞式的并发问题。
  • 三、对于Volatile,为多线程资源共享问题解决了部分需求,在非依赖自身的操做的状况下,对变量的改变将对任何线程可见。
  • 四、对于ThreadLocal的出现,并非解决多线程资源共享的问题,而是用来提供线程内的局部变量,省去参数传递这个没必要要的麻烦,ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。ThreadLocal是为每个线程都提供了一份独有的变量,各个线程互不影响。
相关文章
相关标签/搜索