线程封闭之ThreadLocal和栈封闭

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

线程封闭

当访问共享数据时,通常是要使用同步。如果要避免使用同步,就是不提供共享数据。如果仅在单线程中访问数据,就不需要同步,这种技术就叫做线程封闭,它是实现线程安全最简单的方式之一。当某个对象封闭在一个线程当中时将自动实现线程安全性,即使被封闭的对象本身它并不是安全的,实现线程主要有三种方式。

1. Ad-hoc线程封闭

Ad-hoc是指维护线程封闭是由程序自己去实现和维护。Ad-hoc非常脆弱,因为它没有一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到特定的线程上。

2. 栈封闭

栈封闭简单理解就是通过局部变量来实现线程封闭,多个线程访问对象的同一个方法,方法内部的局部变量会拷贝到每个线程的线程栈当中,只有当前线程才能访问到,互不干扰。所以局部变量是不被多个线程所共享的。

示例:

public class ThreadTest {
    private int num;
    public void test(int key){
        int flag=0;
        for(int i=0;i<key;i++){
            flag=flag+1;
            num=num+1;
        }
        System.out.println(Thread.currentThread().getName()+"num"+num);
        System.out.println(Thread.currentThread().getName()+"flag"+flag);
    }

    public static void main(String[] args) {

        ThreadTest threadTest=new ThreadTest();
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.test(10);
            }
        });
        thread1.start();
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.test(9);
            }
        });
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}

# 结果
Thread-0num10
Thread-0flag10
Thread-1num19
Thread-1flag9

复制代码

由示例可以看出,在类上有一个全局变量num,方法里有一个局部变量flag,启动两个线程去调用这个方法,发现num的结果是两个线程结果之和,而flag是互不干扰,所以局部变量是不被多个线程所共享的。

3.ThreadLocal

维护线程封闭一种更规范的方法就是使用ThreadLocal。ThreadLocal提供get和set方法,这些方法为每个使用该变量的线程都存有一份独立的副本因此get总是返回的是当前线程在调用set时设置的值。

ThreadLocal一般用于防止对可变的单实例变量或者全局变量进行共享。例如在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动的时候初始化这个对象,从而避免在调用每个方法都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的,通过将JDBC的连接保存到ThreadLocal中,每个线程都会拥有属于自己的连接。

ThreadLocal代码示例:

/** 线程封闭示例 */
public class Demo7 {
    /** threadLocal变量,每个线程都有一个副本,互不干扰 */
    public static ThreadLocal<String> value = new ThreadLocal<>();

    /**
     * threadlocal测试
     *
     * @throws Exception
     */
    public void threadLocalTest() throws Exception {

        // threadlocal线程封闭示例
        value.set("这是主线程设置的123"); // 主线程设置值
        String v = value.get();
        System.out.println("线程1执行之前,主线程取到的值:" + v);

        new Thread(new Runnable() {
            @Override
            public void run() {
                String v = value.get();
                System.out.println("线程1取到的值:" + v);
                // 设置 threadLocal
                value.set("这是线程1设置的456");

                v = value.get();
                System.out.println("重新设置之后,线程1取到的值:" + v);
                System.out.println("线程1执行结束");
            }
        }).start();
        Thread.sleep(5000L); // 等待所有线程执行结束
        v = value.get();
        System.out.println("线程1执行之后,主线程取到的值:" + v);

    }

    public static void main(String[] args) throws Exception {
        new Demo7().threadLocalTest();
    }
}
# 结果
线程1执行之前,主线程取到的值:这是主线程设置的123
线程1取到的值:null
重新设置之后,线程1取到的值:这是线程1设置的456
线程1执行结束
线程1执行之后,主线程取到的值:这是主线程设置的123

复制代码

通过代码示例,可以看到主线程和子线程的执行结果互不干扰。当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用ThreadLocal。至于ThreadLocal如何实现为每个线程提供单独的副本,请看ThreadLocal源码分析 ,欢迎关注我,一起学习,一起进步。

往期精彩回顾

Synchronized原理分析

让小程序完美支持Markdown,最详细教程来了

学习的路上永远都需要志同道合的人一起探讨,一起前行,关注我,一起学习吧

猜你喜欢

转载自juejin.im/post/7017824870486507551