第六章 保障线程安全的设计技术--《java多线程编程实战指南-核心篇》

栈空间是为线程的执行而准备的一段固定大小的内存空间,每个线程都由其栈空间。栈空间实在线程创建的时候分配的。线程执行一个方法前,java虚拟机会在该线程的栈空间中为这个方法调用创建一个栈帧。栈帧用于存储相应方法的局部变量、返回值等私有数据。

无状态对象不含任何实例变量,不包含任何静态变量或者其包含的静态变量都是只读的,是线程安全的。

不可变对象是指一经创建其状态就保持不变的对象,也具有线程安全性。

不可变对象需满足以下条件:

  • 类本身使用final修饰,这是为了防止通过创建子类来改变其定义的行为。
  • 所有字段都是用final修饰的:使用final修饰不仅仅是从语义上说明被修饰字段的值不可改变;更重要的是这个语义在多线程环境下保证了被修饰字段的初始化安全,即final修饰的字段在对其他线程可见时,它必定是初始化完成的。
  • 对象在初始化过程中没有逸出:防止其他类(如该类的内部匿名类)在对象初始化过程中修改其状态。
  • 任何字段,若其引用了其他状态可变的对象(如集合、数组等),则这些字段必须是private修饰的,并且这些字段值不能对外暴露。若有相关方法要返回这些字段值,则应该进行防御性赋值如调用Collections.unmodifiableSet()。

ThreadLocal

ThreadLocal是线程持有对象,该实例通常会被作为某个类的静态字段使用。

ThreadLocal可能存在的问题:1.退化和数据错乱,即当一个线程执行多个任务时,残留上个任务执行的结果,因此在每次使用前应当先清空;2.ThreadLocal可能导致内存泄漏、伪内存泄漏。

ThreadLocal的内部实现机制:在java平台中,每个线程(Thread实例)内部会维护一个类似HashMap的对象,我们称之为ThreadLocalMap。每个ThreadLocalMap内部都会包含若干个Entry。因此,我们可以说每个线程都拥有若干个这样的条目,相应的线程就被称为这些条目的属主线程。Entry的key是一个ThreadLocal实例,value是一个线程持有对象。因此,Entry的作用相当于为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系。由于Entry对ThreadLocal实例的引用(通过key引用)是一个弱引用,因此它不会阻止被引用的ThreadLocal实例被垃圾回收,即其所在的Entry的Key会被置为null。此时,相应的Entry就成了无效条目。另一方面,由于Entry对线程特有对象的引用是强引用,因此如果无效条目本身对他的可达强引用,那么无效条目也会阻止其引用的线程特有对象被垃圾回收。有鉴于此,当ThreadLocalMap中有新的ThreadLocal到线程持有的对象的映射关系被创建(相当于有新的Entry被添加到ThreadLocalMap)的时候,ThreadLocalMap会将无效条目清理掉,这打破了无效条目对象特有对象的强引用,从而使相应的线程特有对象能够被垃圾回收。但是,这个处理也有一个缺点--一个线程访问过线程局部变量之后如果改线程有对其可达的强引用,并且该线程长时间内处于非运行状态,那么该线程的ThreadLocalMap可能就不会有任何变化,因此相应的ThreadLocalMap中的无效条目也不会被清理,这就可能导致这些线程的各个Entry所引用的线程特有对象都无法被垃圾回收,即导致了伪内存泄漏。

内存泄漏指由于对象无法永远无法被垃圾回收导致其占用的java虚拟机内存无法被释放。持续的内存泄漏会导致java虚拟机可用的内存主键减少,并最终可能导致java虚拟机内存溢出OOM,知道JVM宕机。

伪内存泄漏类似于内存泄漏。所不同的是,伪内存泄漏中对象所占用的内存在其不再被使用后的相当长时间仍然无法被回收,甚至可能永远无法被回收。业绩就是说,伪内存泄漏中对象占用的内存空间可能会被回收,也可能永远无法被回收。

