共计 5182 个字符,预计需要花费 13 分钟才能阅读完成。
前言
对于 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 是为每一个线程都提供了一份独有的变量,各个线程互不影响。