一、背景
最近在项目中遇见这么一个问题,多线程下访问一个单例中的一个属性,这个属性又不能在单例初始化的时候赋值,而是需要到在使用的时候调用仓储层的外部接口或者数据库查询接口获取。那么如果多线程状态下,这个属性肯定会被多次赋值,如果在赋值的时候另外的线程正好在获取使用,在极端情况下可能会出现NPE,所以想了一个逻辑来解决这个问题。
二、解决方案逻辑分析
首先,多个线程同时对一个属性去赋值,或多或少会出现在赋值期间,别的线程在使用的情况,我首先想到的时候,每个线程在取值之前都确认一下这个值是否存在,并且还要保证多个线程中只有一个线程能去给这个属性赋值(为了减少外部调用开销),不然也会出现这边在赋值那边在取值的情况。所以首先需要在调用外部、赋值属性的时候加一把锁,然后用volatile保证各个线程可见性,也为了防止指令重排,导致NPE的情况。
三、代码实现
StartThreadMain.java
public class StartThreadMain {
public static void main(String[] args) throws InterruptedException {
Thread test = new Thread(new MonitorValueObjectThread());
Thread test1 = new Thread(new AssignValueThread());
Thread test2 = new Thread(new AssignValueThread());
Thread test3 = new Thread(new AssignValueThread());
Thread test4 = new Thread(new AssignValueThread());
Thread test5 = new Thread(new AssignValueThread());
Thread test6 = new Thread(new AssignValueThread());
test.start();
test1.start();
test2.start();
test3.start();
test4.start();
test5.start();
test6.start();
Thread.sleep(100);
Thread test7 = new Thread(new AssignValueThread());
Thread test8 = new Thread(new AssignValueThread());
Thread test9 = new Thread(new AssignValueThread());
Thread test10 = new Thread(new AssignValueThread());
Thread test11 = new Thread(new AssignValueThread());
Thread test12 = new Thread(new AssignValueThread());
test7.start();
test8.start();
test9.start();
test10.start();
test11.start();
test12.start();
Thread.sleep(100000);
}
}
共享值对象ValueObject.java
import java.util.Map;
public class ValueObject {
private static ValueObject INSTANCE = new ValueObject();
private volatile Map<String, String> map;
public static ValueObject getInstance() {
return INSTANCE;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
赋值逻辑线程类AssignValueThread.java
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class AssignValueThread extends Thread {
private final Logger log = LoggerFactory.getLogger(AssignValueThread.class);
private static volatile AtomicBoolean isLoad = new AtomicBoolean(false);
@Override
public void run() {
log.info("到达线程");
if (CollectionUtils.isEmpty(ValueObject.getInstance().getMap()) && !isLoad.get()) {
log.info("等待锁");
synchronized (isLoad) {
if (!isLoad.get()) {
log.info("已加锁");
log.info("正在执行赋值逻辑");
// 伪造调用外部接口耗时10ms
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
// 赋值逻辑
Map<String, String> map = new HashMap<>();
map.put("10", "10");
ValueObject.getInstance().setMap(map);
log.info("已设置Map值");
isLoad.set(true);
log.info("isLoad赋值为True");
} else {
log.info("获得锁,但是发现属性已经赋值,不在调用赋值逻辑");
}
log.info("释放锁");
}
} else {
log.info("发现Map值已存在,或者赋值逻辑已被执行过了!");
}
}
}
监控共享值线程MonitorValueObjectThread.java
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
@Slf4j
public class MonitorValueObjectThread extends Thread {
private final Logger log = LoggerFactory.getLogger(MonitorValueObjectThread.class);
@Override
public void run() {
while (true) {
if (CollectionUtils.isEmpty(ValueObject.getInstance().getMap())) {
// log.info("------------Map值为空-------------");
} else {
log.info("------------Map值已存在-------------");
break;
}
}
}
}
结果:只有线程5执行了赋值逻辑,并且赋值后,后到的线程(比如线程15,正在等待锁的线程除外),直接可以返回已确认已赋值,直接获取属性值,不需要争夺锁资源。
15:49:27.877 [Thread-5] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.877 [Thread-3] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.877 [Thread-9] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.877 [Thread-13] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.882 [Thread-5] INFO com.cmbchina.mutithread.demo.AssignValueThread - 等待锁
15:49:27.877 [Thread-7] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.877 [Thread-11] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.882 [Thread-9] INFO com.cmbchina.mutithread.demo.AssignValueThread - 等待锁
15:49:27.882 [Thread-11] INFO com.cmbchina.mutithread.demo.AssignValueThread - 等待锁
15:49:27.882 [Thread-3] INFO com.cmbchina.mutithread.demo.AssignValueThread - 等待锁
15:49:27.882 [Thread-13] INFO com.cmbchina.mutithread.demo.AssignValueThread - 等待锁
15:49:27.882 [Thread-5] INFO com.cmbchina.mutithread.demo.AssignValueThread - 已加锁
15:49:27.882 [Thread-7] INFO com.cmbchina.mutithread.demo.AssignValueThread - 等待锁
15:49:27.882 [Thread-5] INFO com.cmbchina.mutithread.demo.AssignValueThread - 正在执行赋值逻辑
15:49:27.904 [Thread-5] INFO com.cmbchina.mutithread.demo.AssignValueThread - 已设置Map值
15:49:27.904 [Thread-5] INFO com.cmbchina.mutithread.demo.AssignValueThread - isLoad赋值为True
15:49:27.904 [Thread-1] INFO com.cmbchina.mutithread.demo.MonitorValueObjectThread - ------------Map值已存在-------------
15:49:27.904 [Thread-5] INFO com.cmbchina.mutithread.demo.AssignValueThread - 释放锁
15:49:27.904 [Thread-7] INFO com.cmbchina.mutithread.demo.AssignValueThread - 获得锁,但是发现属性已经赋值,不在调用赋值逻辑
15:49:27.904 [Thread-7] INFO com.cmbchina.mutithread.demo.AssignValueThread - 释放锁
15:49:27.904 [Thread-13] INFO com.cmbchina.mutithread.demo.AssignValueThread - 获得锁,但是发现属性已经赋值,不在调用赋值逻辑
15:49:27.904 [Thread-13] INFO com.cmbchina.mutithread.demo.AssignValueThread - 释放锁
15:49:27.904 [Thread-3] INFO com.cmbchina.mutithread.demo.AssignValueThread - 获得锁,但是发现属性已经赋值,不在调用赋值逻辑
15:49:27.904 [Thread-3] INFO com.cmbchina.mutithread.demo.AssignValueThread - 释放锁
15:49:27.904 [Thread-11] INFO com.cmbchina.mutithread.demo.AssignValueThread - 获得锁,但是发现属性已经赋值,不在调用赋值逻辑
15:49:27.905 [Thread-11] INFO com.cmbchina.mutithread.demo.AssignValueThread - 释放锁
15:49:27.905 [Thread-9] INFO com.cmbchina.mutithread.demo.AssignValueThread - 获得锁,但是发现属性已经赋值,不在调用赋值逻辑
15:49:27.905 [Thread-9] INFO com.cmbchina.mutithread.demo.AssignValueThread - 释放锁
15:49:27.982 [Thread-15] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.983 [Thread-23] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.982 [Thread-21] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.982 [Thread-19] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.983 [Thread-15] INFO com.cmbchina.mutithread.demo.AssignValueThread - 发现Map值已存在,或者赋值逻辑已被执行过了!
15:49:27.983 [Thread-23] INFO com.cmbchina.mutithread.demo.AssignValueThread - 发现Map值已存在,或者赋值逻辑已被执行过了!
15:49:27.983 [Thread-21] INFO com.cmbchina.mutithread.demo.AssignValueThread - 发现Map值已存在,或者赋值逻辑已被执行过了!
15:49:27.982 [Thread-17] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.983 [Thread-19] INFO com.cmbchina.mutithread.demo.AssignValueThread - 发现Map值已存在,或者赋值逻辑已被执行过了!
15:49:27.983 [Thread-25] INFO com.cmbchina.mutithread.demo.AssignValueThread - 到达线程
15:49:27.983 [Thread-25] INFO com.cmbchina.mutithread.demo.AssignValueThread - 发现Map值已存在,或者赋值逻辑已被执行过了!
15:49:27.983 [Thread-17] INFO com.cmbchina.mutithread.demo.AssignValueThread - 发现Map值已存在,或者赋值逻辑已被执行过了!