首页 > 编程语言 >Java并发编程实战读书笔记(二)

Java并发编程实战读书笔记(二)

时间:2024-07-24 12:59:58浏览次数:12  
标签:容器 队列 同步 Java 读书笔记 对象 编程 并发 线程

对象的组合

在设计线程安全的类时,确保数据的一致性和防止数据竞争是至关重要的。这通常涉及三个基本要素:确定构成对象状态的所有变量,明确约束这些状态变量的不变性条件,以及建立管理对象状态并发访问的策略。

要确定构成对象状态的所有变量相对简单,但需注意状态应封装在对象内,避免外部直接访问导致数据不一致的风险。例如,一个银行账户类可能将其余额作为其状态变量。

不变性条件则较为复杂,它定义了哪些状态转换是允许的。例如,银行账户的余额不能为负数。在没有充分了解对象的不变形条件与后验条件下,很难保证线程安全性。因此,需要借助原子性和封装性来达成这一点。

并发访问管理策略的选择多样,包括使用synchronized关键字、显式锁如ReentrantLock、原子类如AtomicLong或并发集合如ConcurrentHashMap等。每种策略都有其适用场景,选择哪一种取决于特定的需求和性能考量。

ConcurrentHashMap为例,它通过分段锁机制实现了高并发性和伸缩性,允许多个读取线程和一定量的写入线程并发修改Map,从而提高多线程环境下的性能。相比之下,HashTable则是通过在每个方法上使用synchronized关键字实现同步,但这导致了较低的并发性能。

在某些情况下,即便对象本身是线程安全的,如果将其发布出去也可能破坏封闭性。封闭类的状态使得分析类的线程安全性时无需检查整个程序,从而更易于构造线程安全的类。

数据封装在对象内部可以将数据的访问限制在对象的方法上,这样更容易确保线程在访问数据时总能持有正确的锁。例如,Vector的add方法通过synchronized关键字同步,保证了线程安全,但效率较低。通常建议使用Collections.synchronizedList(arrayList)将线程不安全的ArrayList转换为线程安全的List。

实例封闭

  1. 作用域限制与封闭实例
    • 对象可以封闭在某个作用域内,例如作为类的私有成员或局部变量。封闭在某个作用域内的对象更容易监控和分析,因为能够访问该对象的代码路径是已知的。
    • 例如,线程Local中的变量仅在当前线程中可见和可访问,这极大地减少了多线程之间的数据竞争。每个线程都可以独立地访问自己的副本而不会影响其他线程,从而确保了线程安全性。
  2. 私有锁与内置锁
    • 使用私有的锁对象而不是对象的内置锁或其他公有访问的锁,可以避免客户代码错误地参与到同步策略中。私有锁可以被封装在类内部,仅通过特定的方法暴露必要的同步行为。
    • 例如,使用ReentrantLock作为私有锁可以实现更细粒度的同步控制。在并发编程中,这种私有锁的使用可以有效地保护共享资源,并且更加灵活地设计同步策略,从而提升性能并减少死锁的风险。
  3. 线程局部变量与封闭实例
    • 在某些情况下,可以将对象封闭在线程内,即从一个方法传递到另一个方法中,而不是在多个线程之间共享。这样,对象仅在单个线程内使用,避免了并发访问带来的问题。
    • 例如,在线程中创建的局部对象,只在该线程的生命周期内使用,不会与其他线程共享。这种方法在高并发环境下尤其有用,因为它大大减少了线程间共享数据的可能性。
  4. 公有方法与同步策略
    • 通过提供公有方法来访问和修改封闭对象的状态,可以在这些方法中实现适当的同步策略。例如,可以通过加锁机制(如synchronized关键字或显式锁)来确保每次只有一个线程能够访问对象的某个状态。
    • 这种方法结合了封装和同步的优点,使得对象的状态在多线程环境中保持一致。同时,由于对象的封闭性,分析其线程安全性时只需检查有限的代码路径,无需考虑整个程序的复杂性。
  5. 并发容器与同步包装器
    • Java提供了多种并发容器,如ConcurrentHashMap和CopyOnWriteArrayList等,它们通过精细设计的锁机制和数据结构优化来提高并发性能。
    • 使用Collections.synchronizedList等方法可以将非线程安全的容器转变为线程安全的容器,通过封装来实现同步。这些同步包装器持有对底层容器的唯一引用,并通过同步方法保护所有对底层容器的访问,从而实现线程安全。
  6. 封闭机制与加锁策略
    • 封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。例如,在封闭的作用域内使用局部锁可以进一步限制对象的访问范围,从而降低数据竞争的风险。
    • 封闭机制使得不同的状态变量可以由不同的锁来保护,这样的灵活性有助于设计高效且线程安全的类。例如,分离锁(Separate Locks)模式允许不同的操作持有不同的锁,从而增加并发性。
  7. 发布控制与逸出检查
    • 封闭对象不应发布到其封闭作用域之外的环境中。对象的发布可能会导致封闭性的破坏,使得对象被外部代码以不受控的方式访问和修改。
    • 例如,从方法返回一个内部的私有对象的引用,或者在公共方法中暴露内部状态的详细信息,都可能破坏封闭性和线程安全性。因此,应严格控制对象的发布,确保不会逸出其封闭的作用域。

