ThreadLocal不好用?那是你没用对!
在 Java 中,如果要问哪个类使用简单,但用好最不简单?我想你的脑海中一定会浮现出一次词——“ThreadLocal”。
确实如此,ThreadLocal 原本设计是为了解决并发时,线程共享变量的问题,但由于过度设计,如弱引用和哈希碰撞,从而导致它的理解难度大和使用成本高等问题。当然,如果稍有不慎还是导致脏数据、内存溢出、共享变量更新等问题,但即便如此,ThreadLocal 依旧有适合自己的使用场景,以及无可取代的价值,比如本文要介绍了这两种使用场景,除了 ThreadLocal 之外,还真没有合适的替代方案。
使用场景1:本地变量
我们以多线程格式化时间为例,来演示 ThreadLocal 的价值和作用,当我们在多个线程中格式化时间时,通常会这样操作。
① 2个线程格式化
当有 2 个线程进行时间格式化时,我们可以这样写:
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 来进行时间格式化。
② 10个线程格式化
当线程的数量从 2 个升级为 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 的数量不算少,但程序还是可以正常运行的。
③ 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);
    }
}
以上程序的执行结果为:

当我们怀着无比喜悦的心情去运行程序的时候,却发现意外发生了,这样写代码竟然会出现线程安全的问题。从上述结果可以看出,程序的打印结果竟然有重复内容的,正确的情况应该是没有重复的时间才对。
PS:所谓的线程安全问题是指:在多线程的执行中,程序的执行结果与预期结果不相符的情况。
a) 线程安全问题分析
为了找到问题所在,我们尝试查看 SimpleDateFormat 中 format 方法的源码来排查一下问题,format 源码如下:
private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
    // 注意此行代码
    calendar.setTime(date);
    boolean useDateFormatSymbols = useDateFormatSymbols();
    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }
        switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;
            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;
            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
        }
    }
    return toAppendTo;
}
从上述源码可以看出,在执行 SimpleDateFormat.format 方法时,会使用 calendar.setTime 方法将输入的时间进行转换,那么我们想象一下这样的场景:
- 线程 1 执行了 
calendar.setTime(date)方法,将用户输入的时间转换成了后面格式化时所需要的时间; - 线程 1 暂停执行,线程 2 得到 
CPU时间片开始执行; - 线程 2 执行了 
calendar.setTime(date)方法,对时间进行了修改; - 线程 2 暂停执行,线程 1 得出 
CPU时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。 
正常的情况下,程序的执行是这样的:

非线程安全的执行流程是这样的:

b) 解决线程安全问题:加锁
当出现线程安全问题时,我们想到的第一解决方案就是加锁,具体的实现代码如下:
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);
    }
}
以上程序的执行结果为:

从上述结果可以看出,使用了 synchronized 加锁之后程序就可以正常的执行了。
加锁的缺点
加锁的方式虽然可以解决线程安全的问题,但同时也带来了新的问题,当程序加锁之后,所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了。
有没有既能解决线程安全问题,又能提高程序的执行速度的解决方案呢?
答案是:有的,这个时候 ThreadLocal 就要上场了。
c) 解决线程安全问题:ThreadLocal
1.ThreadLocal 介绍
ThreadLocal 从字面的意思来理解是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能使用自己的变量。
以上面线程池格式化时间为例,当线程池中有 10 个线程时,SimpleDateFormat 会存入 ThreadLocal 中,它也只会创建 10 个对象,即使要执行 1000 次时间格式化任务,依然只会新建 10 个 SimpleDateFormat 对象,每个线程调用自己的 ThreadLocal 变量。
2.ThreadLocal 基础使用
ThreadLocal 常用的核心方法有三个:
- set 方法:用于设置线程独立变量副本。 没有 set 操作的 ThreadLocal 容易引起脏数据。
 - get 方法:用于获取线程独立变量副本。 没有 get 操作的 ThreadLocal 对象没有意义。
 - remove 方法:用于移除线程独立变量副本。 没有 remove 操作容易引起内存泄漏。
 
ThreadLocal 所有方法如下图所示:

官方说明文档:https://docs.oracle.com/javase/8/docs/api/
ThreadLocal 基础用法如下:
/**
 * @公众号:Java中文社群
 */
