Java基础知识之ThreadLocal

一.ThreadLocal 变量定义:
ThreadLocal 是Java里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。

二.API中的解释:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
例如,以下类生成对每个线程唯一的局部标识符。线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,在后续调用中不会更改。

  import java.util.concurrent.atomic.AtomicInteger;

 public class UniqueThreadIdGenerator {

     private static final AtomicInteger uniqueId = new AtomicInteger(0);

     private static final ThreadLocal < Integer > uniqueNum = 
         new ThreadLocal < Integer > () {
             @Override protected Integer initialValue() {
                 return uniqueId.getAndIncrement();
         }
     };
 
     public static int getCurrentThreadId() {
         return uniqueId.get();
     }
 } // UniqueThreadIdGenerator

每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
在这里插入图片描述
三.我们先通过一个例子来看一下 ThreadLocal 的使用

public class MyThreadLocalStringDemo {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private String getString() {
        return threadLocal.get();
    }

    private void setString(String string) {
        threadLocal.set(string);
    }

    public static void main(String[] args) {
        int threads = 9;
        MyThreadLocalStringDemo demo = new MyThreadLocalStringDemo();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                demo.setString(Thread.currentThread().getName());
                System.out.println(demo.getString());
                countDownLatch.countDown();
            }, "thread - " + i);
            thread.start();
        }
    }
}

运行结果如下:
在这里插入图片描述
这里可能有的人会觉得在例子中我们完全可以通过加锁来实现这个功能。是的没错,加锁确实可以解决这个问题,但是在这里我们强调的是线程数据隔离的问题,并不是多线程共享数据的问题。假如我们这里除了getString() 之外还有很多其他方法也要用到这个 String,这个时候各个方法之间就没有显式的数据传递过程了,都可以直接中 ThreadLocal 变量中获取,这才是 ThreadLocal 的核心,相同线程数据共享不同的线程数据隔离。由于ThreadLocal 是支持泛型的,这里采用的是存放一个 String 来演示,其实可以存放任何类型,效果都是一样的。

四.ThreadLocal 源码分析
在分析源码前我们明白一个事那就是对象实例与 ThreadLocal 变量的映射关系是由线程 Thread 来维护的,换句话说就是对象实例与 ThreadLocal 变量的映射关系是存放的一个 Map 里面(这个 Map 是个抽象的 Map 并不是 java.util 中的 Map ),而这个 Map 是 Thread 类的一个字段!而真正存放映射关系的 Map 就是 ThreadLocalMap。下面我们通过源码的中几个方法来看一下具体的实现。

//set 方法	
public void set(T value) {	
    Thread t = Thread.currentThread();	
    ThreadLocalMap map = getMap(t);	
    if (map != null)	
        map.set(this, value);	
    else	
        createMap(t, value);	
}	
	
//获取线程中的ThreadLocalMap 字段!!	
ThreadLocalMap getMap(Thread t) {	
    return t.threadLocals;	
}	
	
//创建线程的变量	
void createMap(Thread t, T firstValue) {	
     t.threadLocals = new ThreadLocalMap(this, firstValue);	
}	

在 set 方法中首先获取当前线程,然后通过 getMap 获取到当前线程ThreadLocalMap 类型的变量 threadLocals,如果存在则直接赋值,如果不存在则给该线程创建 ThreadLocalMap 变量并赋值。赋值的时候这里的 this 就是调用变量的对象实例本身。


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();	
}	
	
	
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;	
}	

get 方法也比较简单,同样也是先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

ThreadLocalMap 中使用 Entry[] 数组来存放对象实例与变量的关系,并且实例对象作为 key变量作为 value 实现对应关系。并且这里的 key 采用的是对实例对象的弱引用,(因为我们这里的 key 是对象实例,每个对象实例有自己的生命周期,这里采用弱引用就可以在不影响对象实例生命周期的情况下对其引用)。这部分源码未贴出.

五.实际运用(面试可回答)
场景:高并发情况下,SimpleDateFormat是线程不安全的,我们使用ThreadLocal解决线程安全问题。
解决方案:
4、使用ThreadLocal:每个线程拥有自己的SimpleDateFormat对象。(推荐使用)

