并发的设计模式(一)

1.写在前面

笔者介绍并发的工具类,以及基础的东西,今天笔者打算来介绍一下并发中的设计模式。

2.不变性模式

2.1概述

不变性模式,所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。

2.2快速实现具备不可变性的类

就是将一个类所有的属性都设置成final的,并且只允许存在只读的方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是final的,也就是不允许继承。

java中一些常用的类String,Long,Integer,Double等,它们的类和属性都是final的,所有的方法均是只读的。可能有读者会问,String不是提供了修改的方法吗?其实做法就是创建一个新的不可变的对象,这是与可变对象一个重要的区别,可变对象往往是修改自己的属性。

2.3利用享元模式避免创建重复对象

java中一些常用类都是利用享元模式来优化对象的创建的。

享元模式本质上其实就是一个对象池,利用享元模式创建对象的逻辑也很简单:创建之前,首先去对象池里看看是不是存在;如果已经存在;就利用对象池里的对象;如果不存在,就会创建一个对象,并且把这个新创建出来的对象放进对象池里。

2.4使用不可变性模式的注意事项

  • 对象的所有属性都是final的,并不能保证不可变性
  • 不可变对象也需要正确发布

在使用不可变的模式的时候一定要确保保持不可变性的边界在哪里,是否要求属性对象也具备不可变性。

//Foo 线程安全
final class Foo{
	final int age=0;
	final int name="abc";
}
//Bar 线程不安全
class Bar {
	Foo foo;
	void setFoo(Foo f){
		this.foo=f;
	}
}

3.Copy-On-Write模式

3.1概述

写时复制,通常用来解决不可变模式的写的操作的。

3.2应用场景

CopyOnWriteArrayList和CopyOnWriteArraySet这两个容器。对应的是Copy-on-Write 容器,这个Copy-on-Write更多的体现是一种延时的策略,只有在真正需要赋值的时候才复制,而不是提前复制好。同时Copy-on-Write 还支持按需复制,所以 Copy-on-Write 在操作系统领域是能够提升性能的。

在操作系统领域,除了创建进程用到了 Copy-on-Write,很多文件系统也同样用到了,例如 Btrfs (B-Tree File System)、aufs(advanced multi-layered unification filesystem)等。

Docker 容器镜像的设计是 Copy-on-Write,甚至分布式源码管理系统 Git 背后的设计思想都有 Copy-on-Write

Copy-on-Write 最大的应用领域还是在函数式编程领域。

CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用 Copy-on-Write 容器效果就非常好了

4.线程本地存储

3.1概述

没有共享,就没有伤害,避免共享

3.2ThreadLocal的使用方法

我们都知道SimpleDateFormat 不是线程安全的,那如果需要在并发场景下使用它,你该怎么办呢?

其实有一个办法就是用 ThreadLocal 来解决,下面的示例代码就是 ThreadLocal 解决方案的具体实现,这段代码与前面 ThreadId 的代码高度相似,同样地,不同线程调用SafeDateFormat 的 get() 方法将返回不同的 SimpleDateFormat 对象实例,由于不同线程并不共享 SimpleDateFormat,所以就像局部变量一样,是线程安全的。

static class SafeDateFormat {
	// 定义 ThreadLocal 变量
	static final ThreadLocal<DateFormat> tl=ThreadLocal.withInitial(
		()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
	static DateFormat get(){
		return tl.get();
	}
}
// 不同线程执行下面代码
// 返回的 df 是不同的
DateFormat df = SafeDateFormat.get();

3.3ThreadLocal的工作原理

自己实现的

class MyThreadLocal<T> {
	Map<Thread, T> locals = new ConcurrentHashMap<>();
	// 获取线程变量
	T get() {
		return locals.get(
		Thread.currentThread());
	}
	// 设置线程变量
	void set(T t) {
		locals.put(
		Thread.currentThread(), t);
	}
}

java实现的

class Thread {
	// 内部持有 ThreadLocalMap
	ThreadLocal.ThreadLocalMap threadLocals;
}
class ThreadLocal<T>{
	public T get() {
		// 首先获取线程持有的
		//ThreadLocalMap
		ThreadLocalMap map = Thread.currentThread().threadLocals;
		// 在 ThreadLocalMap 中
		// 查找变量
		Entry e = map.getEntry(this);
		return e.value;
	}
	static class ThreadLocalMap{
        // 内部是数组而不是 Map
        Entry[] table;
        // 根据 ThreadLocal 查找 Entry
        Entry getEntry(ThreadLocal key){
        // 省略查找逻辑
		}
        //Entry 定义
        static class Entry extends WeakReference<ThreadLocal>{
            Object value;
        }
     }
}

不容易产生内存泄露。在我们的设计方案中,ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这就意味着,只要 ThreadLocal对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有
ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用 (WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。Java 的这种实现方案虽然看上去复杂一些,但是更加安全。

3.4ThreadLocal 与内存泄露

在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?原因就出在线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着 Thread 持有的 ThreadLocalMap一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。

那在线程池中,我们该如何正确使用 ThreadLocal 呢?其实很简单,既然 JVM 不能做到自动释放对 Value 的强引用,那我们手动释放就可以了。如何能做到手动释放呢?估计你马上想到try{}finally{}方案了,这个简直就是手动释放资源的利器。示例的代码如下,你可以参考学习。

ExecutorService es;
ThreadLocal tl;
es.execute(()->{
	//ThreadLocal 增加变量
	tl.set(obj);
	try {
		// 省略业务逻辑代码
	}finally {
		// 手动清理 ThreadLocal
		tl.remove();
	}
});

3.5InheritableThreadLocal 与继承性

通过 ThreadLocal 创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过ThreadLocal 创建了线程变量 V,而后该线程创建了子线程,你在子线程中是无法通过ThreadLocal 来访问父线程的线程变量 V 的。

如果你需要子线程继承父线程的线程变量,那该怎么办呢?其实很简单,Java 提供了InheritableThreadLocal 来支持这种特性,InheritableThreadLocal 是 ThreadLocal 子类,所以用法和 ThreadLocal 相同,这里就不多介绍了。

不过,我完全不建议你在线程池中使用 InheritableThreadLocal,不仅仅是因为它具有ThreadLocal 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 InheritableThreadLocal,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命。

5.写在最后

本篇博客大概的介绍了下几种并发的设计模式。后面还会介绍其他的并发的设计模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值