public class ThreadLocalExample {
    // 创建一个 ThreadLocal 对象
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        // 线程执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + " 存入值:" + threadName);
                // 在 ThreadLocal 中设置值
                threadLocal.set(threadName);
                // 执行方法,打印线程中设置的值
                print(threadName);
            }
        };
        // 创建并启动线程 1
        new Thread(runnable, "MyThread-1").start();
        // 创建并启动线程 2
        new Thread(runnable, "MyThread-2").start();
    }
    /**
     * 打印线程中的 ThreadLocal 值
     * @param threadName 线程名称
     */
    private static void print(String threadName) {
        try {
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印结果
            System.out.println(threadName + " 取出值:" + result);
        } finally {
            // 移除 ThreadLocal 中的值(防止内存溢出)
            threadLocal.remove();
        }
    }
}
以上程序的执行结果为:

从上述结果可以看出,每个线程只会读取到属于自己的 ThreadLocal 值。
3.ThreadLocal 高级用法
① 初始化:initialValue
public class ThreadLocalByInitExample {
    // 定义 ThreadLocal
    private static ThreadLocal<String> threadLocal = new ThreadLocal(){
        @Override
        protected String initialValue() {
            System.out.println("执行 initialValue() 方法");
            return "默认值";
        }
    };
    public static void main(String[] args) {
        // 线程执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 执行方法,打印线程中数据(未设置值打印)
                print(threadName);
            }
        };
        // 创建并启动线程 1
        new Thread(runnable, "MyThread-1").start();
        // 创建并启动线程 2
        new Thread(runnable, "MyThread-2").start();
    }
    /**
     * 打印线程中的 ThreadLocal 值
     * @param threadName 线程名称
     */
    private static void print(String threadName) {
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印结果
        System.out.println(threadName + " 得到值:" + result);
    }
}
以上程序的执行结果为:

当使用了 #threadLocal.set 方法之后,initialValue 方法就不会被执行了,如下代码所示:
public class ThreadLocalByInitExample {
    // 定义 ThreadLocal
    private static ThreadLocal<String> threadLocal = new ThreadLocal() {
        @Override
        protected String initialValue() {
            System.out.println("执行 initialValue() 方法");
            return "默认值";
        }
    };
    public static void main(String[] args) {
        // 线程执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + " 存入值:" + threadName);
                // 在 ThreadLocal 中设置值
                threadLocal.set(threadName);
                // 执行方法,打印线程中设置的值
                print(threadName);
            }
        };
        // 创建并启动线程 1
        new Thread(runnable, "MyThread-1").start();
        // 创建并启动线程 2
        new Thread(runnable, "MyThread-2").start();
    }
    /**
     * 打印线程中的 ThreadLocal 值
     * @param threadName 线程名称
     */
    private static void print(String threadName) {
        try {
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印结果
            System.out.println(threadName + "取出值:" + result);
        } finally {
            // 移除 ThreadLocal 中的值(防止内存溢出)
            threadLocal.remove();
        }
    }
}
以上程序的执行结果为:

为什么 set 方法之后,初始化代码就不执行了?
要理解这个问题,需要从 ThreadLocal.get() 方法的源码中得到答案,因为初始化方法 initialValue 在 ThreadLocal 创建时并不会立即执行,而是在调用了 get 方法只会才会执行,测试代码如下:
import java.util.Date;
public class ThreadLocalByInitExample {
    // 定义 ThreadLocal
    private static ThreadLocal<String> threadLocal = new ThreadLocal() {
        @Override
        protected String initialValue() {
            System.out.println("执行 initialValue() 方法 " + new Date());
            return "默认值";
        }
    };
    public static void main(String[] args) {
        // 线程执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 得到当前线程名称
                String threadName = Thread.currentThread().getName();
                // 执行方法,打印线程中设置的值
                print(threadName);
            }
        };
        // 创建并启动线程 1
        new Thread(runnable, "MyThread-1").start();
        // 创建并启动线程 2
        new Thread(runnable, "MyThread-2").start();
    }
    /**
     * 打印线程中的 ThreadLocal 值
     * @param threadName 线程名称
     */
    private static void print(String threadName) {
        System.out.println("进入 print() 方法 " + new Date());
        try {
            // 休眠 1s
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印结果
        System.out.println(String.format("%s 取得值:%s %s",
                threadName, result, new Date()));
    }
}
以上程序的执行结果为:

