【Java 多线程 8】同步容器与并发容器

  时间:2022-06-20 13:23:31  阅读量:51  评论数:0  作者:

学习编程,分析源码很重要,【Java 多线程 8】同步容器与并发容器这篇文章通过代码举例讲述得比较清晰,想了解的可以参考学习一下。

在这里插入图片描述

一、为什么这种方式不能实现线程安全性?

分析一段代码:

package com.guor.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ListHelper<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    public synchronized boolean putIfAbsent(E x){
        boolean absent = !list.contains(x);
        if(absent){
            list.add(x);
        }
        return absent;
    }
}

毕竟putIfAbsent已经声明了synchronized 类型的变量,对不对?问题在于在错误的锁上进行了同步。无论list使用哪一个锁来保护它的状态,可以确定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,这意味着putIfAbsent相对于List的其它操作来说并不是原子的,因此就无法确保当putIfAbsent执行时另一个线程不会修改链表。
要想使这个方法能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护器状态的锁来保护这段客户端代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。
在Vector和同步封装器的文档中指出,它们通过使用Vector或封装器的内置锁来支持客户端加锁,下面代码可以实现线程安全的list操作。

public boolean putIfAbent(E x){
    synchronized (list){
        boolean absent = !list.contains(x);
        if(absent){
            list.add(x);
        }
        return absent;
    }
}

二、组合

当为现有的类添加一个原子操作时,有一种更好的方法:组合。
下面代码中ImprovedList通过将List对象的操作委托给底层的List实例来实现List的操作,同时还添加了一个原子的putIfAbsent方法。(与Collections.synchronizedList和其它容器封装器一样,ImprovedList假设把某个链表对象传给构造函数以后,客户代码不会再直接使用这个对象,而只能通过ImprovedList来访问它。)

package com.guor.util;

import java.util.List;

public class ImprovedList<T> implements List<T> {

    private final List<T> list;

    public ImprovedList(List<T> list){
        this.list = list;
    }

    public synchronized boolean putIfAbsent(T x){
        boolean absent = !list.contains(x);
        if(absent){
            list.add(x);
        }
        return absent;
    }

    public synchronized void clear(){
        list.clear();
    }
    
    ...
}

ImprovedList通过自身的内置锁增加了一层额外的加锁,它并不关心底层的list是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一致的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。

三、同步容器类

同步容器类包括Vector和Hashtable,这些实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。
同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护符合操作。容器上常见的复合操作包括:迭代、跳转以及条件运算,例如“若没有则添加”。在同步容器类中,这些符合操作在没有客户端加锁的情况下仍然是线程安全的,但当其他线程并发地修改容器时,它们可能会表现出意料之外的行为。

四、隐藏迭代器

虽然加锁可以防止迭代器抛出ConcurrentModificationException,但你必须要记住所有对共享容器进行迭代的地方都需要加锁。实际情况要更加复杂,因为在某些情况下,迭代器会隐藏起来。比如log.info("set content is :"+set),编译器将字符串的连接操作转换为调用StringBuilder.append(Object),而这个方法又会调用容器的toString方法,标准容器的toString方法将迭代容器,并在每个元素上调用toString来生成容器内容的格式化表示。
容器的hashCode和equals等方法也会间接地执行迭代操作,当容器作为另一个容器的元素和键值时,就会出现这种情况。

五、并发容器

jdk1.5提供了多种并发容器来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。
另一方面,并发容器是针对多个线程并发访问设计的。在jdk1.5中增加了ConcurrentHashMap,用来替代同步且基于散列的Map以及CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步的List。在新的ConcurrentMap接口中增加了对一些常见复合操作的支持,例如“若没有则添加”、替换以及有条件删除等。
通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。
jdk1.5增加了两种新的容器类型:Queue和BlockingQueue。Queue用来临时保存一组等待处理的元素。它提供了几种实现,包括ConcurrentLinkedQueue,这是一个传统的先进先出队列,以及PriorityQueue,这是一个非并发的优先队列。Queue上的操作不会阻塞,如果队列为空,那么获取元素的操作将返回空值。虽然可以用List来模拟Queue的行为,事实上,正是通过LinkedList来实现Queue的,但还需要一个Queue的类,因为它能去掉List的随机访问需求,从而实现更高效的并发。
BlockingQueue扩展了Queue,增加了可阻塞的插入和获取等操作,如果队列为空,那么获取元素的操作将一直阻塞,直到队列中出现一个可用的元素。如果队列已满,那么插入元素的操作将一直阻塞,直到队列中出现可用的空间。
同步容器在执行每个操作期间都持有一个锁。

六、ConcurrentHashMap

与HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种更细的加锁机制来实现更大程度的共享,这种机制成为分段锁。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。ConcurrentHashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。
ConcurrentHashMap返回的迭代器具有弱一致性,而并非“及时失败”。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。

七、享誉全球的 Java 经典著作《Java核心技术》基于Java 17全面升级!

Java 诞生 27 年来,这本享誉全球的 Java 经典著作《Core Java》一路伴随着 Java 的成长,得到了百万 Java 开发者的青睐,几乎出现在每个“学Java要看什么书”类似的书单里,影响了几代技术人。

27年间,每当 Java 有新的 LTR 版本发布,这本书都会随之更新,这次也不例外。现在,针对 Java 17 新特性的《Java核心技术》第 12 版*中文版(卷1)终于上市了!

《Java核心技术》第 12 版涵盖了 Java 17 的最新特性,相应调整了部分内容结构,同时延续之前版本的优良传统,利用清晰明了的示例加以解释,并提供了全部示例代码,以便读者学习和灵活应用。它将续写从前的辉煌,使开发者能及时跟上 Java 前进的步伐。

在这里插入图片描述
我们寻找了50位曾经看着《Java核心技术》这本书成长起来的KOL推荐本书!大家的寄语全部收录在第12版新书中!
这本书究竟是怎样的一本书,得到众多开发者的一致推荐呢?
1、《Java核心技术》并非市面那些零基础速成的书,很好地避免了开发基础书容易犯的“大而泛”的问题,尽管内容繁多,但对知识点的介绍并非泛泛而过。通过周密组织,从Java繁杂的内容中整理出一条清晰的主线,构成一个完整的知识体系。
2、整本书不仅让你深入了解设计和实现Java应用涉及的所有基础知识和Java特性,还会帮助你掌握开发Java程序所需的全部基本技能。
3、作者凯.霍斯特曼是Java技术坚定的倡导者,至今仍常年在国际上的各类计算机峰会上进行技术分享。在位于硅谷中心的圣何塞州立大学教学30余年,为硅谷的顶尖科技公司培养了大量计算机专业人才。非常熟知大厂要什么!所以他的书也是非常有针对性。
4、为帮助大家更轻松地学习Java,作者还亲自录制了配套视频讲解课程,视频配有中文配音+中文字幕,与纸书涵盖内容基本一致,适配Java SE8以后的版本。
纸书+视频搭配学习,学习Java更轻松。(B站搜索“Java核心技术站”直达)。

相信在学习Java的道路上有了《Java核心技术》这本书的辅助,你一定可以做到事半功倍。

八、Java核心技术大会2022

视频号【华章计算机】免费观看所有直播,8个专场、24个主题、近30位国内外顶级专家深度分享与探讨Java的变革与未来。

在这里插入图片描述

上一篇:【Java 多线程 7】通过socket、多线程、动态代理、反射 实现RPC远程方法调用

下一篇:Java学习路线总结,搬砖工逆袭Java架构师

关键词:java,多线程,同步,容器,并发