前提
当多个线程或者多个模块同时访问同一个对象的数据时,会出现什么样的情况呢?
例如:我们模拟两个线程来对同一个变量进行赋值,然后赋值之后,用两个对象来取值,我们期望的是每个线程中对象取出来的值是该线程产生的,代码如下:
public class ThreadShareDate {
private static Integer date = null;
static public void main(String... args){
for (int i = 0; i < 2; i++){
new Thread(new Runnable() {
@Override
public void run() {
// 首先随机产生一个值
int i1 = new Random().nextInt();
// 将随机值赋给data
date = i1;
System.out.println(Thread.currentThread().getName()+"随机生成了一个值:"+date);
// 调用两个对象的取值方法
new Worker().get(date);
new User().get(date);
}
}).start();
}
}
}
class Worker{
public void get(Integer data){
System.out.println("worker-"+Thread.currentThread().getName()+"获取到数据:"+data);
}
}
class User{
public void get(Integer data){
System.out.println("user-"+Thread.currentThread().getName()+"获取到数据:"+data);
}
}
输出结果:
Thread-1随机生成了一个值:1665052484
worker-Thread-1获取到数据:1665052484
user-Thread-1获取到数据:1665052484
Thread-0随机生成了一个值:1665052484
worker-Thread-0获取到数据:1665052484
user-Thread-0获取到数据:1665052484
重复执行该代码,并没有达到我们想要的效果,发现两个线程产生的值是一样的,是因为随机值每次都产生的相同的吗?其实并不是;原因是因为当第一个线程在执行打印操作的时候,第二个线程重新对data进行了赋值操作,所以会导致出现这种值相同的情况;
示意图:
如图:我们希望多个线程操作相同的变量或者表达式所产生的数据是和线程绑定在一起的,即是说单个线程范围类的数据是共享的,而不应该被其他线程所影响到!
应用场景:转账由一个事物控制着,如果张三给李四转账,使用connection 打开事物和提交事物;与此同时,王五也给找六转账开启了事物,赵六收到账后此时用connection关闭事物,如果这两个转账的connection是同一个对象,那么再张三给李四转账还没完成时就关闭了connection连接,就会出现错误。
那么应该如何解决这个问题呢?可以想到,我们用一个map来存数据,用当前线程来做key,代码如下:
public class ThreadShareDate {
private static Map<Thread,Integer> shareMap = new HashMap<>();
static public void main(String... args){
for (int i = 0; i < 2; i++){
new Thread(new Runnable() {
@Override
public void run() {
// 首先随机产生一个值
int i1 = new Random().nextInt();
shareMap.put(Thread.currentThread(),i1);
System.out.println(Thread.currentThread().getName()+"随机生成了一个值:"+i1);
new Worker().get(shareMap);
new User().get(shareMap);
}
}).start();
}
}
}
class Worker{
public void get(Map<Thread,Integer> shareMap){
System.out.println("worker-"+Thread.currentThread().getName()+"获取到数据:"+shareMap.get(Thread.currentThread()));
}
}
class User{
public void get(Map<Thread,Integer> shareMap){
System.out.println("user-"+Thread.currentThread().getName()+"获取到数据:"+shareMap.get(Thread.currentThread()));
}
}
输出结果:
Thread-0随机生成了一个值:1110940790
worker-Thread-0获取到数据:1110940790
user-Thread-0获取到数据:1110940790
Thread-1随机生成了一个值:-54151318
worker-Thread-1获取到数据:-54151318
user-Thread-1获取到数据:-54151318
这样做就可以避免其他线程在运行时,造成相互之间的影响,虽然是多个线程操作同一个shareMap,但是因为key不相同,所以线程内的数据不会收到影响;
使用ThreadLocal同样可以达到相同的效果,代码如下:
public class ThreadShareDate {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
static public void main(String... args){
for (int i = 0; i < 2; i++){
new Thread(new Runnable() {
@Override
public void run() {
// 首先随机产生一个值
int i1 = new Random().nextInt();
threadLocal.set(i1);
System.out.println(Thread.currentThread().getName()+"随机生成了一个值:"+i1);
new Worker().get(threadLocal);
new User().get(threadLocal);
}
}).start();
}
}
}
class Worker{
public void get(ThreadLocal<Integer> threadLocal){
System.out.println("worker-"+Thread.currentThread().getName()+"获取到数据:"+threadLocal.get());
}
}
class User{
public void get(ThreadLocal<Integer> threadLocal){
System.out.println("user-"+Thread.currentThread().getName()+"获取到数据:"+threadLocal.get());
}
}
输出结果:
Thread-0随机生成了一个值:816945296
Thread-1随机生成了一个值:372017075
worker-Thread-1获取到数据:372017075
user-Thread-1获取到数据:372017075
worker-Thread-0获取到数据:816945296
user-Thread-0获取到数据:816945296
可以看到,当我们设定好泛型之后,直接将值set进去,然后调用get方法就能够取到当前线程所产生的值,这是为什么呢?我们先看下ThreadLocal的set,get源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
可以看到,ThreadLocal 也是封装了一个map,然后用当前线程做key来进行存值和取值,所以多个线程同时进行操作时,可以正确的获取到当前线程的值;
ps:至于更多详细的源码,需要大家自己去了解了,比如代码中的ThreadLocalMap
说到这儿,我们都是只在玩一个数据,那么如果多个数据应该怎么办呢?面向对象的思维告诉我们,将多个数据封装到一个对象里就行了,没错,代码如下:
public class ThreadShareDate {
private static ThreadLocal<SuperMan> threadLocal = new ThreadLocal<>();
static public void main(String... args){
for (int i = 0; i < 2; i++){
new Thread(new Runnable() {
@Override
public void run() {
// 首先随机产生一个值
int i1 = new Random().nextInt();
// 将数据封装成对象然后存入到threadlocal里面去
threadLocal.set(new SuperMan(i1,"durant"+i1));
System.out.println(Thread.currentThread().getName()+"随机生成了一个值:"+i1);
// 调用取值方法
new Worker().get(threadLocal);
new User().get(threadLocal);
}
}).start();
}
}
}
class Worker{
public void get(ThreadLocal<SuperMan> threadLocal){
SuperMan superMan = threadLocal.get();
System.out.println("worker-"+Thread.currentThread().getName()+"获取到数据:"+superMan.toString());
}
}
class User{
public void get(ThreadLocal<SuperMan> threadLocal){
SuperMan superMan = threadLocal.get();
System.out.println("user-"+Thread.currentThread().getName()+"获取到数据:"+superMan.toString());
}
}
class SuperMan{
private Integer money;
private String name;
public SuperMan(Integer money,String name){
this.money = money;
this.name = name;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "name="+name+",money="+money;
}
}
输出结果:
Thread-1随机生成了一个值:-1071341512
worker-Thread-1获取到数据:name=durant-1071341512,money=-1071341512
Thread-0随机生成了一个值:-1919276389
worker-Thread-0获取到数据:name=durant-1919276389,money=-1919276389
user-Thread-1获取到数据:name=durant-1071341512,money=-1071341512
user-Thread-0获取到数据:name=durant-1919276389,money=-1919276389
这样就可以达到我们想要的目的了,但是这样做是不好的,为什么呢?因为将threadlocal暴露出来了,我们可以尝试着将threadlocal封装到对象内部,然后直接使用不就更好了?代码如下:
public class ThreadShareDate {
private static ThreadLocal<SuperMan> threadLocal = new ThreadLocal<>();
static public void main(String... args){
for (int i = 0; i < 2; i++){
new Thread(new Runnable() {
@Override
public void run() {
// 首先随机产生一个值
int i1 = new Random().nextInt();
// 我们操作的对象就是从threadlocal 里面根据当前线程取出来的对象
SuperMan superMan = SuperMan.getShareDateInstance();
superMan.setMoney(i1);
superMan.setName("durant"+i1);
System.out.println(Thread.currentThread().getName()+"随机生成了一个值:"+superMan.toString());
new Worker().get(superMan);
new User().get(superMan);
}
}).start();
}
}
}
class Worker{
public void get(SuperMan superMan){
System.out.println("worker-"+Thread.currentThread().getName()+"获取到数据:"+superMan.toString());
}
}
class User{
public void get(SuperMan superMan){
System.out.println("user-"+Thread.currentThread().getName()+"获取到数据:"+superMan.toString());
}
}
class SuperMan{
private Integer money;
private String name;
// 私有化构造器
private SuperMan(){}
// 将threadlocal封装到对象内部,没有暴露出去
private static ThreadLocal<SuperMan> map = new ThreadLocal<>();
// 提供一个提供对象的方法
public static SuperMan getShareDateInstance(){
// 直接从ThreadLocal中取当前线程的对象,如果没有,造一个对象存进去
SuperMan superMan = map.get();
if (superMan == null){
superMan = new SuperMan();
map.set(superMan);
}
return superMan;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "name="+name+",money="+money;
}
}
输出结果:
Thread-0随机生成了一个值:name=durant802953833,money=-802953833
worker-Thread-0获取到数据:name=durant-802953833,money=-802953833
user-Thread-0获取到数据:name=durant-802953833,money=-802953833
Thread-1随机生成了一个值:name=durant-2101316927,money=-2101316927
worker-Thread-1获取到数据:name=durant-2101316927,money=-2101316927
user-Thread-1获取到数据:name=durant-2101316927,money=-2101316927
总结
ThreadLocal可以很好的实现线程范围内的数据共享,当我们设计对象时,将ThreadLocal封装到对象内部,体现了面向对象的思维,调用者就只用关心对象的数据就行了!