从上述打印的时间可以看出:initialValue 方法并不是在 ThreadLocal 创建时执行的,而是在调用 Thread.get 方法时才执行的。
接下来来看 Threadlocal.get 源码的实现:
public T get() {
    // 得到当前的线程
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 判断 ThreadLocal 中是否有数据
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            // 有 set 值,直接返回数据
            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;
}
从上述源码可以看出,当 ThreadLocal 中有值时会直接返回值 e.value,只有 Threadlocal 中没有任何值时才会执行初始化方法 initialValue。
注意事项—类型必须保持一致
注意在使用 initialValue 时,返回值的类型要和 ThreadLoca 定义的数据类型保持一致,如下图所示:

如果数据不一致就会造成 ClassCaseException 类型转换异常,如下图所示:

② 初始化2:withInitial
import java.util.function.Supplier;
public class ThreadLocalByInitExample {
    // 定义 ThreadLocal
    private static ThreadLocal<String> threadLocal =
            ThreadLocal.withInitial(new Supplier<String>() {
                @Override
                public String get() {
                    System.out.println("执行 withInitial() 方法");
                    return "默认值";
                }
            });
    public static void main(String[] args) {
        // 线程执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                // 执行方法,打印线程中设置的值
                print(threadName);
            }
        };
        // 创建并启动线程 1
        new Thread(runnable, "MyThread-1").start();
        // 创建并启动线程 2
        new Thread(runnable, "MyThread-2").start();
    }
    /**
     * 打印线程中的 ThreadLocal 值
     * @param threadName 线程名称
     */
    private static void print(String threadName) {
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印结果
        System.out.println(threadName + " 得到值:" + result);
    }
}
以上程序的执行结果为:

通过上述的代码发现,withInitial 方法的使用好和 initialValue 好像没啥区别,那为啥还要造出两个类似的方法呢?客官莫着急,继续往下看。
③ 更简洁的 withInitial 使用
withInitial 方法的优势在于可以更简单的实现变量初始化,如下代码所示:
public class ThreadLocalByInitExample {
    // 定义 ThreadLocal
    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");
    public static void main(String[] args) {
        // 线程执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                // 执行方法,打印线程中设置的值
                print(threadName);
            }
        };
        // 创建并启动线程 1
        new Thread(runnable, "MyThread-1").start();
        // 创建并启动线程 2
        new Thread(runnable, "MyThread-2").start();
    }
    /**
     * 打印线程中的 ThreadLocal 值
     * @param threadName 线程名称
     */
    private static void print(String threadName) {
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印结果
        System.out.println(threadName + " 得到值:" + result);
    }
}
以上程序的执行结果为:

4.ThreadLocal 版时间格式化
了解了 ThreadLocal 的使用之后,我们回到本文的主题,接下来我们将使用 ThreadLocal 来实现 1000 个时间的格式化,具体实现代码如下:
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 MyThreadLocalByDateFormat {
    // 创建 ThreadLocal 并设置默认值
    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
    public static void main(String[] args) {
        // 创建线程池执行任务
        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();
        // 线程池执行完任务之后关闭
        threadPool.shutdown();
    }
    /**
     * 格式化并打印时间
     * @param date 时间对象
     */
    private static void formatAndPrint(Date date) {
        // 执行格式化
        String result = dateFormatThreadLocal.get().format(date);
        // 打印最终结果
        System.out.println("时间:" + result);
    }
}
以上程序的执行结果为:

