Java并发-05-多线程并发设计模式-01

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模式的时候,需要注意以下两点:

  1. 对象的所有属性都是final的,并不能保证不可变性;

  1. 不可变对象也需要正确发布。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值