当前位置 > it书童 > java > 正文

ThreadLocal一次解决老大难问题

java it书童 2021-01-09 17:12:52 0赞 0踩 52阅读 0评论

ThreadLocal 的两大使用场景

每个线程需要一个独享的对象

通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random

每个 Thread 内有自己的实例副本,不共享

层层递进引入 ThreadLocal

当只有两个线程时,每个线程都可以创建专属的对象,反正负担又不大,随便玩

package juc.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;

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(10017);
                System.out.println(date);
            }
        }).start();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }

}

现在看起来没什么问题,两个线程相处得很愉快

将线程加到30个呢?肯定不能手动一个个创建线程,用 for 循环处理

package juc.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalNormalUsage01 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 30; 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) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }

}

用 for 循环太 low 了,如果有 1000 个任务呢?难道要建 1000 个线程?

这时候自然得用线程池了

package juc.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }

}

问题,每次都要创建 SimpleDateFormat 对象,创建与销毁的开销太大

其实,并不需要每次都创建,将类作为共享的全局变量,即可复用

package juc.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalNormalUsage02 {

    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 ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }

}

可是,这样出现吊诡的问题:有重复的日期出现

多个线程共用一个线程不安全的 SimpleDateFormat 对象,导致并发错误

用加锁的方式解决,给类加锁

public String date(int seconds) {
    Date date = new Date(1000 * seconds);
    String s = null;
    synchronized (ThreadLocalNormalUsage04.class) {
        s = dateFormat.format(date);
    }
    return s;
}

用 synchronized 加锁,效率过低,多个线程一个个排队等锁

这时,就需要用到 ThreadLocal

给线程池中的每个线程都创建一个 df 对象,保证了线程安全,并且高效

package juc.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.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");
        }
    };
}

用 lambda 表达式优化

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

每个线程内需要保存全局变量

例如在拦截器中获取用户信息,可以让不同方法直接使用,避免参数传递的麻烦

用 ThreadLocal 保存一些业务内容,这些信息在同一个线程内相同,但是不同的线程使用的业务内容不同。在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set() 过的对象,避免将这个对象作为参数传递的麻烦

强调的是在同一个请求内(即同一个线程内)不同方法间的共享

每个线程不共享对象,各个线程的不同方法共享对象

package juc.threadlocal;

public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("超哥");
        UserContextHolder.holder.set(user);
        new Service2().process();
        new Service3().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2 拿到用户名 " + user.name);
        // 业务逻辑:更新用户登录信息
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3 拿到用户名 " + user.name);
        // 业务逻辑:发放优惠券
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

ThreadLocal 的两种用法总结

  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)

  2. 在任何方法中都可以轻松获取到该对象

ThreadLocal 的好处与原理

  • 达到线程安全

  • 不需要加锁,提高执行效率

  • 更高效地利用内存、节省开销

  • 免去传参的繁琐,使得代码耦合度更低,更优雅

每个 Thread 对象中都持有一个 ThreadLocalMap 成员变量,ThreadLocalMap 存放多个 ThreadLocal 对象

ThreadLocal 的重要方法

  • initialValue()

返回当前线程的初始值,这是一个处以加载的方法,只有在调用 get() 时,才会触发,如果不重写此方法,会返回 null

  • set()

为线程设置一个新值

  • get()

获取线程对应的 value, 如果是首次调用,则会调用 initialize() 来得到这个值

  • remove()

删除对应线程的值

关于我
一个文科出身的程序员,追求做个有趣的人,传播有价值的知识,微信公众号主要分享读书思考心得,不会有代码类文章,非程序员的同学请放心订阅
转载须注明出处:https://www.itshutong.com/articles/1017