SimpleDateFormat的线程不安全性
现在有一个需求:在多线程环境下去格式化时间。那么我们就需要SimpleDateFormat 类
public class Thread2 {
public static ExecutorService pool = Executors.newFixedThreadPool(10);
//类变量
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//每个线程获取时间的方法
private String handlerDate(int seconds) {
Date date = new Date(1000 * seconds);
return sdf.format(date);
}
@Test
public void testThreadLocal() throws InterruptedException{
for(int i = 0; i < 1000; i++) {
final int curIndx = i;
pool.submit(new Runnable() {
@Override
public void run() {
String date = new Thread2().handlerDate(curIndx);
System.out.println(date);
}
});
}
Thread.sleep(5000);
pool.shutdown();
}
}
我们会发现输出里出现了重复的时间格式化内容,这是因为SimpleDateFormat是一个线程不安全的类,其实例对象在多线程环境下作为共享数据,会发生线程不安全问题。
我们可以通过synchronized关键字来限制handlerDate方法:
private String handlerDate(int seconds) {
Date date = new Date(1000 * seconds);
String format;
synchronized (ThreadLocalUsage02.class) {
format = sdf.format(date);
}
return format;
}
不过锁会带来性能的下降,因此我们可以使用其他的方法来解决:ThreadLocal。
ThreadLocal
ThreadLocal不是线程,更不是本地线程,而是Thread的局部变量,它是每个线程独享的本地变量,每个线程都有自己的ThreadLocal,它们是线程隔离的。
示例1
下面来用ThreadLocal来解决SimpleDateFormat类的问题
public class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH hh:mm:ss");
}
};
}
修改上例handlerDate方法
private String handlerDate(int seconds) {
Date date = new Date(1000 * seconds);
//获取sdf
SimpleDateFormat sdf = ThreadSafeFormatter.dateFormatThreadLocal.get();
return sdf.format(date);
}
测试方法不变,运行后会发现不会出现重复的问题。
ThreadLocal将SimpleDateFormat对象用ThreadLocal包装了一层,使得多个线程内部都有一个SimpleDateFormat对象副本,每个线程使用自己的SimpleDateFormat,这样就不会产生线程安全问题了。
示例2
假设我们有一个学生类
public class Student {
String name;
public Student(String name) {
this.name = name;
}
}
我们需要将某一学生信息在线程内的所有方法中共享,因此我们可以将Student对象作为方法参数来进行传递。
class Service1 {
public void process(Student student) {
System.out.println(student.name);
}
}
class Service1 {
public void process(Student student) {
System.out.println(student.name);
}
}
class Service1 {
public void process(Student student) {
System.out.println(student.name);
}
}
但这样做会产生代码冗余问题,并且可维护性差。此外你可以使用HashMap来存储学生信息,这样后续的使用直接get方法即可。
但是在多线程环境下需要使用ConcurrentHashMap,但它所使用的CAS和锁机制会产生性能问题。
因此我们可以使用ThreadLocal来实现不同方法的资源共享
@Test
public void testThreadLocal2() {
new Service1().process();
}
static class StudentThreadLocal {
public static ThreadLocal<Student> studentLocal = new ThreadLocal<>();
}
class Service1 {
//设置名字并往后传递
public void process() {
Student stu = new Student("gua");
//将User对象存储到 holder 中
StudentThreadLocal.studentLocal.set(stu);
new Service2().process();
}
}
class Service2 {
public void process() {
Student stu = StudentThreadLocal.studentLocal.get();
System.out.println("Service2拿到学生名: " + stu.name);
new Service3().process();
}
}
class Service3 {
public void process() {
Student stu = StudentThreadLocal.studentLocal.get();
System.out.println("Service3拿到学生名: " + stu.name);
}
}
原理
首先Thread类里维护了ThreadLocalMap成员变量
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
ThreadLocal类里有一个静态内部类ThreadLocalMap。而在ThreadLocalMap类里有一个Entry类,它继承了ThreadLocal类的弱引用,并将其作为key,value为Object类型。
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 默认的数组初始化容量
private static final int INITIAL_CAPACITY = 16;
// Entry数组,大小必须为2的幂
private Entry[] table;
// 数组内部元素个数
private int size = 0;
// 数组扩容阈值,默认为0,创建了ThreadLocalMap对象后会被重新设置
private int threshold;
//构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化Entry数组,大小 16
table = new Entry[INITIAL_CAPACITY];
// 用第一个键的哈希值对初始大小减一取模得到索引,和HashMap计算桶下标的原理一样。
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 将Entry对象存入数组指定位置
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 初始化扩容阈值,第一次设置为10
setThreshold(INITIAL_CAPACITY);
}
}
}
总结来说:
- 线程类Thread内部有成员变量:类型是ThreadLocalMap的threadLocals
- ThreadLocalMap类里有一个内部类Entry,ThreadLocalMap持有属性:Entry数组table负责存储键值对,其中key是ThreadLocal的弱引用,value是要存储的对象。
- ThreadLocalMap是ThreadLocal的内部类,我们在前面通过ThreadLocal对象来使用的set,get方法主要调用了其内部类ThreadLocalMap的方法。
set方法
该方法获取当前线程的成员变量threadLocals,并将threadLocals - value作为键值对存储在Entry数组talbe里
public class ThreadLocal<T> {
public void set(T value) {
//获取调用此方法的线程
Thread t = Thread.currentThread();
//获取t的成员变量threadLocals(ThreadLocal.ThreadLocalMap threadLocals)
ThreadLocalMap map = getMap(t);
//若map不为空,则说明当前线程内部已经有ThreadLocalMap对象,则将成员变量threadLocals对象作为键,存入的value作为值存储到ThreadLocalMap中
if (map != null)
map.set(this, value);
// 否则创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 创建一个ThreadLocalMap对象并将值存入其中,并赋值给当前线程的threadLocals成员变量
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
下面是存储键值对的set方法。它将键值对存储在Entry数组table里。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//计算当前ThreadLocal对象作为键在Entry数组中的下标索引
int i = key.threadLocalHashCode & (len-1);
//获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果是同一对象,则设置新值,返回
if (k == key) {
e.value = value;
return;
}
//如果不是同一对象,则判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//通过循环中的nextIndex找到下一个合适的位置并放入键值对
tab[i] = new Entry(key, value);
int sz = ++size;
//判断是否要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//如果当前i是最后一个,则从数组头部重新找。这种方式是开放寻址法的应用(而HashMap采用的是链表方式存储哈希冲突)
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
get方法
我们通过该方法获取存储在当前线程的成员变量threadLocals里的值
public T get() {
Thread t = Thread.currentThread();
//当前线程t的成员变量threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取En
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果没找到或Entry数组为空,则进行初始化:将当前ThreadLocal对象作为key,null作为value。
//然后存入到当前线程的ThreadLocalMap对象中
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue(); //返回null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //返回当前线程的成员变量threadLocals
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove方法
// 获取当前线程的成员变量threadLocals,它将作为key来查找Entry数组里的value值。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap内存泄露问题
首先简单介绍一下java的四大引用
(1)强引用:java默认的引用类型。比如String str = new String("jnju");,其中str就是一个强引用。一个对象如果具有强引用那么只要这种引用还存在就不会被回收。
(2)软引用:如果一个对象具有软引用,在JVM发生内存溢出之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会调用垃圾回收期回收掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
(3)弱引用:如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。
弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象被回收掉之后,再调用get方法就会返回null。
(4)虚引用:所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。
我们再回到存放键值对的类Entry:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
由此可知:ThreadLocal的弱引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法,可以理解ThreadLocalMap中的键是ThreadLocal的弱引用。
内存泄漏是指某个对象不会再被使用,但是该对象的内存却无法被收回
正常情况下 当Thread运行结束后,ThreadLocal中的value会被回收,因为没有任何强引用了。
但是在非正常情况下,当Thread一直在运行始终不结束,强引用就不会被回收,此时存在以下调用链 Thread-->ThreadLocalMap-->Entry(key为null)-->value 因为调用链中的 value 和 Thread 存在强引用,所以value无法被回收,就有可能出现OOM。
JDK的设计考虑到了这个问题,所以在set()、remove()、resize()方法中会扫描到key为null的Entry,并且把对应的value设置为null,这样value对象就可以被回收。但是只有在调用set()、remove()、resize()这些方法时才会进行这些操作,如果没有调用这些方法并且线程不停止,那么调用链就会一直存在,所以可能会发生内存泄漏。
因此z在使用完ThreadLocal后,要调用remove()方法。