1-Immutability模式
所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。
如何实现一个具备不可变性的类?
将一个类所有的属性都设置成final的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是final的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。
JDK里很多类都具备不可变性,String和Long、Integer、Double等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。
1.1-JDK8中的String
String的核心成员变量字段:private final char value[]; 这个字段就是final的
再看substring方法,每次(除非无需截取)返回的是一个新的对象new,原来字符串不回发生变化
1.2-Immutability使用正确姿势
在使用Immutability模式的时候,需要注意以下两点:
对象的所有属性都是final的,并不能保证不可变性;
不可变对象也需要正确发布。
Foo具备不可变性,线程安全,但是类Bar并不是线程安全的,类Bar中持有对Foo的引用foo,对foo这个引用的修改在多线程中并不能保证可见性和原子性。
//Foo线程安全
final class Foo{
final int age=0;
final int name="abc";
}
//Bar线程不安全
class Bar {
Foo foo;
void setFoo(Foo f){
this.foo=f;
}
}
如果你的程序仅仅需要foo保持可见性,无需保证原子性,那么可以将foo声明为volatile变量,这样就能保证可见性。如果你的程序需要保证原子性,那么可以通过原子类来实现。
final AtomicReference<XXX> rf = new AtomicReference<>(new XXX(0,0));
2-享元模式
比如上面的string不变模式所有的修改操作都创建一个新的不可变对象,是不是创建的对象太多了,浪费,我们可以优化减少对象的创建呢?当然可以,享元模式就可以解决这种问题。
利用享元模式可以减少创建对象的数量,从而减少内存占用。Java语言里面Long、Integer、Short、Byte等这些基本数据类型的包装类都用到了享元模式。
享元模式本质上其实就是一个对象池,利用享元模式创建对象的逻辑也很简单:创建之前,首先去对象池里看看是不是存在;如果已经存在,就利用对象池里的对象;如果不存在,就会新创建一个对象,并且把这个新创建出来的对象放进对象池里。
看Integer的valueOf方法
IntegerCache默认缓存了-128到127的值;这个对象池在JVM启动的时候就创建好了,而且这个对象池一直都不会变化,也就是说它是静态的。
"Integer 和 String 类型的对象不适合做锁",其实基本上所有的基础类型的包装类都不适合做锁,因为它们内部用到了享元模式,这会导致看上去私有的锁,其实是共有的。
new Integer(3) ==new Integer(3) 返回true
3-Copy-on-Write模式
所谓Copy-on-Write,经常被缩写为COW或者CoW,顾名思义就是写时复制。不可变对象的写操作往往都是使用Copy-on-Write方法解决的,当然Copy-on-Write的应用领域并不局限Immutability模式。
Java中CopyOnWriteArrayList和CopyOnWriteArraySet这两个Copy-on-Write容器,它们背后的设计思想就是Copy-on-Write;通过Copy-on-Write这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。
CopyOnWriteArrayList#add方法
CopyOnWriteArrayList和CopyOnWriteArraySet这两个Copy-on-Write容器在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用Copy-on-Write容器效果就非常好了。
3-线程本地存储模式ThreadLocal
多个线程同时读写同一共享变量存在并发问题。那如果变量不共享,每个线程使用自己的变量就不存在线程安全问题。没有共享,就没有伤害。
3.1-ThreadLocal介绍
ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在堆内的 TLAB 中。
ThreadLocal 实例通常来说都是 private static 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的。
ThreadLocal 采用以空间换时间的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰。
3.2-ThreadLocal应用场景
ThreadLocal 适用于以下场景:
(1)在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
(2)在hiberate中管理session。
(3)在JDK8之前,为了解决SimpleDateFormat的线程安全问题。
(4)获取当前登录用户上下文。
(5)临时保存权限数据。
(6)使用MDC保存日志信息。
public class DateUtil {
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static String format(Date date) {
return threadLocal.get().format(date);
}
}
ThreadLocal原理以及底层结构单独文章介绍,不在此文章介绍之列。
3.3-父子线程如何共享数据
前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。
public class TestThreadLocal {
public static void main(String[] args) {
ThreadLocal<String> threadLocal=new ThreadLocal<>();
threadLocal.set("hello,threadLocal");
System.out.println("父线程获取数据:"+threadLocal.get());
new Thread(()->{
System.out.println("子线程获取数据:"+threadLocal.get());
}).start();
}
}
控制台输出:
父线程获取数据:hello,threadLocal
子线程获取数据:null
3.3.1-InheritableThreadLocal---JDK自带
public class TestInheritableThreadLocal {
public static void main(String[] args) {
ThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
threadLocal.set("hello,InheritableThreadLocal");
System.out.println("父线程获取数据:"+threadLocal.get());
new Thread(()->{
System.out.println("子线程获取数据:"+threadLocal.get());
}).start();
}
}
控制台输出:
父线程获取数据:hello,InheritableThreadLocal
子线程获取数据:hello,InheritableThreadLocal
3.3.2-TransmittableThreadLocal
在真实的业务场景中,一般很少用单独的线程,绝大多数,都是用的线程池。那么,在线程池中如何共享ThreadLocal对象生成的数据呢?
问题:
public class TestInheritableThreadLocal2 {
public static void main(String[] args) {
InheritableThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
threadLocal.set("hello,InheritableThreadLocal");
System.out.println("父线程获取数据:"+threadLocal.get());
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(()->{
System.out.println("第一次子线程获取数据:"+threadLocal.get());
});
threadLocal.set("world,InheritableThreadLocal");
executorService.submit(()->{
System.out.println("第二次子线程获取数据:"+threadLocal.get());
});
}
}
控制台输出:
父线程获取数据:hello,InheritableThreadLocal
第一次子线程获取数据:hello,InheritableThreadLocal
第二次子线程获取数据:hello,InheritableThreadLocal
原因分析:
第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。所以我们看到,在主线程中将数据设置成hello,InheritableThreadLocal,第一次从线程池中获取了正确的数据hello,InheritableThreadLocal。
之后,在主线程中又将数据改成world,InheritableThreadLocal,但在第二次从线程池中获取数据却依然是hello,InheritableThreadLocal。
因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据world,InheritableThreadLocal,还是获取的老数据hello,InheritableThreadLocal。
那么,这该怎么办呢?
答:使用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
public class TestTransmittableThreadLocal {
public static void main(String[] args) {
TransmittableThreadLocal<String> threadLocal=new TransmittableThreadLocal<>();
threadLocal.set("hello,InheritableThreadLocal");
System.out.println("父线程获取数据:"+threadLocal.get());
ExecutorService executorService =TtlExecutors.getTtlExecutorService(Executors.newSingleThreadExecutor());
executorService.submit(()->{
System.out.println("第一次子线程获取数据:"+threadLocal.get());
});
threadLocal.set("world,InheritableThreadLocal");
executorService.submit(()->{
System.out.println("第二次子线程获取数据:"+threadLocal.get());
});
}
}
控制台输出:
父线程获取数据:hello,InheritableThreadLocal
第一次子线程获取数据:hello,InheritableThreadLocal
第二次子线程获取数据:world,InheritableThreadLocal