摘要

之前代码Review的时候,大佬们说到了,我的写法会引起线程安全问题,简单了解下。

正文

一、SimpleDateFormat线程安全问题

参考文章

我的写法就类似于下面这种,将SimpleDateFormat作为了静态成员变量,大佬的建议就是提取一个方法,然后通过该方法获取SimpleDateFormat对象。

1.1 parse

先看个parse案例

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    private static final String str = "2021-08-30 12:23:11";

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                try {
                    System.out.println(sdf.parse(str));
                } catch (ParseException e) {
                    e.printStackTrace();
                }

            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}

运行结果如图

1.png

通过查看源码来找出这个问题的原因。

左键点进去,发现是调用了parse(String source)方法

2.png

再次左键点进去,发现是调用了自身的抽象方法,那么就找这个抽象方法的实现类,也就是SimpleDateFormat。

3.png

通过阅读文档,大概能了解到,如果将SimpleDateFormat作为静态变量,那么就相当于是一个共享变量,而其父类DateFormat中有一个成员变量Calendar。此时,多个线程进行操作时,因为SimpleDateFormat已经作为共享变量了,所以多线程中使用的Calendar其实是同一个,此时进行clear方法,之后再进行set时,会出现各种问题。比如:

  • a线程赋值之后,b线程给clear掉,此时a线程结果不对
  • a线程赋值之后,b线程也赋值,a线程结果不对
  • a线程执行到一半的时候,b线程也执行,a线程部分数值不对

4.png

看网上说,ParsePositioin也会存在线程安全问题,但是我发现源码中,每次执行时,ParsePosition都是重新new的,所以应该不存在该问题。

5.png

1.2 format

format就很好理解了

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Test {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                Date date =  randomDate("2010-09-20", "2021-09-22");
                System.out.println(Thread.currentThread()+":"+date+"->"+sdf.format(date));

            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }

    public static Date randomDate(String beginDate, String endDate) {
        try {
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
            Date start = format.parse(beginDate);// 构造开始日期
            Date end = format.parse(endDate);// 构造结束日期
            // getTime()表示返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
            if (start.getTime() >= end.getTime()) {
                return null;
            }
            long date = random(start.getTime(), end.getTime());
            return new Date(date);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static long random(long begin, long end) {
        long rtn = begin + (long) (Math.random() * (end - begin));
        // 如果返回的是开始时间和结束时间,则递归调用本函数查找随机值
        if (rtn == begin || rtn == end) {
            return random(begin, end);
        }
        return rtn;
    }
}

运行结果

6.png

查看源码

7.png

这个就很容易想了,多个线程执行时,a线程设置了时间,b线程又设置另一个时间,就会导致问题。

1.3 解决方案

三种种主流的解决方案

  1. 使用局部变量,每次调用,都有一个对象。也就是我现在的做法,将new对象的操作提取个方法出来。这个做法有个问题,频繁地创建对象,对象在方法内不被外部调用时,方法结束,就会被回收。频繁的进行创建对象、回收对象,会影响性能。
  2. 使用 ThreadLocal。使用 ThreadLocal 实现每个线程都可以得到单独的一个SimpleDateFormat的实例。与使用局部变量的不同的是,1个请求进来是1个线程,如果是使用局部变量来处理,那么每个方法里,都需要new一个SimpleDateFormat,如果三个方法用到,就有三个对象;而用ThreadLocal的话,只会产生1个对象,1个线程1个对象。
  3. java8及其以上,可以使用DateTimeFormatter,线程安全的

使用ThreadLocal,参考Java开发手册

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Test {
    private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        SimpleDateFormat sdf = sdfThreadLocal.get();
        String strDate = sdf.format(new Date());
        System.out.println(strDate);
    }
}

使用DateTimeFormatter

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Test {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        // DateTimeFormatter 自带的格式方法
        LocalDateTime now = LocalDateTime.now();
        // DateTimeFormatter 把日期对象,格式化成字符串
        String strDate = formatter.format(now);
        System.out.println(strDate);
    }
}

二、ThreadLocal

2.1 概念

ThreadLocal类用来提供线程内部的局部变量,让这些变量在多线程访问时,能保证各个线程里的变量相对独立于其他线程内的变量。

简单理解

  1. 在多线程并发的场景下
  2. 通过ThreadLocal在同一线程不同组件中传递公共变量
  3. 每个线程的变量都是独立的,不会互相影响

2.2 使用

8.png

常用方法

  • set():将变量绑定到当前线程中
  • get():获取当前线程绑定的变量

需求

在多线程并发的场景下,实现线程隔离,也就是每个线程中的变量都是相互独立的。设置变量、取出变量都是同一个。

线程A:操作的是变量1

