前言

对于ThreadLocal、Volatile、synchronized、Atomic这四个关键字,我想一提及到大家必定都想到的是解决在多线程并发环境下资源的共享问题,然而要细说每一个的特点、区别、利用场景、外部实现等,却可能模糊不清,说不出个所以然来,所以,本文就对这几个关键字做一些作用、特点、实现上的解说。

1、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的实现有三步:

  • 1、循环(for(;;)、while)
  • 2、CAS(CompareAndSet)
  • 3、回退(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。

外部实现

采纳Lock-Free算法代替锁+原子操作指令实现并发状况下资源的平安、残缺、一致性;

2、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++;   }  }

3、synchronized

作用

synchronized叫做同步锁,是Lock的一个简化版本,因为是简化版本,那么性能必定是不如Lock的,不过它操作起来不便,只须要在一个办法或把须要同步的代码块包装在它外部,那么这段代码就是同步的了,所有线程对这块区域的代码拜访必须先持有锁能力进入,否则则拦挡在里面期待正在持有锁的线程处理完毕再获取锁进入,正因为它基于这种阻塞的策略,所以它的性能不太好,然而因为操作上的劣势,只须要简略的申明一下即可,而且被它申明的代码块也是具备操作的原子性。

用法

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

外部实现
重入锁ReentrantLock+一个Condition,所以说是Lock的简化版本,因为一个Lock往往能够对应多个Condition;

4、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对资源的耗费略微大点,但能够保障变量操作的原子性,保障变量的一致性,最佳实际则是二者联合一起应用。

  • 1、对于synchronized的呈现,是解决多线程资源共享的问题,同步机制采纳了“以工夫换空间”的形式:拜访串行化,对象共享化。同步机制是提供一份变量,让所有线程都能够拜访。
  • 2、对于Atomic的呈现,是通过原子操作指令+Lock-Free实现,从而实现非阻塞式的并发问题。
  • 3、对于Volatile,为多线程资源共享问题解决了局部需要,在非依赖本身的操作的状况下,对变量的扭转将对任何线程可见。
  • 4、对于ThreadLocal的呈现,并不是解决多线程资源共享的问题,而是用来提供线程内的局部变量,省去参数传递这个不必要的麻烦,ThreadLocal采纳了“以空间换工夫”的形式:拜访并行化,对象独享化。ThreadLocal是为每一个线程都提供了一份独有的变量,各个线程互不影响。