public class DateUtils {
    /**
     * 锁对象
     */
    private static final Object lockObj = new Object();
    /**
     * 存放不同的日期模板格式的sdf的Map
     */
    private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();
    /**
     * 返回一个ThreadLocal的sdf,每个线程只会new一次sdf
     *
     * @param pattern
     * @return
     */
    private static SimpleDateFormat getSdf(final String pattern) {
        ThreadLocal<SimpleDateFormat> tl = sdfMap.get(pattern);

        // 此处的双重判断和同步是为了防止sdfMap这个单例被多次put重复的sdf
        if (tl == null) {
            synchronized (lockObj) {
                tl = sdfMap.get(pattern);
                if (tl == null) {
                    // 只有Map中还没有这个pattern的sdf才会生成新的sdf并放入map
                    System.out.println("put new sdf of pattern " + pattern + " to map");
                    // 这里是关键,使用ThreadLocal<SimpleDateFormat>替代原来直接new SimpleDateFormat
                    tl = new ThreadLocal<SimpleDateFormat>() {
                        @Override
                        protected SimpleDateFormat initialValue() {
                            System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern);
                            return new SimpleDateFormat(pattern);
                        }
                    };
                    sdfMap.put(pattern, tl);
                }
            }
        }
        return tl.get();
    }
    /**
     * 使用ThreadLocal<SimpleDateFormat>来获取SimpleDateFormat,这样每个线程只会有一个SimpleDateFormat
     * 如果新的线程中没有SimpleDateFormat,才会new一个
     * @param date
     * @param pattern
     * @return
     */
    public static String format(Date date, String pattern) {
        return getSdf(pattern).format(date);
    }
    public static Date parse(String dateStr, String pattern) throws Exception {
        return getSdf(pattern).parse(dateStr);
    }
}

测试代码:

public class DateUtilTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                DateUtils.format(new Date(), "yyyy-MM-dd");
            };
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                DateUtils.format(new Date(), "yyyy-MM-dd");
            };
        };
        Thread t3 = new Thread() {
            @Override
            public void run() {
                DateUtils.format(new Date(), "yyyy-MM-dd");
            };
        };
        Thread t4 = new Thread() {
            @Override
            public void run() {
                try {
                    DateUtils.parse("2017-06-10 12:00:01", "yyyy-MM-dd HH:mm:ss");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
        };
        Thread t5 = new Thread() {
            @Override
            public void run() {
                try {
                    DateUtils.parse("2017-06-10 12:00:01", "yyyy-MM-dd HH:mm:ss");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
        };
        System.out.println("单线程执行:");
        ExecutorService exec1 = Executors.newFixedThreadPool(1);
        exec1.execute(t1);
        exec1.execute(t2);
        exec1.execute(t3);
        exec1.execute(t4);
        exec1.execute(t5);
        exec1.shutdown();

        Thread.sleep(1000);

        System.out.println("双线程执行:");
        ExecutorService exec2 = Executors.newFixedThreadPool(2);
        exec2.execute(t1);
        exec2.execute(t2);
        exec2.execute(t3);
        exec2.execute(t4);
        exec2.execute(t5);
        exec2.shutdown();
    }
}

运行结果如下:
在这里插入图片描述
1)1个线程执行5个任务,new了两个SimpleDateFormat对象
2)2个线程执行5个任务,new了四个SimpleDateFormat对象,每个线程拥有两个格式为“yyyy-MM-dd”、“yyyy-MM-dd HH:mm:ss”的对象,线程之间没有共享SimpleDateFormat对象,对于每个线程,都是线性执行,也不会出现共享Calendar的现象。故完美解决SimpleDateFormat线程不安全的问题。
如果使用第一种方式,5个任务的执行,肯定需要new出5个SimpleDateFormat,对于单个线程没有复用的概念。但使用ThreadLocal,对于单个线程,相同格式是可以复用SimpleDateFormat对象,所以这种方式,既可以保证线程安全,又可以不耗费系统太多资源,其实这种思想和web中request,response对象是一样的,都是通过线程隔离,每个线程维护一份自己的对象来保证线程安全。

发布了99 篇原创文章 · 获赞 2 · 访问量 2613

猜你喜欢

转载自blog.csdn.net/weixin_41588751/article/details/105231421
今日推荐