首页 > 编程语言 >Java并发编程一ThreadLocal初使用

Java并发编程一ThreadLocal初使用

时间:2022-11-09 18:39:38浏览次数:47  
标签:01 Java 16 08 编程 ThreadLocal date new public


推荐:​​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​​表达式。


标签:01,Java,16,08,编程,ThreadLocal,date,new,public
From: https://blog.51cto.com/u_15870611/5837709

相关文章

  • Java并发编程一引用类型、升级类型原子类初使用加源码分析
    推荐:​​Java并发编程汇总​​Java并发编程一引用类型、升级类型原子类初使用加源码分析首先我们来看一看有哪些原子类。现在我们来看看该如何去使用这些引用类型、升级类......
  • Java并发编程一ReentrantReadWriteLock初使用
    推荐:​​Java并发编程汇总​​Java并发编程一ReentrantReadWriteLock初使用​​ReentrantReadWriteLock​​是一种读写锁,从类名也可以看出来。​​ReentrantReadWriteLock​......
  • Java中String被称为不可变字符串的原因
    很多东西,看似可变,实际上不过是是新桃换旧符罢了。 代码:/***String之所以被称为不可变字符串*/staticvoidtestString(){Stringstr="i......
  • mac下java和mvn的环境配置
    原文:https://blog.csdn.net/w605283073/article/details/111770386   https://www.pudn.com/news/62f8c6905425817ffc462029.htmlmvn打包报错:Nocompilerisprov......
  • LINQ编程总结
        LINQtoSQL查询表达式适用于关系数据库,查询表达式是基于对象的,它要求将数据库表和试图映射到实体,LINQTOSQL让数据库的操作变得更加的简单,完全弱化了数据库......
  • 计算机网络应用层:DNS、P2P和Socket编程
    DNS域名系统(DomainNameSystem,DNS)的主要任务是主机名到IP地址的转换的目录服务。DNS是:一个由分层DNS服务器实现的分布式数据库;一个使得主机能够查询分布式数据库的应......
  • JAVA遍历Map所有元素
    //JDK1.5Mapmap=newHashMap();for(Objectobj:map.keySet()){Objectkey=obj;Objectvalue=map.get(obj);}123456//JDK1.4......
  • java 串口工具jSerialComm
    由于项目之前用的串口工具RXTX只支持到jdk1.8然后项目目前用到的jdk是13的所以在网上找了一下找到了这个 jSerialComm目前使用是支持13及1.8的没做其它jdk版本测试......
  • Java线程安全
    线程安全的本质其实第一张图的例子是有问题的,主内存中的变量是共享的,所有线程都可以访问读写,而线程工作内存又是线程私有的,线程间不可互相访问。那在多线程场景下,图上的线程......
  • Caused by: java.lang.NoClassDefFoundError: net/minidev/asm/FieldFilter 报错的解
    Causedby:org.springframework.beans.factory.BeanCreationException:Errorcreatingbeanwithname'requestMappingHandlerAdapter'definedinclasspathresourc......