基础构建模块

同步容器类

同步容器类包括Vector和Hashtable,它们提供了线程安全的访问方式。然而,在使用这些容器类时,需要注意以下几点:

  1. 对于迭代器,如果在迭代过程中容器被其他线程修改,迭代器会抛出ConcurrentModificationException异常。这是因为迭代器在检查容器是否被修改时没有进行同步,可能会导致失效的计数值。这种设计是为了降低并发修改操作对程序性能的影响。

  2. 对于复合操作(如addAll、removeAll等),即使容器本身是同步的,这些操作也可能不是原子性的。这意味着在执行这些操作时,可能会有其他线程同时修改容器,导致不可预测的结果。为了避免这种情况,可以使用锁或其他同步机制来确保复合操作的原子性。

  3. 对于可变容器,如ArrayList和HashMap,虽然它们本身不是同步的,但可以通过使用Collections.synchronizedList或Collections.synchronizedMap方法将它们包装成同步容器。这样,在使用这些容器时,需要手动进行同步以确保线程安全。

并发容器

并发容器是Java 5.0引入的新型容器,它们为多线程环境提供了更好的性能和并发性。与同步容器不同,并发容器使用更细粒度的锁或无锁算法来提高并发访问的性能。下面详细介绍几种主要的并发容器:

ConcurrentHashMap

ConcurrentHashMap是Java提供的一个线程安全的哈希表,它通过分段锁(Lock Striping)技术提高了并发性能。在ConcurrentHashMap中,整个哈希表被分成多个段,每个段都有自己的锁。这样,不同段的更新操作可以并行进行,从而提升了整个哈希表的并发能力。

CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全的列表,它采用了“写入时复制”(Copy-On-Write)策略。这意味着每次对列表进行修改操作(如添加、删除元素)时,都会创建并重新发布一个新的列表副本,而不是在原有列表上进行修改。这种策略适合读操作远多于写操作的场景,因为读操作通常可以在不加锁的情况下进行。

ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链接节点的、线程安全的队列。它使用了无锁算法来实现并发控制,从而提高了并发性能。

BlockingQueue

BlockingQueue是一个扩展了Queue接口的阻塞队列,它支持可阻塞的插入和获取等操作。如果队列为空,获取元素的操作将一直阻塞,直到队列中有元素可用;如果队列已满(对于有界队列),插入元素的操作将一直阻塞,直到队列中有空间可用。

ConcurrentSkipListMap 和 ConcurrentSkipListSet

ConcurrentSkipListMap和ConcurrentSkipListSet分别是基于跳表实现的线程安全的映射和集合。它们提供了类似TreeMap和TreeSet的排序功能,但在并发性能上进行了优化。

Deque

Deque(双端队列)是一个可以在两端进行插入和删除操作的线性数据结构。在Java中,Deque接口继承自Queue接口,因此它具有Queue的所有功能,同时还提供了额外的方法来支持从两端的操作。

ArrayDeque是Deque接口的一个实现,它使用数组作为底层数据结构。由于数组支持随机访问,因此ArrayDeque可以在常数时间内完成头部和尾部的插入和删除操作。

