当前位置:网站首页>ThreadLocal还不会?来看看!

ThreadLocal还不会?来看看!

2022-06-09 12:40:00 little-peter

ThreadLocal设计是为了解决并发时,线程共享变量的问题,由于过度设计,如弱引用和哈希碰撞,导致其令人难以理解和使用成本高等问题。除此之外,使用稍有不慎还会造成脏数据和内存泄漏,共享变量更新等问题。但即便如此,ThreadLocal依然有自己的适用场景,以及无可取代的价值,比如接下来要介绍的这两种使用场景,除了 ThreadLocal 之外,还真没有合适的替代方案。

 使用场景1:本地变量

 我们以多线程格式化时间为例,来演示 ThreadLocal 的价值和作用,当我们在多个线程中格式化时间时,通常会这样操作。

  1. 当有两个线程进行时间格式化时,我们可以这样写:
import java.text.SimpleDateFormat;

import java.util.Date;


public class Test {

    public static void main(String[] args) throws InterruptedException {

        // 创建并启动线程1

        Thread t1 = new Thread(new Runnable() {

            @Override

            public void run() {

                // 得到时间对象

                Date date = new Date(1 * 1000);

                // 执行时间格式化

                formatAndPrint(date);

            }

        });

        t1.start();

        // 创建并启动线程2

        Thread t2 = new Thread(new Runnable() {

            @Override

            public void run() {

                // 得到时间对象

                Date date = new Date(2 * 1000);

                // 执行时间格式化

                formatAndPrint(date);

            }

        });

        t2.start();

    }


    /**

     * 格式化并打印结果

     * @param date 时间对象

     */

    private static void formatAndPrint(Date date) {

        // 格式化时间对象

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

        // 执行格式化

        String result = simpleDateFormat.format(date);

        // 打印最终结果

        System.out.println("时间:" + result);

    }

}

以上程序运行结果为:

上面代码因为创建的线程数量不多,所以我们可以给每个线程创建一个私有对象 SimpleDateFormat来进行时间格式化。

2.10个线程进行时间格式化

当有10个线程进行时间格式化时,我们可以使用for循环创建多个线程执行时间格式化,具体代码如下:

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

public class Test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            // 创建线程
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 得到时间对象
                    Date date = new Date(finalI * 1000);
                    // 执行时间格式化
                    formatAndPrint(date);
                }
            });
            // 启动线程
            thread.start();
        }
    }
    /**
     * 格式化并打印时间
     * @param date 时间对象
     */
    private static void formatAndPrint(Date date) {
        // 格式化时间对象
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        // 执行格式化
        String result = simpleDateFormat.format(date);
        // 打印最终结果
        System.out.println("时间:" + result);
    }
}

以上程序执行结果为:

从上述结果可以看出,虽然此时创建的线程数和 SimpleDateFormat 的数量不算少,但程序还是可以正常运行的。

3.1000个线程格式化

当我们将线程的数量从 10 个变成 1000 个的时候,我们就不能单纯的使用 for 循环来创建 1000 个线程的方式来解决问题了,因为这样频繁的新建和销毁线程会造成大量的系统开销和线程过度争抢 CPU 资源的问题。所以经过一番思考后,我们决定使用线程池来执行这 1000 次的任务,因为线程池可以复用线程资源,无需频繁的新建和销毁线程,也可以通过控制线程池中线程的数量来避免过多线程所导致的 CPU 资源过度争抢和线程频繁切换所造成的性能问题,而且我们可以将 SimpleDateFormat 提升为全局变量,从而避免每次执行都要新建 SimpleDateFormat 的问题,于是我们写下了这样的代码:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class App {
    // 时间格式化对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池执行任务
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 执行任务
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 得到时间对象
                    Date date = new Date(finalI * 1000);
                    // 执行时间格式化
                    formatAndPrint(date);
                }
            });
        }
        // 线程池执行完任务之后关闭
        threadPool.shutdown();
    }

    /**
     * 格式化并打印时间
     * @param date 时间对象
     */
    private static void formatAndPrint(Date date) {
        // 执行格式化
        String result = simpleDateFormat.format(date);
        // 打印最终结果
        System.out.println("时间:" + result);
    }
}

以上程序执行结果为:

上面结果说明我们的代码是线程不安全的。

线程安全问题:在多线程的执行中,程序的执行结果与预期结果不相符的情况

  • 为什么会出现上述问题呢?

为了找到问题所在,我们尝试查看 SimpleDateFormatformat 方法的源码来排查一下问题,format 源码如下:

private StringBuffer format(Date date, StringBuffer toAppendTo,

                                FieldDelegate delegate) {

    // 注意此行代码

    calendar.setTime(date);

 从上述源码可以看出,在执行 SimpleDateFormat.format 方法时,会使用 calendar.setTime 方法将输入的时间进行转换,那么我们想象一下这样的场景:

  1. 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;
  2. 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;
  3. 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;
  4. 线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。
  • 解决线程安全问题:加锁

当出现线程安全问题时,我们想到的第一解决方案就是加锁,具体的实现代码如下:

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;


public class App {

    // 时间格式化对象

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");


    public static void main(String[] args) throws InterruptedException {

        // 创建线程池执行任务

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,

                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));

        for (int i = 0; i < 1000; i++) {

            int finalI = i;

            // 执行任务

            threadPool.execute(new Runnable() {

                @Override

                public void run() {

                    // 得到时间对象

                    Date date = new Date(finalI * 1000);

                    // 执行时间格式化

                    formatAndPrint(date);

                }

            });

        }

        // 线程池执行完任务之后关闭

        threadPool.shutdown();

    }


    /**

     * 格式化并打印时间

     * @param date 时间对象

     */

    private static void formatAndPrint(Date date) {

        // 执行格式化

        String result = null;

        // 加锁

        synchronized (App.class) {

            result = simpleDateFormat.format(date);

        }

        // 打印最终结果

        System.out.println("时间:" + result);

    }

}

加锁的缺点

加锁的方式虽然可以解决线程安全的问题,但同时也带来了新的问题,当程序加锁之后,所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了

有没有既能解决线程安全问题,又能提高程序的执行速度的解决方案呢?

有的,这个时候 ThreadLocal就要上场了。

好菜在这里:https://blog.csdn.net/qq_49425839/article/details/117389318

参考王磊老师语雀

 

原网站

版权声明
本文为[little-peter]所创,转载请带上原文链接,感谢
https://blog.csdn.net/qq_49425839/article/details/117301426