并发设计模式1-避免共享

并发问题产生条件:多个线程同时对共享变量进行读写操作。
解决并发问题,我们平时都是加互斥锁,防止同时进行读写操作,其实换个角度想,避免共享,就可以了。

接下来介绍三种避免共享的三种模式:
Immutability 模式、Copy-on-Write 模式和线程本地存储模式。

Immutability 模式(不变性模式)

该模式解决并发问题的思路就是让共享变量只有读操作,没有写操作。即,不变性,**就是创建对象之后,状态就不再发生变化。**变量一旦赋值就不允许赋值。

如何实现不可变性的类?

  1. 将所有的属性都设置成final;
  2. 只允许存在只读方法;
  3. 如果类的属性是引用型,该属性对应的对象也应该是不可变对象。

不可变对象的应用

其实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也不回收,就会造成内存泄漏。

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/104699773