线程B:操作的是变量2

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Test {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Test test = new Test();
        for(int i=0;i<5;i++){
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    test.setContent(Thread.currentThread().getName()+"的数据");
                    System.out.println("==========");
                    System.out.println(Thread.currentThread().getName()+"----->"+test.getContent());
                }
            });
            thread.setName("线程"+i);
            thread.start();
        }
    }
}

这就产生了线程安全问题。

9.png

解决办法就是将content绑定到ThreadLocal,这样就能保证每个线程拿到的变量都是相互独立的

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {
    ThreadLocal<String> tl=new ThreadLocal<>();
    private String content;

    public String getContent() {
        //获取当前线程绑定的content
        return tl.get();
    }

    public void setContent(String content) {
        //变量content绑定到当前线程
        tl.set(content);
    }

    public static void main(String[] args) {
        Test test = new Test();
        for(int i=0;i<5;i++){
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    test.setContent(Thread.currentThread().getName()+"的数据");
                    System.out.println("==========");
                    System.out.println(Thread.currentThread().getName()+"----->"+test.getContent());
                }
            });
            thread.setName("线程"+i);
            thread.start();
        }
    }
}

2.3 ThreadLocal与synchronized

用synchronized同步代码块,也能达到同样的效果

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Test {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Test test = new Test();
        for(int i=0;i<5;i++){
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (Test.class){
                        test.setContent(Thread.currentThread().getName()+"的数据");
                        System.out.println("==========");
                        System.out.println(Thread.currentThread().getName()+"----->"+test.getContent());
                    }
                }
            });
            thread.setName("线程"+i);
            thread.start();
        }
    }
}

synchronized加了锁,并发执行变成了只能排队进行访问,失去了并发性,效率就会降低。

synchronizedThreadLocal
原理同步机制采用时间换空间的方式,只提供了一份变量,让不同线程排队访问ThreadLocal采用**空间换时间**的方式,为每一个线程都提供了一份变量副本,从而实现同时访问而相互不干扰。
侧重点多个线程间访问资源同步多个线程间数据相互隔离

关于这个变量副本,就相当于每个线程中new了一个变量,并且仅有一个变量。

2.4 应用场景

1.将数据库连接通过ThreadLocal绑定到线程上,从而能保证成功回滚

2.每个线程内保存一个共享的对象,比如SimpleDateFormat,就不用每用一次就new一次了

2.5 实现原理

JDK1.8以前的ThreadLocal

每个ThreadLocal都创建一个Map,然后用当前线程作为Key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果

JDK1.8以后的ThreadLocal

每个Thread内部有一个ThreadLocalMap,ThreadLocal对象本身作为Key,要存储的局部变量作为value。具体的过程

  1. 每个Thread线程内部都有一个Map,即ThreadLocalMap。如果多个线程在操作同一个threadLocal里的值,那实际上就是每个Thread里面的都有一个key为threadLocal对象,value为变量副本的map。这边动手写源码会好理解不少,可以参照下方的源码理解。
  2. Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  3. Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置线程的变量值

10.png

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class ThreadLocalTest {
//    public static void main(String[] args) {
//        ThreadLocal<Integer> threadLocal=new ThreadLocal<>();
//        //设置当前线程变量
//        threadLocal.set(1);;
//        //获取当前线程变量
//        Integer integer = threadLocal.get();
//        //获取到值之后,进行清空
//        threadLocal.remove();
//        System.out.println(integer);
//    }


    ThreadLocal<String> tl=new ThreadLocal<>();
    private String content;

    public String getContent() {
        //获取当前线程绑定的content
        return tl.get();
    }

    public void setContent(String content) {
        //变量content绑定到当前线程
        tl.set(content);
    }

    public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();
        System.out.println(test.tl);
        for(int i=0;i<5;i++){
            @SuppressWarnings("all")
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    String content=Thread.currentThread().getName()+
                        "的数据"+"======获取threadLocal====>"+
                        test.tl;
                    test.setContent(content);
                    System.out.println(Thread.currentThread().getName()+
                                       "----->"+
                                       test.getContent());
                }
            });
            thread.setName("线程"+i);
            thread.start();
        }
        /**
         * 这些线程里面,每个线程,都有一个ThreadLocalMap
         * 每个ThreadLocalMap,都存储key为java.lang.ThreadLocal@44e81672,数据为自己专有数据
         * 获取的时候,也是去当前线程里的map中,根据java.lang.ThreadLocal@44e81672来拿
         */
        Thread[] threads=new Thread[5];
        Thread.currentThread().getThreadGroup().enumerate(threads);
        System.out.println(Arrays.asList(threads));

        /**
         * 整个过程运行结果:
         *
         * java.lang.ThreadLocal@44e81672
         * 线程0----->线程0的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
         * [Thread[main,5,main], Thread[线程0,5,main], Thread[线程1,5,main], Thread[线程2,5,main], Thread[线程3,5,main]]
         * 线程2----->线程2的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
         * 线程1----->线程1的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
         * 线程3----->线程3的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
         * 线程4----->线程4的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
         */
    }
}

