实现多线程安全的三种方法,
1. 使用锁机制 synchronize、lock方式:为资源加锁,可参考我前面文章。
2. 使用 java.util.concurrent 下面的类库:有JDK提供的线程安全的集合类:AtomicInteger、AtomicStampedReference、ConcurrentHashMap、CopyOnWriteArrayList、ReentrantLock,FutureTask,CountDownLatch。
3. 多实例、或者是多副本(ThreadLocal):ThreadLocal可以为每个线程的维护一个私有的本地变量。
这里主要讲讲ThreadLocal:
ThreadLocal解决多线程的并发问题的思路很简单:就是为每一个线程维护一个副本变量,从而让线程拥有私有的资源,就不再需要去竞争进程中的资源。每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
在多线程环境下,如何防止自己的变量被其他线程篡改?
api:
void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
T get():返回此线程局部变量的当前线程副本中的值。
void remove():移除此线程局部变量当前线程的值。
1、ThreadLocal 的底层实现是用ThreadLocalMap(它是ThreadLocal的静态内部类,ThreadLocalMap中的Entry继承WeakReference),每一个线程都存在一个ThredLocalMap,其中的元素的 key 值为当前的ThreadLocal对象,而值对应线程的变量副本 。当调用 get、set 方法时,使用的是当前在运行的线程的ThreadLocalMap对象,从而可以获取、设置当前在运行的线程的私有变量。
部分源码:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
2、当我们在调用get()方法的时候,先获取当前线程,然后获取到当前线程的ThreadLocalMap对象,再根据当前的ThreadLocal对象,获取到Entry,如果非空,那么取出ThreadLocal的value,否则进行初始化,初始化就是将initialValue的值set到ThreadLocalMap中。
部分源码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue(); //null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
3、当我们调用set()方法的时候,很常规,就是将值设置进ThreadLocal中。跟get()方法差不多。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
eg:
public class MyTest {
//一般将ThreadLocal的对象修饰为static对象,方便多个线程共享
public static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask());
Thread t2 = new Thread(new MyTask());
Thread t3 = new Thread(new MyTask());
//启动3个线程
t1.start();
t2.start();
t3.start();
}
}
class MyTask implements Runnable{
@Override
public void run() {
MyTest.threadLocal.set("0");
//线程计算5次
for(int i=1;i<5;i++){
MyTest.threadLocal.set( MyTest.threadLocal.get().toString()+i);
}
System.out.println("线程"+Thread.currentThread().getName()+"的计算结果:"+MyTest.threadLocal.get());
}
}
运行结果:
线程Thread-1的计算结果:01234
线程Thread-0的计算结果:01234
线程Thread-2的计算结果:01234
是不是感觉好神奇!3个线程都是使用同一个 ThreadLocal的静态变量对象 来存储或者获取值,但是3个线程的计算结果却是互不影响,相互独立。 没错,这就是ThreadLocal的解决并发的方式,它为每个线程维护着一个线程的私有变量,同时,它又可以被所有线程所共享。
ThreadLocalMap源码:在插入过程中,
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.
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> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下: 1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上; 2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value; 3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;
长度和hashMap的长度相同(16),但是数组中的元素Entry不是一个链表。
Entry源码(ThreadLocalMap的静态内部类):
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
内存泄漏问题:程序申请内存之后,无法释放已经申请的内存空间,也无法使用该内存空间,多次内存泄漏就会导致内存溢出。
可能会发生内存泄漏,由于Entry中的key是弱引用的,如果发生GC那么key就会被回收掉,此时ThreadLocalMap中就会存在key为null的Entry,但却无法访问key为null的Entry的value值,如果线程还未结束的话,该Entry就不会回收,发生内存泄漏。
解决内存泄漏问题:
调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象(get方法如果没有找到对应的Entry那么就会重新设置给ThreadLocal设置一个null值,set方法可能会将原来的Entry覆盖掉),这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
WeakHashMap如何解决内存泄漏问题:
WeakHashMap$Entry:
private static class Entry extends WeakReference
解决内存泄漏问题主要是expungeStaleEntries方法,它将去翻找ReferenceQueue(存放GC后的Entry对象)中的Entry,然后去WeakHashMap的entry数组中找到索引,然后从对应的链中设置相关的Entry的value为null,到这里就完成了相关数据的清理,put、get、remove、size等方法都能够触发expungeStaleEntries方法,但如果不调用这些方法那么可能会发生内存泄漏。