并发问题产生条件:多个线程同时对共享变量进行读写操作。
解决并发问题,我们平时都是加互斥锁,防止同时进行读写操作,其实换个角度想,避免共享,就可以了。
接下来介绍三种避免共享的三种模式:
Immutability 模式、Copy-on-Write 模式和线程本地存储模式。
Immutability 模式(不变性模式)
该模式解决并发问题的思路就是让共享变量只有读操作,没有写操作。即,不变性,**就是创建对象之后,状态就不再发生变化。**变量一旦赋值就不允许赋值。
如何实现不可变性的类?
- 将所有的属性都设置成final;
- 只允许存在只读方法;
- 如果类的属性是引用型,该属性对应的对象也应该是不可变对象。
不可变对象的应用
其实JAVA SDK中很多类都具备不可变性,比如经常用到的基本类型的包装类Integer,String,Long,Double等,内部都实现了不可变性,其属性都是final类型的,但是你会发现一个问题,这些类中有存在修改的方法,比如String.replace方法,这是怎么回事呢?
String replace(char oldChar, char newChar) {
。。。。省略
//创建一个新的字符串返回
//原字符串不会发生任何变化
return new String(buf, true);
}
它是创建了新的String对象返回,并没有破坏不可变性类的原理,这里你就恍然大悟了把。
但是如果你一直修改replace,就一直创建对象返回,浪费内存,并且创建耗费时间,不太好吧,那如何解决呢?
其实JAVA SDK是使用了享元模式来解决,那什么是享元模式呢,其实它本质上就是一个对象池,创建对象之前先判断该对象池中是否存在该对象,存在直接去对象池中的对象返回,不存在,创建对象返回,并且放到缓存池当中。
下面看Long内部是如何实现的。
//其实JVM在启动时就在方法区中创建好了存放[-128,127]Long对象的静态对象池。
static class LongCache {
static final Long cache[]
= new Long[-(-128) + 127 + 1];
static {
for(int i=0; i<cache.length; i++)
cache[i] = new Long(i-128);
}
}
Long valueOf(long l) {
final int offset = 128;
// 取的时候直接从该静态对象池中取
if (l >= -128 && l <= 127) {
return LongCache
.cache[(int)l + offset];
}
return new Long(l);
}
所以你有时候会发现创建的Long s1=12,Long s2=12,s1==s2是true,引用的是同一个对象!
Copy-on-Write模式
看到这个,你应该想到两个线程安全的容器,CopyOnWriteArrayList和CopyOnWriteArraySet,这两个容器就是该模式实现的。
这两个容器实现原理就是,在读的时候它是不加锁的,在进行写操作时,不是直接在原对象上修改,而是先copy原对象,然后修改copy的对象,最后把该对象指向修改后的对象。
使用场景:读多写少的情况
举例:dubbo
dubbo在做负载均衡后,客户端请求后,它是使用自定义tcp远程调用服务RPC前,会根据请求拉取对应的服务路由表(这个就是对路由表读操作),然后根据一定的算法选择一个服务访问。当然dubbo也提供了对应的添加服务或者删除服务(对路由表写操作)。
在项目中,请求数量是非常大的,但是添加或者删除服务是非常少的,所以该读多写少非常适合做这个。
线程本地存储模式
顾名思义,就是把数据存放在每个线程自己的空间中,不共享。
下面直接上代码看如何使用Threadlocal
static class ThreadId {
static final AtomicLong
nextId=new AtomicLong(0);
//定义ThreadLocal变量
static final ThreadLocal<Long>
tl=ThreadLocal.withInitial(
()->nextId.getAndIncrement());
//此方法会为每个线程分配一个唯一的Id
static long get(){
return tl.get();
}
}
下面就讲解一下java中Threadlocal原理:
其实也没啥,就是给每个线程分配一块独属于自己的空间就可以,你可能会想到map,kay作为线程的唯一表示,value作为存放的值。
确实,你会发现源码Thread中存在一个map.
class Thread {
//内部持有ThreadLocalMap
ThreadLocal.ThreadLocalMap
threadLocals;
}
Threadlocal作为工具类,用自己线程从Thread中的map取出属于自己线程的值。
class ThreadLocal<T>{
public T get() {
//首先获取线程持有的
//ThreadLocalMap
ThreadLocalMap map =
Thread.currentThread()
.threadLocals;
//在ThreadLocalMap中
//查找变量
Entry e =
map.getEntry(this);
return e.value;
其实,你会发现它把这个map放到每个Thread中,为什么不放到ThreadLocal呢?
答:防止产生内存泄漏,因为ThreadLocal是一个工具类,在调用后,你不主动释放,它是不会主动回收的。
而Thread线程是结束后可以回收的,那么map也就可以回收了。
所以你要是在线程池中使用ThreadLocal就需要手动清理ThreadLocal ,否则线程不回收,map也不回收,就会造成内存泄漏。
来源:CSDN
作者:qq_599571116
链接:https://blog.csdn.net/qq_42634696/article/details/104699773