推荐:Java并发编程汇总
Java并发编程一ThreadLocal初使用
任务
为了方便使用以及展现ThreadLocal
的优点,这里首先给出一个任务,然后不断地去加大任务难度,再根据具体任务去迭代代码,到最后引出ThreadLocal
;我们假设每个线程的任务很简单,就是打印日期(线程给出秒数即可)。
现在我们只有2
个打印日期的任务。
代码一
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 描述:2个线程打印日期
*/
public class ThreadLocalNormalUsage00 {
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(104707);
System.out.println(date);
}
}).start();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
输出:
1970-01-02 13:05:07
1970-01-01 08:00:10
这种情况显然是线程安全的(栈封闭)。
现在我们要加大任务难度了,我们有10
个打印日期的任务。
代码二
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 描述:10个线程打印日期
*/
public class ThreadLocalNormalUsage01 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
Thread.sleep(100);
}
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
输出:
1970-01-01 08:00:00
1970-01-01 08:00:01
1970-01-01 08:00:02
1970-01-01 08:00:03
1970-01-01 08:00:04
1970-01-01 08:00:05
1970-01-01 08:00:06
1970-01-01 08:00:07
1970-01-01 08:00:08
1970-01-01 08:00:09
这种情况也是线程安全的。
现在我们的任务又要加大难度了,我们有1000
个打印日期的任务,并且使用线程池。
代码三
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述:1000个打印日期的任务,用线程池来执行
*/
public class ThreadLocalNormalUsage02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
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();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
输出:
1970-01-01 08:16:36
1970-01-01 08:16:37
1970-01-01 08:16:38
1970-01-01 08:16:39
1970-01-01 08:15:33
1970-01-01 08:16:19
1970-01-01 08:16:21
1970-01-01 08:16:18
1970-01-01 08:16:06
1970-01-01 08:15:59
1970-01-01 08:15:57
1970-01-01 08:15:56
1970-01-01 08:15:53
这里只是一部分输出,因为肯定会输出一千行日期,多线程下线程的执行顺序是不确定的,所以上面输出的顺序也是不确定的,但是这些输出都是不一样的,因为这种情况是线程安全的。
有没有发现一个问题,线程每次调用date()
都会重新创建一个SimpleDateFormat
实例,资源重用率太低了,每调用date()
一次就创建SimpleDateFormat
实例一次,在调用date()
的需求很大时,这样频繁的创建、使用、再被GC
回收,对系统、性能等都是不友好的,这里是可以优化的。
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
迭代一
这里创建了一个静态的SimpleDateFormat
实例,方便多个线程执行任务时使用(大家想想看,会有什么问题?)。
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述:1000个打印日期的任务,用线程池来执行
*/
public class ThreadLocalNormalUsage03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
输出:
1970-01-01 08:16:36
1970-01-01 08:16:36
1970-01-01 08:16:33
1970-01-01 08:16:30
1970-01-01 08:16:26
1970-01-01 08:16:26
输出也只截取了一部分,很明显这里的输出有相同的,因为我们创建静态的SimpleDateFormat
实例,方便多个线程执行任务时使用,导致了线程安全问题,因为之前的SimpleDateFormat
实例是栈封闭的(定义在方法里面),是每个线程独享的,没有多个线程去操作它。
迭代二
为了解决这种线程安全问题,我们对可能造成线程安全问题的地方加锁,这里就用synchronized
代码块的形式,当然也可以用Lock
相关的方法去实现。
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述:加锁来解决线程安全问题
*/
public class ThreadLocalNormalUsage04 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage04().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
return s;
}
}
输出:
1970-01-01 08:16:25
1970-01-01 08:15:28
1970-01-01 08:16:39
1970-01-01 08:15:54
1970-01-01 08:15:59
1970-01-01 08:16:38
1970-01-01 08:16:03
1970-01-01 08:16:37
1970-01-01 08:16:36
1970-01-01 08:16:31
显然这种情况是线程安全的,但使用了synchronized
代码块,性能肯定会受到影响。
迭代三
最后我们使用ThreadLocal
来进行优化,大家先不用去关心ThreadLocal
的最佳实践问题(用什么修饰符去修饰变量更好等问题)。
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
*/
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
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();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
输出:
1970-01-01 08:16:32
1970-01-01 08:16:33
1970-01-01 08:16:24
1970-01-01 08:16:34
1970-01-01 08:16:23
1970-01-01 08:16:20
1970-01-01 08:16:38
1970-01-01 08:16:19
1970-01-01 08:16:21
1970-01-01 08:16:17
1970-01-01 08:16:39
1970-01-01 08:16:37
1970-01-01 08:16:36
1970-01-01 08:16:35
1970-01-01 08:16:31
1970-01-01 08:16:30
这种情况当然也是线程安全的,因为每个线程都会使用自己的SimpleDateFormat
实例,又因为线程池中的每一个线程都可能会执行多个任务,所以一个SimpleDateFormat
实例就可以用于多个任务的执行,而不会造成线程安全问题,并且不需要使用synchronized
代码块,性能会有很大的提升,ThreadLocal
是一种以空间换时间的思想,但结合线程池这种复用技术,这种空间也可以得到很大程度的复用。
下面我们来解释一下上面的代码。
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
进入get()
。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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()
是对当前运行这个get()
方法的线程来操作的,而跟其他的线程无关,这也是避免线程安全问题的关键所在。
Thread t = Thread.currentThread();
有些细节大家先不用去关心,等看完这篇博客,再去看下面这篇博客可能就会理解了,不过还是建议大家去看看源码。
ThreadLocal-面试必问深度解析
很显然当前线程的ThreadLocalMap
是null
,因为之前并没有放任何数据,所以会调用setInitialValue()
。
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
很明显这里调用了我们重写的initialValue()
,我们重写的initialValue()
便会返回一个SimpleDateFormat
实例,之后会将这个SimpleDateFormat
实例放入当前线程的ThreadLocalMap
中。
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
这种定义方法也是类似的。
/**
* Creates a thread local variable. The initial value of the variable is
* determined by invoking the {@code get} method on the {@code Supplier}.
*
* @param <S> the type of the thread local's value
* @param supplier the supplier to be used to determine the initial value
* @return a new thread local variable
* @throws NullPointerException if the specified supplier is null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
/**
* An extension of ThreadLocal that obtains its initial value from
* the specified {@code Supplier}.
*/
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
到最后还是重写了initialValue()
,initialValue()
会调用supplier.get()
,也就是我们写的Lambda
表达式。