从上述结果可以看出,使用 ThreadLocal 也可以解决线程并发问题,并且避免了代码加锁排队执行的问题。
使用场景2:跨类传递数据
除了上面的使用场景之外,我们还可以使用 **ThreadLocal** 来实现线程中跨类、跨方法的数据传递。比如登录用户的 User 对象信息,我们需要在不同的子系统中多次使用,如果使用传统的方式,我们需要使用方法传参和返回值的方式来传递 User 对象,然而这样就无形中造成了类和类之间,甚至是系统和系统之间的相互耦合了,所以此时我们可以使用 ThreadLocal 来实现 User 对象的传递。
确定了方案之后,接下来我们来实现具体的业务代码。我们可以先在主线程中构造并初始化一个 User 对象,并将此 User 对象存储在 ThreadLocal 中,存储完成之后,我们就可以在同一个线程的其他类中,如仓储类或订单类中直接获取并使用 User 对象了,具体实现代码如下。
主线程中的业务代码:
public class ThreadLocalByUser {
    public static void main(String[] args) {
        // 初始化用户信息
        User user = new User("Java");
        // 将 User 对象存储在 ThreadLocal 中
        UserStorage.setUser(user);
        // 调用订单系统
        OrderSystem orderSystem = new OrderSystem();
        // 添加订单(方法内获取用户信息)
        orderSystem.add();
        // 调用仓储系统
        RepertorySystem repertory = new RepertorySystem();
        // 减库存(方法内获取用户信息)
        repertory.decrement();
    }
}
User 实体类:
/**
 * 用户实体类
 */