装饰器模式

装饰器模式可以用来实现线程安全,其基本思想是为非线程安全对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全对象而是访问其外包装对象。外包装对象与相应的非线程安全对象具有相同的接口,因此客户端代码使用外包装对象的方式与直接使用相应的非线程安全对象的方式相同,而外包装对象内部通常会借助锁,以线程安全的方式调用响应非线程安全对象的同签名方法来实现其对外暴露的各个方法。如:

Collections.synchronizedList()、Collections.synchronizedMap()、Collections.synchronizedXXX()。。。。

使用装饰器模式来实现线程安全的一个好处就是关注点分离;

使用装饰器模式实现线程安全存在一些缺点(特指上方jdk提供的方式):

  • 首先这些同步集合的iterator方法返回的Iterator实例并不是线程安全的。为了保障对同步集合的遍历操作的线程安全性,我们需要对遍历操作进行加锁
    package JavaCoreThreadPatten.capter06;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Iterator;
    import java.util.List;
    
    public class SyncCollectionSafeTraversal {
        final List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        public void test(){
            Iterator<String> iterator = syncList.iterator();
            //需要对该对象进行加锁,因为返回的Iterator是非线程安全的,降低了并发性能
            synchronized (syncList){
                while (iterator.hasNext()){
                    System.err.println(iterator.next());
                }
            }
        }
    }
    

    对同步集合进行遍历操作的时候,我们需要以被遍历同步集合对象本身作为内部锁。这样做实质上是利用了内部锁的排他性,从而阻止了遍历过程中其他线程改变了同步集合的内部结构。因此,这种遍历是不利于提高并发性的。另外,对便利操作进行加锁时,我们选用的内部锁必须和相应的同步集合内部用于保障其自身线程安全所使用的锁保持一致。也就是说,这一定程度上要求我们必须知道同步集合对象内部的一些细节,显然这是有悖于面向对象编程中的信息封装原则。

  • 其次,这些同步集合在其实现线程安全的时候通常是使用一个粗粒度的锁,即使用一个锁来保护其内部所有的共享状态。因此,这些同步集合虽然可以确保线程安全,但是也可能导致锁的高争用,从而导致较大的上下文切换的开销。

并发集合

并发集合对象自身就支持对其进行线程安全的遍历操作。应用代码对并发集合对象进行遍历的时候无需加锁就可以实现便利操作的线程安全。并且,对并发集合的遍历操作和对其进行更新操作是可以由不同的线程并发执行的,从而有利于充分提高系统的并发性。

并发集合实现线程安全的遍历通常由两种方式。一种是对待遍历对象的快照进行遍历。快照是在Iterator实例被创建的那一刻待遍历对象内部结构的一个只读副本,它反映了待遍历集合的某一时刻的状态。由于对同一个并发集合进行遍历操作的每个线程会得到各自的一分快照,因此快照相当于这些线程的线程特有对象。所以,这种方式下进行遍历操作的线程无需加锁就可以实现线程安全。另外,由于快照是只读的,因此这种遍历方式所返回的Iterator实例是不支持remove方法的。这种方式的优点是遍历操作和更新操作之间互不影响,缺点是当被遍历的集合比较大时,创建快照的直接或者间接开销会比较大。CopyOnWriteArrayList和CopyOnWriteArraySet就使用这种遍历方式。另一种是对待遍历对象进行准实时的变量。所谓准实时是指遍历操作不是针对待遍历对象的副本进行的,但又不借助锁来保障线程安全,从而使得遍历操作可以与更新操作并发进行。并且,遍历过程中其他线程对被遍历对象的内部结构的更新可能会(也可能不会)被反映出来。这种遍历方式所返回的Iterator实例可以支持remove方法。ConcurrentLinkedQueue和ConcurrentHashMap等并发集合就采用这种遍历方式。由于Iterator是被设计用来一次只被一个线程使用的,因此如果有多个线程需要进行遍历操作,那么这些线程之间是不适宜共享同一个Iterator实例的。