LinkedBlockingDeque也是Deque接口的一个实现,但它是线程安全的。它使用链表作为底层数据结构,并通过锁和其他同步机制来实现线程安全。

以下是一些使用Deque和ArrayDeque的示例代码:

import java.util.ArrayDeque;
import java.util.Deque;

public class DequeExample {
    public static void main(String[] args) {
        // 创建一个ArrayDeque实例
        Deque<Integer> deque = new ArrayDeque<>();

        // 在队列头部插入元素
        deque.addFirst(1);
        deque.addFirst(2);
        deque.addFirst(3);

        // 在队列尾部插入元素
        deque.addLast(4);
        deque.addLast(5);

        // 打印队列内容
        System.out.println("Deque: " + deque);

        // 移除并返回队列头部的元素
        int firstElement = deque.removeFirst();
        System.out.println("Removed first element: " + firstElement);

        // 移除并返回队列尾部的元素
        int lastElement = deque.removeLast();
        System.out.println("Removed last element: " + lastElement);

        // 打印队列内容
        System.out.println("Deque after removals: " + deque);
    }
}

这段代码首先创建了一个ArrayDeque实例,然后在队列头部和尾部分别插入了一些元素。接着,它移除并打印了队列头部和尾部的元素。最后,它打印了移除元素后的队列内容。

CountDownLatch

闭锁(Latch)是一种同步工具类,用于控制线程的执行顺序。它允许一个或多个线程等待一组事件发生后再继续执行。闭锁的状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。当事件发生时,计数器递减,而await方法则等待计数器达到零,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。

CountDownLatch是闭锁的一种实现,它可以使一个或多个线程等待一组事件发生。例如,可以使用CountDownLatch来确保某个计算在其需要的所有资源都被初始化之后才继续执行;确保某个服务在其依赖的所有其他服务都已经启动之后才启动;等待直到某个操作的所有参与者都就绪再继续执行。

Semaphore

计数信号量(Counting Semaphore)是一种同步工具类,用于控制同时访问某个特定资源的操作数量或执行某个指定操作的数量。它通过管理一组虚拟许可来实现同步,许可的初始数量可通过构造函数指定。

在使用计数信号量时,线程可以首先尝试获得许可,如果还有剩余的许可,线程可以继续执行,并在使用完资源后释放许可。如果没有可用的许可,acquire方法将阻塞直到有可用许可,或者等待被中断或超时。release方法则用于将许可返回给信号量。

二值信号量是计数信号量的一种特殊形式,其初始值为1,可以用做互斥体(mutex),并具备不可重入的加锁语义。当一个线程拥有这个唯一的许可时,它就拥有了互斥锁。

计数信号量还可以用于实现资源池,如数据库连接池。在这种情况下,可以将Semaphore的计数值初始化为池的大小,并在从池中获取资源之前首先调用acquire方法获取许可。当资源使用完毕后,调用release方法释放许可。这样,acquire方法会一直阻塞直到资源池不为空。

另一种简单的实现阻塞对象池的方法是使用BlockingQueue来保存池中的资源。

CyclicBarrier

栅栏(Barrier)是一种特殊的同步工具,它能使一组线程在继续执行前等待直到所有线程都到达某个特定点。栅栏的关键特性是所有线程都必须同时到达栅栏位置才能继续执行,这与闭锁不同,闭锁用于等待某个事件发生。

CyclicBarrier是栅栏的一个实现,它允许一定数量的线程反复地在某个点汇集。这对于并行迭代算法特别有用,这种算法通常将一个问题分解成一系列独立的子问题。当线程到达栅栏位置时,它会调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。一旦所有线程都到达,栅栏将打开,所有线程将被释放,并且栅栏将被重置以便再次使用。

如果对await的调用超时或被中断,栅栏将被认为是“打破”的,此时所有阻塞的await调用都将终止并抛出BrokenBarrierException。如果成功通过栅栏,await将为每个线程返回一个唯一的到达索引号。这个索引可以用来选举一个领导线程,在下一次迭代中执行一些特殊任务。