如此设计的好处

  1. 每个Map存储的Entry数量减少,Entry可以理解成键值对的对象。以前的方式是Map的key是存储的线程,线程越多,Entry键值对越多。现在将Map放到Thread里面,Entry数量减少。实际开发中,ThreadLocal数量往往少于Thread数量
  2. Thread销毁的时候,ThreadLocalMap也会相应的进行销毁,减少了内存的使用。

单纯的文字描述看不太懂的话,可以参照ThreadLocal的get方法源码

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * 返回该线程局部变量的当前线程副本中的值。
 * 如果变量在当前线程中没有值,它首先被初始化为initialValue方法调用返回的值。
 * 
 * @return 当前线程中对应key为threadLocal本身的值
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);// return t.threadLocals;
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

三、TransmittableThreadLocal

meethigher/transmittablethreadlocal-test: 由浅入深,简单使用transmittablethreadlocal

参考文章

3.1 ThreadLocal存在的问题

如果想要在父线程中,通过ThreadLocal赋值,子线程中取get的时候,是获取不到的。

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    threadLocal.set(1);
    /**
     * 父线程中,给threadLocal赋值,父线程的ThreadLocalMap中存储了该threadLocal
     * 在子线程的map中,没有threadLocal对象这个key,所以直接threadLocal.get()是拿不到值的
     * 如果想要在异步线程中获取父线程的值,需要通过InheritableThreadLocal
     */
    new Thread(new Runnable() {
        @Override
        public void run() {
            Integer integer = threadLocal.get();//null
            threadLocal.remove();
            System.out.println(integer);
        }
    }).start();

3.2 InheritableThreadLocal

InheritableThreadLocal可以解决在父子线程中,变量共享。

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal=new InheritableThreadLocal<>();
    threadLocal.set(1);
    /**
     * InheritableThreadLocal只适用于显示创建线程(new Thread())
     * 如果用线程池,由于是线程重复使用,就需要使用TransmittableThreadLocal
     */
    new Thread(new Runnable() {
        @Override
        public void run() {
            Integer integer = threadLocal.get();
            threadLocal.remove();
            System.out.println(integer);
        }
    }).start();
}

但是,如果使用线程池的话,由于线程池会存在复用的情况,就会读取数据混乱的情况。

类似于下面

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void main(String[] args) throws InterruptedException {
    /**
     * 使用InheritableThreadLocal在使用线程池时,会存在父子线程间传递数据混乱的问题
     * 使用TransmittableThreadLocal解决
     */
    ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
    ThreadPoolExecutor executor = getExecutor();
    CountDownLatch firstCountDownLatch = new CountDownLatch(1);
    CountDownLatch secondCountDownLatch = new CountDownLatch(1);
    threadLocal.set(1);
    executor.execute(() -> {
        System.out.println("子线程获取" + threadLocal.get());
        threadLocal.remove();
        System.out.println("异步任务1");
        firstCountDownLatch.countDown();
    });
    firstCountDownLatch.await();
    System.out.println("父线程获取" + threadLocal.get());

    threadLocal.set(2);
    executor.execute(() -> {
        System.out.println("子线程获取" + threadLocal.get());
        threadLocal.remove();
        System.out.println("异步任务2");
        secondCountDownLatch.countDown();
    });
    secondCountDownLatch.await();
    System.out.println("父线程获取" + threadLocal.get());

    executor.shutdown();

    /**
     * 输出结果:
     * 子线程获取1
     * 异步任务1
     * 父线程获取1
     * 子线程获取null
     * 异步任务2
     * 父线程获取2
     */
}

3.3 TransmittableThreadLocal

通过TransmittableThreadLocal就可以解决上面存在的问题

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static void main(String[] args) throws InterruptedException {
    ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
    ThreadPoolExecutor executor = getExecutor();
    CountDownLatch firstCountDownLatch = new CountDownLatch(1);
    CountDownLatch secondCountDownLatch = new CountDownLatch(1);
    threadLocal.set(1);
    executor.execute(TtlRunnable.get(() -> {
        System.out.println("子线程获取" + threadLocal.get());
        threadLocal.remove();
        System.out.println("异步任务1");
        firstCountDownLatch.countDown();
    }));
    firstCountDownLatch.await();
    System.out.println("父线程获取" + threadLocal.get());

    threadLocal.set(2);
    executor.execute(TtlRunnable.get(() -> {
        System.out.println("子线程获取" + threadLocal.get());
        threadLocal.remove();
        System.out.println("异步任务2");
        secondCountDownLatch.countDown();
    }));
    secondCountDownLatch.await();
    System.out.println("父线程获取" + threadLocal.get());

    executor.shutdown();

    /**
     * 运行结果:
     *
     * 子线程获取1
     * 异步任务1
     * 父线程获取1
     * 子线程获取2
     * 异步任务2
     * 父线程获取2
     */
}