如果有多个线程需要对同一个并发集合进行遍历操作,那么这些线程不适合共享同一个Iterator实例

另外,并发集合内部在保障其线程安全的时候通常不借助锁,而是使用CAS操作,或者对锁进行了优化,比如使用粒度极小的锁。因此,并发集合的可伸缩性一般要比相应的同步集合高,即使用并发集合的程序相比于使用相应同步集合的程序而言,并发线程数的增加所带来的程序的吞吐率的提升要更加显著。而使用同步集合的程序随着并发线程数量的上升,这些同步集合内部所使用的锁的争用所导致的上下文切换开销越来越大,最终有可能使程序的吞吐率一定程度上降低或者恒定到一定的水平。

ConcurrentLinkedQueue是Queue接口的一个线程安全类,它相当于LinkedList的线程安全办,可以作为Collections.synchronizedList()的替代品。ConcurrentLinkedQueue内部访问其共享状态变量的时候并不需要借助锁,而是使用CAS操作来保障线程安全的。因此,ConcurrentLinkedQueue是非阻塞的,其使用不会导致当前线程被暂停,因此也就避免了上下文切换的开销。ConcurrentLinkedQueue所使用的遍历方式是准实时。与BlockQueue的实现类相比,ConcurrentLinkedQueue更适合于更新操作和遍历操作并发的场景,比如一个或多个线程往/从队列中添加/删除元素,而另一个或多个线程则对相应队列进行遍历操作。而BlockingQueu的实现类则更适合于多个线程并发更新同一队列的场景,比如在生产者-消费者模式中生产者线程往队列中添加元素,而消费者线程从队列中移除元素。(这个对比没看明白

ConcurrentHashMap是Map接口的一个线程安全实现类,它相当于HashMap的线程安全版,可以作为Hashtable和Collections.synchronizedMap()的替代品。ConcurrentHashMap内部使用类粒度极小的锁(分段锁)来保障其线程安全。ConcurrentHashMap的读取操作基本上不会导致锁的使用。另外,默认情况下ConcurrentHashMap可以支持16个并发更新线程,即这些线程可以在不导致锁的争用情况下进行并发更新。因此,ConcurrentHashMap可以支持比较高的并发性,并且其锁的开销一般比较小。ConcurrentHashMap中一个构造器支持concurrentcyLevel参数可以使我们调整ConcurrentHashMap支持的并发更新线程数。当然,既然这个值是可以调整的,那么这个值就不会是越大或者越小就越好。这个值越大表示相应的开销越大,越小标识它越可能导致并发更新时出现锁的争用。因此,concurrentcyLevel的值要调整也必须是根据实际需要来权衡。

CopyOnWriteArrayList是List接口的一个线程安全实现类,它相当于ArrayList的线程安全版。CopyOnWriteArrayList内部会维护一个实例变量array用于引用一个数组。该数组用于存储列表的各个元素。CopyOnWriteArrayList的更新操作是通过创建一个新的数组newArray,并把老的数组的内容复制到newArray,然后对newArray进行更新并将array引用指向newArray。因此,array所引用的数组相当于当前CopyOnWriteArrayList实例的一个快照,而对CopyOnWriteArrayList的更新操作所导致的对象的复制(主要是对象引用的复制)的开销相当于这个快照的间接开销。CopyOnWriteArrayList所使用的遍历方式就是快照。因此,CopyOnWriteArrayList适用于遍历操作远比更新操作频繁或者不希望在遍历的时候加锁的场景。而在其他场景下,我们可能仍然要考虑使用Collections.synchronizedList()。

CopyOnWriteArraySet是Set接口的一个线程安全实现类,它相当于HashSet的线程安全版。CopyOnWriteArraySet内部实现使用了一个CopyOnWriteArrayList实例,因此CopyOnWriteArraySet的试用场景与CopyOnWriteArrayList相似。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值