什么是ThreadLocal?
先看看JDK中的源码是怎样描述的:
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).翻译过来大概是这样的:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
下面是使用 ThreadLocal 的一个完整示例:
public class ThreadLocalDemo {
private static ThreadLocal threadLocal = new ThreadLocal<>();
private static int value = 0;
public static class ThreadLocalThread implements Runnable {
@Override
public void run() {
threadLocal.set((int)(Math.random() * 100));
value = (int) (Math.random() * 100);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf(Thread.currentThread().getName() + ": threadLocal=%d, value=%d\n", threadLocal.get(), value);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadLocalThread());
Thread thread2 = new Thread(new ThreadLocalThread());
thread.start();
thread2.start();
thread.join();
thread2.join();
}
}
下面是一种可能的输出:
Thread-0: threadLocal=87, value=15
Thread-1: threadLocal=69, value=15
我们看到虽然 threadLocal 是静态变量,但是每个线程都有自己的值,不会受到其他线程的影响。
具体实现
ThreadLocal 的实现思想,我们在前面已经说了,每个线程维护一个ThreadLocalMap的映射表,映射表的 key 是 ThreadLocal 实例本身,value 是要存储的副本变量。ThreadLocal 实例本身并不存储值,它只是提供一个在当前线程中找到副本值的 key。 如下图所示:
单例模式和多线程:
在文章开始之前我们还是有必要介绍一下什么是单例模式。单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。(2)、单例需要有能力为整个系统提供这一唯一实例。
1、饿汉式单例
饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:
public class MySingleton {
private static MySingleton instance = new MySingleton();
private MySingleton(){}
public static MySingleton getInstance() {
return instance;
}
}
以上是单例的饿汉式实现,我们来看看饿汉式在多线程下的执行情况,给出一段多线程的执行代码:
public class MyThread extends Thread{
@Override
public void run() {
System.out.println(MySingleton.getInstance().hashCode());
}
public static void main(String[] args) {
MyThread[] mts = new MyThread[10];
for(int i = 0 ; i < mts.length ; i++){
mts[i] = new MyThread();
}
for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
}
}
以上代码运行结果:
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
从运行结果可以看出实例变量额hashCode值一致,这说明对象是同一个,饿汉式单例实现了。
2、懒汉式单例
懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。为了达到线程安全,又能提高代码执行效率,我们这里可以采用DCL的双检查锁机制来完成,代码实现如下:
public class MySingleton {
//使用volatile关键字保其可见性
volatile private static MySingleton instance = null;
private MySingleton(){}
public static MySingleton getInstance() {
try {
if(instance != null){//懒汉式
}else{
//创建实例之前可能会有一些准备性的耗时工作
Thread.sleep(300);
synchronized (MySingleton.class) {
if(instance == null){//二次检查
instance = new MySingleton();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
运行结果:
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
从运行结果来看,该中方法保证了多线程并发下的线程安全性。
这里在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。
什么是ConcurrentHashMap?
ConcurrentHashMap将整体分成每一个小段,内部使用端(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,它们有自己的锁。只要多个修改的操发生在不同的段上,它们就可以并发进行。如果多个修改发生在同一个端上,那就只能排队等待。把一个整体分成16个端(Segment)。也就是最高支持16个线程的并发修改操作。这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。
下面看一个例子:
public static void demo1() {
final Map count = new ConcurrentHashMap<>();
final CountDownLatch endLatch = new CountDownLatch(2);
Runnable task = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
Integer value = count.get("a");
if (null == value) {
count.put("a", 1);
} else {
count.put("a", value + 1);
}
}
endLatch.countDown();
}
};
new Thread(task).start();
new Thread(task).start();
try {
endLatch.await();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}
}
demo1是两个线程操作ConcurrentHashMap,意图将value变为10。但是,因为多个线程用相同的key调用时,很可能会覆盖相互的结果,造成记录的次数比实际出现的次数少。
当然可以用锁解决这个问题,但是也可以使用ConcurrentMap定义的方法:
V putIfAbsent(K key, V value)
//如果key对应的value不存在,则put进去,返回null。否则不put,返回已存在的value。
boolean remove(Object key, Object value)
//如果key对应的值是value,则移除K-V,返回true。否则不移除,返回false。
boolean replace(K key, V oldValue, V newValue)
//如果key对应的当前值是oldValue,则替换为newValue,返回true。否则不替换,返回false。
于是对demo1进行改进:
public static void demo1() {
final Map count = new ConcurrentHashMap<>();
final CountDownLatch endLatch = new CountDownLatch(2);
Runnable task = new Runnable() {
@Override
public void run() {
Integer oldValue, newValue;
for (int i = 0; i < 5; i++) {
while (true) {
oldValue = count.get("a");
if (null == oldValue) {
newValue = 1;
if (count.putIfAbsent("a", newValue) == null) {
break;
}
} else {
newValue = oldValue + 1;
if (count.replace("a", oldValue, newValue)) {
break;
}
}
}
}
endLatch.countDown();
}
};
new Thread(task).start();
new Thread(task).start();
try {
endLatch.await();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}
}
由于ConcurrentMap中不能保存value为null的值,所以需要处理不存在和已存在两种情况,不过可以使用AtomicInteger来替代。
public static void demo1() {
final Map count = new ConcurrentHashMap<>();
final CountDownLatch endLatch = new CountDownLatch(2);
Runnable task = new Runnable() {
@Override
public void run() {
AtomicInteger oldValue;
for (int i = 0; i < 5; i++) {
oldValue = count.get("a");
if (null == oldValue) {
AtomicInteger zeroValue = new AtomicInteger(0);
oldValue = count.putIfAbsent("a", zeroValue);
if (null == oldValue) {
oldValue = zeroValue;
}
}
oldValue.incrementAndGet();
}
endLatch.countDown();
}
};
new Thread(task).start();
new Thread(task).start();
try {
endLatch.await();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}
}
什么是CopyOnWrite容器?
Copy-on-write简称COW,是一种用于程序设计中的优化策略。JDK里的COW容器有两种:CopyOnWriteArrayList和CopyOnWriteArraySet,COW容器非常有用,可以在非常多的并发场景中使用到。
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的往容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWrite适合在读多写少的场景下使用;CopyOnWrite读的时候是不需要加锁,在读写的时候是加入重入锁。
下面看CopyOnWriteMap的一个例子:
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public class CopyOnWriteMap implements Map, Cloneable {
private volatile Map internalMap;
public CopyOnWriteMap() {
internalMap = new HashMap();
}
public V put(K key, V value) {
synchronized (this) {
Map newMap = new HashMap(internalMap);
V val = newMap.put(key, value);
internalMap = newMap;
return val;
}
}
public V get(Object key) {
return internalMap.get(key);
}
public void putAll(Map extends K, ? extends V> newData) {
synchronized (this) {
Map newMap = new HashMap(internalMap);
newMap.putAll(newData);
internalMap = newMap;
}
}
}
既然CopyOnWrite在进行写操作的时候要进行数组的复制,性能和内存开销比较大,因此它更适用于读多写少的操作,例如缓存。
进行写操作时尽量使用使用CopyOnWriteArrayList.addAll()方法,来避免多次反复写入。