class User {
    public User(String name) {
        this.name = name;
    }
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
ThreadLocal 操作类:
/**
 * 用户信息存储类
 */
class UserStorage {
    // 用户信息
    public static ThreadLocal<User> USER = new ThreadLocal();
    /**
     * 存储用户信息
     * @param user 用户数据
     */
    public static void setUser(User user) {
        USER.set(user);
    }
}
订单类:
/**
 * 订单类
 */
class OrderSystem {
    /**
     * 订单添加方法
     */
    public void add() {
        // 得到用户信息
        User user = UserStorage.USER.get();
        // 业务处理代码(忽略)...
        System.out.println(String.format("订单系统收到用户:%s 的请求。",
                user.getName()));
    }
}
仓储类:
/**
 * 仓储类
 */
class RepertorySystem {
    /**
     * 减库存方法
     */
    public void decrement() {
        // 得到用户信息
        User user = UserStorage.USER.get();
        // 业务处理代码(忽略)...
        System.out.println(String.format("仓储系统收到用户:%s 的请求。",
                user.getName()));
    }
}
以上程序的最终执行结果:

从上述结果可以看出,当我们在主线程中先初始化了 User 对象之后,订单类和仓储类无需进行任何的参数传递也可以正常获得 User 对象了,从而实现了一个线程中,跨类和跨方法的数据传递。
总结
使用 ThreadLocal 可以创建线程私有变量,所以不会导致线程安全问题,同时使用 ThreadLocal 还可以避免因为引入锁而造成线程排队执行所带来的性能消耗;再者使用 ThreadLocal 还可以实现一个线程内跨类、跨方法的数据传递。
参考 & 鸣谢
《码出高效:Java开发手册》
《Java 并发编程 78 讲》
关注公号「Java中文社群」查看更多有意思、涨知识的并发编程文章。
ThreadLocal不好用?那是你没用对!的更多相关文章
- SimpleDateFormat线程不安全的5种解决方案!
		
1.什么是线程不安全? 线程不安全也叫非线程安全,是指多线程执行中,程序的执行结果和预期的结果不符的情况就叫做线程不安全.  线程不安全的代码 SimpleDateFormat 就是一个典型的线程不 ...
 - synchronized 加锁 this 和 class 的区别!
		
synchronized 是 Java 语言中处理并发问题的一种常用手段,它也被我们亲切的称之为"Java 内置锁",由此可见其地位之高.然而 synchronized 却有着多种 ...
 - synchronized 优化手段之锁膨胀机制!
		
synchronized 在 JDK 1.5 之前性能是比较低的,在那时我们通常会选择使用 Lock 来替代 synchronized.然而这个情况在 JDK 1.6 时就发生了改变,JDK 1.6 ...
 - synchronized优化手段:锁膨胀、锁消除、锁粗化和自适应自旋锁...
		
synchronized 在 JDK 1.5 时性能是比较低的,然而在后续的版本中经过各种优化迭代,它的性能也得到了前所未有的提升,上一篇中我们谈到了锁膨胀对 synchronized 性能的提升,然 ...
 - ReentrantLock 中的 4 个坑!
		
JDK 1.5 之前 synchronized 的性能是比较低的,但在 JDK 1.5 中,官方推出一个重量级功能 Lock,一举改变了 Java 中锁的格局.JDK 1.5 之前当我们谈到锁时,只能 ...
 - 1.3w字,一文详解死锁!
		
死锁(Dead Lock)指的是两个或两个以上的运算单元(进程.线程或协程),都在等待对方停止执行,以取得系统资源,但是没有一方提前退出,就称为死锁. 1.死锁演示 死锁的形成分为两个方面,一个是使用 ...
 - UEFI模式安装Win10和Linux双系统
		
最近心血来潮,想装一个Linux.Windows双系统,吸取上次安装的经验,这次一定都要使用UEFI模式启动,需要注意的是必须是支持此种启动模式的系统(一般解压之后都有efi文件夹不需要刻录),这次遇 ...
 - Java----多线程知识点归纳(概念)
		
一.线程与进程的区别: 多个进程的内部数据和状态都是完全独立的,而多线程是共享一块内存空间和一组系统资源,有可能互相影响. ?线程本身的数据通常只有寄存器数据,以及一个 程序执行时使用的堆栈,所以线程 ...
 - 也谈TDD,以及三层架构、设计模式、ORM……:没有免费的午餐
		
想在园子里写点东西已经很久了,但一直没有落笔,忙着做 一起帮 的开发直播,还有些软文做推广,还要做奶爸带孩子,还要……好吧,我承认,真正的原因是: 太特么的难写了! 但再难写也要写啊,要等到“能写好了 ...
 
随机推荐
- 在vscode中go编码发生的问题整理
			
引言 使用VsCode进行Go程序开发,我们肯定会碰到一些问题,这些问题有些是IDE的配置问题,有些是下载包的版本不一致问题,本文主要针对在开发过程中碰到的问题做一个简单的回顾和整理. 前期准备,必看 ...
 - Python图像处理库——PIL
			
PIL全称Python Image Library,是python官方的图像处理库,包含各种图像处理模块.Pillow是PIL的一个派生分支,包含与PIL相同的功能,并且更灵活.python3.0之后 ...
 - RabbitMQ 入门 (Go) - 1. 简介和安装
			
Message Broker(消息代理) 维基百科对 Message Broker 的定义是:Message broker 是一种中介程序模块,它把消息从发送方的正式消息传递协议转化为接收方的正式消息 ...
 - SpringBoot整合Swagger2及使用
			
简介 swagger是一个流行的API开发框架,这个框架以"开放API声明"(OpenAPI Specification,OAS)为基础, 对整个API的开发周期都提供了相应的解决 ...
 - 简单模拟实现javascript中的call、apply、bind方法
			
目录 引子 隐式丢失 硬绑定 实现及原理分析 总体实现(纯净版/没有注释) 写在最后 引子 读完<你不知道的JavaScript--上卷>中关于this的介绍和深入的章节后,对于this的 ...
 - ASP.NET Core可视化日志组件使用
			
前言 今天站长推荐一款日志可视化组件LogDashboard,可以不用安装第三方进程,只需要在项目中安装相应的Nuget包,添加数行代码,就可以实现拥有带Web页面的日志管理面板,十分nice哦. 下 ...
 - Java(215-231)【Object类、常用API】
			
1.Object类的toString方法 java.lang.Object 类 Object 是类层次结构的根(父)类. 每个类(Person,Student...)都使用 Object 作为超(父) ...
 - 火爆外网的 DGS 框架使用
			
Netflix 已开放其 Domain Graph Service(DGS)框架的源代码 ,该框架是为了方便整合 GraphQL 使用,用于简化 GraphQL 的实现. GraphQL 主要是作用于 ...
 - 12.VUE - v-bind 详解
			
v-bind指令用于给html标签设置属性. <!-- 完整语法 --> <a v-bind:href="url"></a> <!-- 缩 ...
 - Alignment of Code UVA - 1593
			
You are working in a team that writes Incredibly Customizable Programming Codewriter (ICPC) which ...