无论是在面试还是在平时得web开发中,我们经常会遇到线程安全问题。在不同线程去操作共享数据得时候(mysql、redis、jvm内存中的数据),必定会涉及到数据安全问题。mysql有自己的行锁来确保安全性。redis是基于内存的单线程数据库,所以不会存在线程安全问题。在jvm中的数据如何保证线程操作的安全性呢?
1、volatile + cas保证线程安全
volatile规定的缓存一致性与有序性,在大部分形况下已经可以保证线程安全了。
而cas更不必多说,在java并发包下的所有的类的实现都是基于抽象队列同步器去实现,而抽象队列同步器就是用cas去做的。
对于这一块可以参考我前面分析的读写锁的分析哪一块
2、synchronize锁
保证线程安全最简单最粗暴的方式就是加锁了。但是加锁也就导致系统并发度降低~~
3、java.util.concurrent
这个包里面提供了一系列的线程安全的类。我们可以使用这些去替代不安全的类。
ConcurrentHashMap map
ReentrantLock 锁—共享锁与排它锁
CompletableFuture 线程编排的类
CopyOnWriteArrayList list
CopyOnWriteArraySet set
CountDownLatch 闭锁
DelayQueue 延时队列
Executors 线程的工具类
Semaphore 信号量
atomic包下的一些类
4、ThreadLocal
threadlocal同样也可以保证线程安全,因为他是将数据保存在了本线程变量中。避免了其他线程操作这个变量。那么就不存在跟其他线程的数据共享问题。
我们就仔细分析下ThreadLocal的实现原理
ThreadLocal的数据存储
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取map 这个map是Thread类中的一个变量 threadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先看下这个createMap是怎么创建的
void createMap(Thread t, T firstValue) {
//初始化了一个Map,并且本线程持有这个map的引用
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//map的实例化 firstKey:ThreadLocal firstValue 存放的值
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化一个entry数组 类似与hashMap的数组结点
table = new Entry[INITIAL_CAPACITY];
//计算threadLocal所在的下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//实例化一个entry结点,放在数据中
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置entry的扩容阙值
setThreshold(INITIAL_CAPACITY);
}
创建好了map那么下一次就会直接使用
再来看下如果 有map的话,值是怎么添加保存的
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
//上面提到了get()会使用快速路径? 什么是快速路径? 后面在分析
Entry[] tab = table;
int len = tab.length;
//计算索引
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//获取threadLocal 这里是一个弱引用 ---> 方便gc
ThreadLocal<?> k = e.get();
//这个位置的threadLocal就是我们正在设置的threadLocal
if (k == key) {
//直接将结点的值替换成新的
e.value = value;
return;
}
//如果k为null,entry结点不为null,但是它持有的threadLocal却为null?
if (k == null) {
//处理~~~
//简单来说就是再一次判断数组中是否有entry存储的threadLocal是我们处理的这个
//没有新增一个entry放在数组中
//最后判断i索引之后(之前)的结点不为空,并且结点持有的threadLocal为null 那么就删除这个结点
replaceStaleEntry(key, value, i);
return;
}
}
//创建一个新的结点 到了这里说明这个threadLocal肯定没有在本线程中存储过值
tab[i] = new Entry(key, value);
int sz = ++size;
//判断i索引之后的结点不为空,并且结点持有的threadLocal为null 那么就删除这个结点
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
上面为什么会有entry结点不为空,但是entry却没有对应的threadLocal? 人为将threadLocal设置为null?
最后看一下get取值
public T get() {
Thread t = Thread.currentThread();
//获取线程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//计算threadLocal的索引,拿到对应的entry结点
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取值
T result = (T)e.value;
return result;
}
}
//没有map初始化一个map并且设置为threadLocal对应的entry结点,值设置为null
return setInitialValue();
}
再来看一下threadLocal、map、thread、entry之间的关系