自己写一个类,实现两个线程分别打印出时间信息。
public class ThreadLocalNormalUsage00 {
public String date(int seconds){
//Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(10);
System.out.println(date);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(1007);
System.out.println(date);
}
}).start();
}
}
如果需要写100个呢?那就要在代码中写100个线程和100个SimpleDateFormat,写法不优雅。
public static void main(String[] args) {
for (int i=0;i<100;i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
}
}
用for循环去写100、1000个,那么就需要100次的创建销毁线程,造成很大的开销。所以此时可以引入线程池。
public static void main(String[] args) {
for (int i=0;i<1000;i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage02().date(finalI);
System.out.println(date);
}
});
threadPool.shutdown();
}
}
这时候便不会创建1000个线程那么多了,但是还是会创建1000个SimpleDateFormat 对象。如果只创建一个SimpleDateFormat 实例,每次都重复利用就可以节省开销。
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public String date(int seconds){
//Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
就比如这样,将SimpleDateFormat 作为一个静态的属性,但是,这样在运行的时候却发现,存在着相同的时间。
但每次传入的值都是不同的,怎么会有相同的时间?这是因为出现了线程安全问题。如果要解决,是不是可以通过加锁的方式来解决。
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public String date(int seconds){
//Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
Date date = new Date(1000 * seconds);
String format = null;
synchronized (ThreadLocalNormalUsage04.class){
format = dateFormat.format(date);
}
return format;
}
这样一来,最后的结果就是正确的了。不过这样,所有的线程串行执行,效率就很低了。
如果在每个线程内部使用一个SimpleDateFormat ,这样既可以保证效率,又不会出现线程安全问题了。
//利用ThreadLocal给每个线程分配自己的SimpleDateFormat对象
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public String date(int seconds){
//Date()参数单位是毫秒,从1970.1.1 00:00:00 计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
public static void main(String[] args) {
for (int i=0;i<1000;i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
像如图这种需求,从service-1一直到service-4,每次都需要传递user。这样会导致代码冗余且不易维护。
我们可以在每个线程内保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。
可以使用一个静态的ThreadLocal,在线程的生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象作为参数传递的麻烦。
//演示ThreadLocal用法二,避免传参
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1{
public void process(){
User user = new User("超哥");
System.out.println("服务一:"+user.Name);
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2{
public void process(){
User user = UserContextHolder.holder.get();
System.out.println("服务二:"+user.Name);
new Service3().process();
}
}
class Service3{
public void process(){
User user = UserContextHolder.holder.get();
System.out.println("服务三:"+user.Name);
}
}
class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
在ThreadLocal第一次get的时候就把对象初始化出来,对象的初始化时机可以由我们控制。
使用set的方式设置对象,可以在对象初始化之后再放入ThreadLocal。
每个Thread对象中都持有一个ThreadLocalMap成员变量。
ThreadLocal.ThreadLocalMap threadLocals = null;
一个ThreadLocalMap中会存有多个ThreadLocal,这些ThreadLocal作为Map的key,其中对应的值作为value。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
观察get()方法的源码可以发现,当这个线程中没有ThreadLocalMap或者其中没有对应此ThreadLocal的Entry,便会返回setInitialValue()。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
在setInitialValue()中,会调用initialValue(),并且将得到的值放到map中,最后返回value。
为这个线程设置一个新的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
功能:得到这个线程对应的value。
get()方法先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value。
删除当前线程对应的值。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
this指的是当前的ThreadLocal对象。
内存泄漏:某个对象不再有用,但是占用的内存却不能被回收。
ThreadLocalMap中的Entry继承自WeakReference,是弱引用。
弱引用的特点是:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
但是在Entry中,value又是强引用。
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。但是如果线程不终止(比如线程池里的),那么key对应的value就不能被回收。
JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null。这样value对象就可以被回收了。
可是如果一个ThreadLocal不再被使用了,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏。
调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏。所以使用完ThreadLocal之后,应该调用remove方法。
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set(){
longThreadLocal.set(Thread.currentThread().getId());
}
public long get(){
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
}
});
thread1.start();
}
}
如果我们不set()直接get(),按理来说会返回null。但是这个代码中却会报错,这是因为get()方法的返回类型是long,而不是Long。
所以正确写法是将get()的返回类型设置为Long。
如果set进去的本来就是一个多线程共享的对象(static),那么还是会出现线程安全问题。
很多地方用到,比如在DateTimeContextHolder类中使用到,还有RequestContextHolder等。
每次HTTP请求都对应一个线程,线程之间互相隔离,这就是ThreadLocal的典型应用场景。