CyclicBarrier还允许你传递一个栅栏操作给构造函数,这是一个Runnable,当成功通过栅栏时,它将在一个子任务线程中执行,但这必须在释放阻塞线程之前完成。

标签:容器,队列,同步,Java,读书笔记,对象,编程,并发,线程
From: https://blog.csdn.net/Luck_gun/article/details/140644665

相关文章

  • Java中的优先级队列(PriorityQueue)(如果想知道Java中有关优先级队列的知识点,那么只看这
        前言:优先级队列(PriorityQueue)是一种抽象数据类型,其中每个元素都关联有一个优先级,元素按照优先级顺序进行处理。✨✨✨这里是秋刀鱼不做梦的BLOG✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客先让我们看一下本文大致的讲解内容:目录1.优......
  • 基于Java+SpringBoot+Vue的卓越导师双选系统的设计与开发(源码+lw+部署文档+讲解等)
    文章目录前言项目背景介绍技术栈后端框架SpringBoot前端框架Vue数据库MySQL(MyStructuredQueryLanguage)具体实现截图详细视频演示系统测试系统测试目的系统功能测试系统测试结论代码参考数据库参考源码获取前言......
  • 模块2 面向对象编程初级 --- 第六章:创建对象
    第六章创建对象主要知识点:1、类的实例化2、构造方法3、对象的使用4、对象的清除学习目标:根据定义的类进行实例化,并且运用对象编写代码完成一定的功能。本章对类进行实例化,生成类的对象,利用对象开始软件的设计过程,掌握对象的使用方法。6.1创建对象概......
  • java内部类详解
    24-07-22java内部类目录24-07-22java内部类什么是内部类内部类的分类局部内部类匿名内部类AnonymousClass(重点)成员内部类静态内部类什么是内部类简单来说,一个类的内部嵌套了另一个类结构,被嵌套的结构被称为内部类,嵌套其他类的类我们称为外部类。在开始学习之前,我们先来回想......
  • Druid出现DruidDataSource - recyle error - recyle error java.lang.InterruptedExce
    原文链接: https://www.cnblogs.com/zhoading/p/14040939.htmlhttps://www.cnblogs.com/lingyejun/p/9064114.html 一、问题回顾线上的代码之前运行的都很平稳,突然就出现了一个很奇怪的问题,看错误信息是第三方框架Druid报出来了,连接池回收连接时出现的问题。1234......
  • Java面试题总结(持续更新)
    1、this关键字和super关键字的区别及联系this关键字用在本类中。在类的内部,可以在任何方法中使用this引用当前对象。this关键字是用来解决全局变量和局部变量之间的冲突。this()可以调用同类中重载的构造方法,并且需要放在第一行。super关键字用在子类中。在子类中可以通......
  • JavaScript中的new map()和new set()使用详细(new map()和new set()的区别)
    简介:newMap():在JavaScript中,newMap()用于创建一个新的Map对象。Map对象是一种键值对的集合,其中的键是唯一的,值可以重复。newSet():在JavaScript中,newSet()是用来创建一个新的Set对象的语法。Set对象是一种集合,其中的值是唯一的,没有重复的值。newSet()可以用......
  • Aspose项目实战!pdf、cells for java
    Aspose实战使用:Excel与PDF转换工具类在这篇博客中,我将分享如何使用Aspose库来实现Excel文件与PDF文件之间的转换。我会重点分析一个工具类AsposeOfficeUtil,这个类封装了多个与Excel和PDF相关的操作方法,帮助开发者高效地进行文件转换和数据处理。此外,还将提......
  • python_网络编程_socket
    一、网络编程的基本概念通信协议:internet协议,任何私有网络支持此协议,就可以接入互联网二、七层协议与四层协议从下到上分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层三、掌握TCP、IP协议ip协议是整个TCP、IP协议族的核心IP地址就是会联网上计算......
  • 某人有100,0000元,每经过一次路口,需要交费,规则如下: 1)当现金>50000时,每次交5% 2)当现
    1publicclassexercise08{2//编写一个main方法3publicstaticvoidmain(){4/*5某人有100,0000元,每经过一次路口,需要交费,规则如下:61)当现金>50000时,每次交5%72)当现金<=50000时,每次交100008编程计算该人可......