/**
* 三个售票员卖完50张票,总量完成即可,售票员每个月固定月薪
*/
public class ThreadLocalDemo {
public static void main(String[] args) {
MovieTicket movieTicket = new MovieTicket();
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
for (int j = 0; j < 20; j++) {
movieTicket.saleTicket();
try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
}
},String.valueOf(i)).start();
}
}
}
class MovieTicket{
int number = 50;
public synchronized void saleTicket(){
if (number > 0){
System.out.println(Thread.currentThread().getName()+"\t"+"号售票员卖出第: "+(number--));
}else{
System.out.println("--------------卖完了");
}
}
}
public class ThreadLocalDemo2 {
public static void main(String[] args) {
House house = new House();
new Thread(() ->{
try {
for (int i = 1; i <= 3; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
} finally {
//记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
house.threadLocal.remove();
}
},"t1").start();
new Thread(() ->{
try {
for (int i = 1; i <= 8; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
} finally {
//记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
house.threadLocal.remove();
}
},"t2").start();
new Thread(() ->{
try {
for (int i = 1; i <= 12; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
} finally {
//记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
house.threadLocal.remove();
}
},"t3").start();
System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
}
}
class House{
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void saleHouse(){
Integer value = threadLocal.get();
value++;
threadLocal.set(value);
}
}
要记得remove(),如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
案例总结:
并发环境下使用 SimpleDateFormat 的 parse 方法将字符串转换成 Date 对象,使用静态的成员变量是非常不安全的。
public class DateUtils {
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parseDate(String stringDate) throws Exception {
return sdf.parse(stringDate);
}
public static void main(String[] args) {
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
try {
System.out.println(DateUtils.parseDate("2022-12-12 11:11:11"));
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
SimpleDateFormat 类内部有一个Calendar对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,
如果你的 SimpleDateFormat 是个 static 的,那么多个 thread 之间就会共享这个 SimpleDateFormat,同时也是共享这个Calendar引用。
方法一:将SimpleDateFormat定义成局部变量。
public class DateUtils {
public static void main(String[] args) {
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("2020-11-11 11:11:11"));
sdf = null;
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。
方法二:ThreadLocal,线程本地变量或者线程本地存储
/**
* ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
*/
public class DateUtils2 {
private static final ThreadLocal<SimpleDateFormat> sdf_threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parseDateTL(String stringDate) throws ParseException {
return sdf_threadLocal.get().parse(stringDate);
}
public static void main(String[] args) {
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
try {
System.out.println(DateUtils2.parseDateTL("2022-12-12 11:11:11"));
} catch (ParseException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
方法三:DateTimeFormatter 代替 SimpleDateFormat
public class DateUtils2 {
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String format(LocalDateTime localDateTime){
return DATE_TIME_FORMATTER.format(localDateTime);
}
public static LocalDateTime parse(String dateString){
return LocalDateTime.parse(dateString,DATE_TIME_FORMATTER);
}
public static void main(String[] args) {
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
System.out.println(DateUtils2.parse("2022-12-12 11:11:11"));
},String.valueOf(i)).start();
}
}
}
threadLocalMap 实际上就是一个以 threadLocal 实例为 key,任意对象为 value 的 Entry 对象。
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map,不过它进行了两层包装,JVM内部维护了一个线程版的Map
内存泄露:指不再会被使用的对象或者变量占用的内存不能被回收。
内存泄露是如何造成的呢?这里我们先要了解什么是强、软、弱、虚四个引用?
假如有一个应用需要读取大量的本地图片:
如果每次读取图片都从硬盘读取则会严重影响性能,
如果一次性全部加载到内存中又可能造成内存溢出。
此时使用软引用可以解决这个问题:
用一个HashMap来保存 图片的路径 和 相应图片对象关联的软引用 之间的映射关系,在内存不足时,
JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
然后,回想刚才在上边看到的 ThreadLocalMap 源码,这个Map里边保存的是以 ThreadLocal 为 key 的对象,这个对象经过了两层包装:第一层使用 WeakReference 将 ThreadLocal 对象变成一个弱引用的对象,第二层是 定义了一个专门的类 Entry 来扩展 WeakReference。
使用弱引用就不会出问题了吗?这里还存在一个问题
总结:弱引用不能百分百保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remove() 方法来删除它。
1、ThreadLocal 不是解决线程间共享数据问题的,而是用于 变量在线程间隔离且在方法间共享的场景。
2、它隐式的在不同线程内创建独立实例副本,避免了实例线程的安全问题。
3、每个线程持有一个只属于自己的Map,维护了ThreadLocal对象与具体实例的映射,该Map只能被持有它的线程访问,故不存在线程安全以及锁的问题。
4、ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题。
5、通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法回收键为 null 的 Entry 对象,从而防止内存泄漏。