ThreadLocal不好用?那是你没用对!

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

在 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 {// 创建并启动线程1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {// 得到时间对象Date date = new Date(1 * 1000);// 执行时间格式化formatAndPrint(date);}});t1.start();// 创建并启动线程2Thread t2 = new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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) 线程安全问题分析

为了找到问题所在,我们尝试查看 SimpleDateFormatformat 方法的源码来排查一下问题,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. 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;

  2. 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;

  3. 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;

  4. 线程 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() {@Overridepublic 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 常用的核心方法有三个:

  1. set 方法:用于设置线程独立变量副本。没有 set 操作的 ThreadLocal 容易引起脏数据。

  2. get 方法:用于获取线程独立变量副本。没有 get 操作的 ThreadLocal 对象没有意义。

  3. 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() {@Overridepublic void run() {String threadName = Thread.currentThread().getName();System.out.println(threadName + " 存入值:" + threadName);// 在 ThreadLocal 中设置值threadLocal.set(threadName);// 执行方法,打印线程中设置的值print(threadName);}};// 创建并启动线程 1new Thread(runnable, "MyThread-1").start();// 创建并启动线程 2new 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 {// 定义 ThreadLocalprivate static ThreadLocal<String> threadLocal = new ThreadLocal(){@Overrideprotected String initialValue() {System.out.println("执行 initialValue() 方法");return "默认值";}};public static void main(String[] args) {// 线程执行任务Runnable runnable = new Runnable() {@Overridepublic void run() {// 执行方法,打印线程中数据(未设置值打印)print(threadName);}};// 创建并启动线程 1new Thread(runnable, "MyThread-1").start();// 创建并启动线程 2new 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 {// 定义 ThreadLocalprivate static ThreadLocal<String> threadLocal = new ThreadLocal() {@Overrideprotected String initialValue() {System.out.println("执行 initialValue() 方法");return "默认值";}};public static void main(String[] args) {// 线程执行任务Runnable runnable = new Runnable() {@Overridepublic void run() {String threadName = Thread.currentThread().getName();System.out.println(threadName + " 存入值:" + threadName);// 在 ThreadLocal 中设置值threadLocal.set(threadName);// 执行方法,打印线程中设置的值print(threadName);}};// 创建并启动线程 1new Thread(runnable, "MyThread-1").start();// 创建并启动线程 2new 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() 方法的源码中得到答案,因为初始化方法 initialValueThreadLocal 创建时并不会立即执行,而是在调用了 get 方法只会才会执行,测试代码如下:

import java.util.Date;public class ThreadLocalByInitExample {// 定义 ThreadLocalprivate static ThreadLocal<String> threadLocal = new ThreadLocal() {@Overrideprotected String initialValue() {System.out.println("执行 initialValue() 方法 " + new Date());return "默认值";}};public static void main(String[] args) {// 线程执行任务Runnable runnable = new Runnable() {@Overridepublic void run() {// 得到当前线程名称String threadName = Thread.currentThread().getName();// 执行方法,打印线程中设置的值print(threadName);}};// 创建并启动线程 1new Thread(runnable, "MyThread-1").start();// 创建并启动线程 2new Thread(runnable, "MyThread-2").start();}/*** 打印线程中的 ThreadLocal 值* @param threadName 线程名称*/private static void print(String threadName) {System.out.println("进入 print() 方法 " + new Date());try {// 休眠 1sThread.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);elsecreateMap(t, value);return value;
}

从上述源码可以看出,当 ThreadLocal 中有值时会直接返回值 e.value,只有 Threadlocal 中没有任何值时才会执行初始化方法 initialValue

注意事项—类型必须保持一致

注意在使用 initialValue 时,返回值的类型要和 ThreadLoca 定义的数据类型保持一致,如下图所示:

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

② 初始化2:withInitial
import java.util.function.Supplier;public class ThreadLocalByInitExample {// 定义 ThreadLocalprivate static ThreadLocal<String> threadLocal =ThreadLocal.withInitial(new Supplier<String>() {@Overridepublic String get() {System.out.println("执行 withInitial() 方法");return "默认值";}});public static void main(String[] args) {// 线程执行任务Runnable runnable = new Runnable() {@Overridepublic void run() {String threadName = Thread.currentThread().getName();// 执行方法,打印线程中设置的值print(threadName);}};// 创建并启动线程 1new Thread(runnable, "MyThread-1").start();// 创建并启动线程 2new 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 {// 定义 ThreadLocalprivate static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");public static void main(String[] args) {// 线程执行任务Runnable runnable = new Runnable() {@Overridepublic void run() {String threadName = Thread.currentThread().getName();// 执行方法,打印线程中设置的值print(threadName);}};// 创建并启动线程 1new Thread(runnable, "MyThread-1").start();// 创建并启动线程 2new 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() {@Overridepublic 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 讲》


往期推荐

ThreadLocal中的3个大坑,内存泄露都是小儿科!


额!Java中用户线程和守护线程区别这么大?


线程的故事:我的3位母亲成就了优秀的我!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544809.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

记一则js替换字符串的问题

2019独角兽企业重金招聘Python工程师标准>>> 软件的一处功能用到EasyUI的表单提交&#xff0c;返回一串字符串&#xff0c;这串字符串里有一段HTML代码&#xff0c;正常的情况下这段HTML代码里的双引号“ 是用 \ 转义过的。在IE中没问题&#xff0c;但是在Firefox和…

『图解Java并发』面试必问的CAS原理你会了吗?

在并发编程中我们都知道i操作是非线程安全的&#xff0c;这是因为 i操作不是原子操作。如何保证原子性呢&#xff1f;常用的方法就是加锁。在Java语言中可以使用 Synchronized和CAS实现加锁效果。Synchronized是悲观锁&#xff0c;线程开始执行第一步就是获取锁&#xff0c;一旦…

SimpleDateFormat线程不安全的5种解决方案!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;1.什么是线程不安全&#xff1f;线程不安全也叫非线程安全&#xff0c;是指多线程执行中&#xff0c;程序的执行结果和预期的…

mac地址漂移flapping的前因后果

一、什么是mac地址flapping?mac地址漂移是指&#xff1a;在同一个vlan内&#xff0c;mac地址表项的出接口出现变更。如图&#xff1a;二、产生的原因1、因为环路或VRRP切换&#xff0c;导致的MAC地址漂移告警。&#xff08;不予关注&#xff09;2、因为无线用户漫游&#xff0…

时间转换竟多出1年!Java开发中的20个坑你遇到过几个?

前言最近看了极客时间的《Java业务开发常见错误100例》&#xff0c;再结合平时踩的一些代码坑&#xff0c;写写总结&#xff0c;希望对大家有帮助&#xff0c;感谢阅读~1. 六类典型空指针问题包装类型的空指针问题级联调用的空指针问题Equals方法左边的空指针问题ConcurrentHas…

Oracle RAC Failover 详解

2019独角兽企业重金招聘Python工程师标准>>> Oracle RAC 同时具备HA(High Availiablity) 和LB(LoadBalance). 而其高可用性的基础就是Failover(故障转移). 它指集群中任何一个节点的故障都不会影响用户的使用&#xff0c;连接到故障节点的用户会被自动转移到健康节…

一个ThreadLocal和面试官大战30个回合

开场杭州某商务楼里&#xff0c;正发生着一起求职者和面试官的battle。面试官&#xff1a;你先自我介绍一下。安琪拉&#xff1a;面试官你好&#xff0c;我是草丛三婊&#xff0c;最强中单&#xff08;妲己不服&#xff09;&#xff0c;草地摩托车车手&#xff0c;第21套广播体…

图文并茂的聊聊Java内存模型!

在面试中&#xff0c;面试官经常喜欢问&#xff1a;『说说什么是Java内存模型(JMM)&#xff1f;』面试者内心狂喜&#xff0c;这题刚背过&#xff1a;『Java内存主要分为五大块&#xff1a;堆、方法区、虚拟机栈、本地方法栈、PC寄存器&#xff0c;balabala……』面试官会心一笑…

AngularJS入门心得2——何为双向数据绑定

前言&#xff1a;谁说Test工作比较轻松&#xff0c;最近在熟悉几个case&#xff0c;差点没疯。最近又是断断续续的看我的AngularJS&#xff0c;总觉得自己还是没有入门&#xff0c;可能是自己欠前端的东西太多了&#xff0c;看不了几行代码就有几个常用函数不熟悉的。看过了大漠…

Java中那些内存泄漏的场景!

虽然Java程序员不用像C/C程序员那样时刻关注内存的使用情况&#xff0c;JVM会帮我们处理好这些&#xff0c;但并不是说有了GC就可以高枕无忧&#xff0c;内存泄露相关的问题一般在测试的时候很难发现&#xff0c;一旦上线流量起来可能马上就是一个诡异的线上故障。内存泄露定义…

ThreadLocal内存溢出代码演示和原因分析!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;前言ThreadLocal 翻译成中文是线程本地变量的意思&#xff0c;也就是说它是线程中的私有变量&#xff0c;每个线程只能操作自…

彻夜怒肝!Spring Boot+Sentinel+Nacos高并发已撸完,快要裂开了!

很多人说程序员是最容易实现财富自由的职业&#xff0c;也确实&#xff0c;比如字节 28 岁的程序员郭宇不正是从普通开发一步步做起的吗&#xff1f;回归行业现状&#xff0c;当开发能力可以满足公司业务需求时&#xff0c;拿到超预期的 Offer 并不算难。最近我也一直在思考这个…

湖南多校对抗5.24

据说A,B,C题都比较水这里就不放代码了 D:Facility Locations 然而D题是一个脑经急转弯的题&#xff1a;有m行&#xff0c;n列&#xff0c;每个位置有可能为0&#xff0c;也可能不为0&#xff0c;问最多选K行是不是可以使得每一列都至少有一个0&#xff0c;其中代价c有个约束条件…

PPT演讲计时器

下载 GitHub 源码地址 如果访问不到的话&#xff0c;可以从百度盘下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1bK4sug-eK85fmPgi9DzhcA 提取码&#xff1a;0vp3 文件&#xff1a;VB.Equal.Timer-VB计时器软件-绿色无残留 写在前面 转眼也工作了两年了&…

2万字!66道并发面试题及答案

我花了点时间整理了一些多线程&#xff0c;并发相关的面试题&#xff0c;虽然不是很多&#xff0c;但是偶尔看看还是很有用的哦&#xff01;话不多说&#xff0c;直接开整&#xff01;01 什么是线程&#xff1f;线程是操作系统能够进⾏运算调度的最⼩单位&#xff0c;它被包含在…

25种代码坏味道总结+优化示例

前言什么样的代码是好代码呢&#xff1f;好的代码应该命名规范、可读性强、扩展性强、健壮性......而不好的代码又有哪些典型特征呢&#xff1f;这25种代码坏味道大家要注意啦1. Duplicated Code &#xff08;重复代码&#xff09;重复代码就是不同地点&#xff0c;有着相同的程…

滚动照片抽奖软件

CODE GitHub 源码 1、女友说很丑的一个软件 说个最近的事情&#xff0c;女友公司过年了要搞活动&#xff0c;需要个抽奖的环节&#xff0c;当时就问我能不能给做一个&#xff0c;正好我也没啥事儿&#xff0c;就在周末的时候用C#做了一个&#xff0c;虽然派上用场了&#xf…

11个小技巧,玩转Spring!

前言最近有些读者私信我说希望后面多分享spring方面的文章&#xff0c;这样能够在实际工作中派上用场。正好我对spring源码有过一定的研究&#xff0c;并结合我这几年实际的工作经验&#xff0c;把spring中我认为不错的知识点总结一下&#xff0c;希望对您有所帮助。一 如何获取…

synchronized 的超多干货!

synchronized 这个关键字的重要性不言而喻&#xff0c;几乎可以说是并发、多线程必须会问到的关键字了。synchronized 会涉及到锁、升级降级操作、锁的撤销、对象头等。所以理解 synchronized 非常重要&#xff0c;本篇文章就带你从 synchronized 的基本用法、再到 synchronize…

团队项目—第二阶段第三天

昨天&#xff1a;快捷键的设置已经实现了 今天&#xff1a;协助成员实现特色功能之一 问题&#xff1a;技术上遇到了困难&#xff0c;特色功能一直没太大的进展。网上相关资料不是那么多&#xff0c;我们无从下手。 有图有真相&#xff1a; 转载于:https://www.cnblogs.com/JJJ…