• [技术干货] Hashmap和ConcurrentHashmap的区别
    HashTable (1)底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化 (2)初始size为11,扩容:newsize = olesize*2+1 (3)计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length  HashMap (1)底层数组+链表实现,可以存储null键和null值,线程不安全 (2)初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂 (3)扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入 (4)插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容) (5)当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀 (6)计算index方法:index = hash & (tab.length – 1) HashMap的初始值还要考虑加载因子:  (7)哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。 (8)加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。 (9)空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。  HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:  (1)容量(capacity):hash表中桶的数量 (2)初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量 (3)尺寸(size):当前hash表中记录的数量 (4)负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)  除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。  HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。  “负载极限”的默认值(0.75)是时间和空间成本上的一种折中:  (1)较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操(HashMap的get()与put()方法都要用到查询) (2)较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销  程序猿可以根据实际情况来调整“负载极限”值。  ConcurrentHashMap (1)底层采用分段的数组+链表实现,线程安全 (2)通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。) (3)Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术 (4)有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁 (5)扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容  Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。  HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为树形结构。  在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null。  Hashtable是线程安全的,它的方法是同步的,可以直接用在多线程环境中。而HashMap则不是线程安全的,在多线程环境中,需要手动实现同步机制。  Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。  先看一下简单的类图:  从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。  ConcurrentHashMap是使用了锁分段技术来保证线程安全的。  锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。  ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。  ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。  HashMap和ConcurrentHashMap的区别总结 (1)HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。  (2)ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。  (3)ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。  原文链接:https://blog.csdn.net/g_ood_good_study/article/details/120438768 
  • [技术干货] HashMap 和 ConcurrentHashMap 的区别
    一、线程安全性 1. HashMap:非线程安全 非线程安全:HashMap 在设计时并没有考虑线程安全问题,因此在多线程环境下同时对 HashMap 进行读写操作可能会导致数据不一致、丢失或抛出 ConcurrentModificationException 异常。 需要外部同步:在多线程环境中使用 HashMap 时,需要使用外部同步来保护对 HashMap 的访问,例如使用 synchronized 块或通过 Collections.synchronizedMap(new HashMap<>()) 来创建线程安全的 Map。 Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>()); 1 AI助手 2. ConcurrentHashMap:线程安全 线程安全:ConcurrentHashMap 是为并发环境设计的,支持多个线程同时进行读写操作而不会出现数据不一致的问题。它通过细粒度的锁机制实现线程安全,而不是像 Hashtable 那样对整个表进行锁定。 锁分段机制:ConcurrentHashMap 使用了一种分段锁定机制,即将整个哈希表分为多个段(Segment),每个段独立进行锁定操作,从而允许多个线程并发访问不同的段,极大地提高了并发性能。 二、锁机制与并发性能 1. HashMap:无锁 无锁机制:HashMap 没有内置锁机制,因此在多线程情况下,必须通过外部同步来确保线程安全。然而,外部同步通常需要对整个表进行锁定,这样会显著降低并发性能,特别是在写操作频繁的情况下。 2. ConcurrentHashMap:分段锁与 CAS 操作 分段锁(Segment Locking):ConcurrentHashMap 最初的实现中采用了分段锁,每个段相当于一个小的哈希表,多个线程可以同时访问不同的段,而不会发生冲突。Java 8 之后,ConcurrentHashMap 引入了更细粒度的锁(使用 CAS(Compare-And-Swap) 操作和 synchronized 块结合),进一步提高了并发性能。 锁的粒度:由于锁的粒度更细,ConcurrentHashMap 能够在高并发环境下表现出更好的性能,特别是在读操作多于写操作的场景中。 三、结构与实现 1. HashMap:哈希表 + 链表/红黑树 基本结构:HashMap 的内部结构是一个哈希表,使用链表法处理哈希冲突。在 Java 8 中,当链表长度超过一定阈值(默认8)时,链表会转换为红黑树,以提高性能。 扩容机制:HashMap 在插入元素时,如果元素数量超过 容量 * 负载因子,就会触发扩容(通常是容量加倍),并重新计算所有键的哈希值,分配到新的数组中。 2. ConcurrentHashMap:哈希表 + Node + CAS 分段锁(Java 7 之前):在 Java 7 及之前的版本中,ConcurrentHashMap 使用了分段锁,每个段(Segment)内部使用类似 HashMap 的结构。每个段独立维护哈希表,并且各个段之间是互相独立的,因此多个线程可以同时访问不同的段。 Node + CAS 操作(Java 8 之后):从 Java 8 开始,ConcurrentHashMap 放弃了分段锁的实现,改用更细粒度的锁和 CAS 操作。ConcurrentHashMap 内部使用 Node 类来存储键值对,并且通过 CAS 操作确保在并发写操作时的线程安全性。 扩容机制:ConcurrentHashMap 也支持动态扩容,但它的扩容过程是并发的,即多个线程可以同时进行扩容操作,而不会阻塞其他线程的读写操作。 四、Null 键和值的处理 1. HashMap:允许 null 键和值 允许一个 null 键:HashMap 允许一个 null 键和多个 null 值,这使得 HashMap 在某些情况下更为灵活。例如,可以使用 null 键表示缺少值的情况。 特殊处理 null:HashMap 内部对 null 键进行了特殊处理,存储在哈希表的第一个桶(index 为 0 )。 2. ConcurrentHashMap:不允许 null 键和值 不允许 null 键和值:ConcurrentHashMap 不允许 null 键和 null 值。如果尝试插入 null 键或值,会抛出 NullPointerException。 设计原因:这是出于并发环境下的设计考虑,因为 null 键或值可能会导致一些歧义或不可预期的行为,从而影响线程安全性。 五、迭代器的Fail-Fast机制 1. HashMap:Fail-Fast迭代器 Fail-Fast机制:HashMap 的迭代器是 Fail-Fast 的,这意味着如果在迭代过程中检测到集合被修改(除了通过迭代器自身的 remove() 方法),迭代器会抛出 ConcurrentModificationException 异常。 迭代中的修改:这种机制的实现依赖于 modCount 字段,HashMap 通过 modCount 来跟踪结构修改的次数。如果 modCount 在迭代过程中发生变化,意味着集合被并发修改,迭代器将无法继续安全地迭代。 2. ConcurrentHashMap:弱一致性迭代器 弱一致性:ConcurrentHashMap 的迭代器是弱一致性的,它不会抛出 ConcurrentModificationException。即使在迭代过程中集合被修改,ConcurrentHashMap 也能保证迭代器不会抛出异常,并且可以看到最新的元素。 并发修改下的安全性:ConcurrentHashMap 的弱一致性意味着迭代器在访问元素时可以看到部分修改,但不会抛出异常。这种设计在高并发环境下非常实用,因为它允许并发读写操作而不阻塞迭代过程。 六、使用场景 1. HashMap:适用于单线程或读多写少的场景 单线程环境:HashMap 最适合在单线程环境中使用,或者在无需担心并发修改的场景中使用。 读多写少的场景:在读操作明显多于写操作的场景中,HashMap 的性能优势更加明显。 2. ConcurrentHashMap:适用于多线程并发场景 高并发场景:ConcurrentHashMap 专为高并发环境设计,适用于需要频繁读写操作的多线程场景,例如缓存、计数器、会话管理等。 部分一致性要求:当需要在迭代过程中允许并发修改时,ConcurrentHashMap 提供了比 HashMap 更灵活和安全的处理方式。 七、扩展:ConcurrentHashMap 的性能优化 1. 分段锁与并发控制 Java 7 及之前:分段锁机制通过将哈希表划分为多个段来减少锁争用,每个段独立加锁,允许多个线程同时访问不同的段,从而提高并发性能。 Java 8 及之后:放弃分段锁机制,采用更细粒度的锁和 CAS 操作(如在树化和扩容时),提高了并发写操作的性能。 2. 扩容与 rehash 并发扩容:ConcurrentHashMap 支持并发扩容,这意味着多个线程可以同时进行 rehash 操作,而不会阻塞其他线程的读写操作。这种设计确保了高并发环境下的扩展性。 八、总结 HashMap 和 ConcurrentHashMap 是 Java 中两种常用的哈希表实现,但它们在设计  目标、线程安全性、性能优化等方面有显著差异:  线程安全性:HashMap 是非线程安全的,适合单线程环境;ConcurrentHashMap 是线程安全的,适合多线程并发场景。 锁机制:HashMap 没有内置锁机制,需要外部同步;ConcurrentHashMap 使用分段锁和 CAS 操作,优化了并发性能。 Null 键和值:HashMap 允许 null 键和值;ConcurrentHashMap 不允许 null 键和值。 迭代器机制:HashMap 的迭代器是 Fail-Fast 的,而 ConcurrentHashMap 的迭代器是弱一致性的,允许并发修改。 使用场景:HashMap 适用于单线程或读多写少的场景;ConcurrentHashMap 适用于高并发、多线程的环境。 ————————————————  原文链接:https://blog.csdn.net/Flying_Fish_roe/article/details/143200317 
  • [技术干货] 彻头彻尾理解 ConcurrentHashMap
    ConcurrentHashMap是J.U.C(java.util.concurrent包)的重要成员,它是HashMap的一个线程安全的、支持高效并发的版本。在默认理想状态下,ConcurrentHashMap可以支持16个线程执行并发写操作及任意数量线程的读操作。本文将结合Java内存模型,分析JDK源代码,探索ConcurrentHashMap高并发的具体实现机制,包括其在JDK中的定义和结构、并发存取、重哈希和跨段操作,并着重剖析了ConcurrentHashMap读操作不需要加锁和分段锁机制的内在奥秘和原理。   本文所有关于 ConcurrentHashMap 的源码都是基于 JDK 1.6 的,不同 JDK 版本之间会有些许差异,但不影响我们对 ConcurrentHashMap 的数据结构、原理等整体的把握和了解。   由于 ConcurrentHashMap 的源代码实现依赖于Java内存模型,所以阅读本文需要读者了解Java内存模型与Volatile语义,具体详见《Java 并发:volatile 关键字解析》一文。同时,ConcurrentHashMap的源代码会涉及到散列算法和链表数据结构,所以,读者需要对散列算法和基于链表的数据结构有所了解,特别是对HashMap的进一步了解和回顾。关于HashMap的详细介绍,请移步我的博文《Map 综述(一):彻头彻尾理解 HashMap》。 一. ConcurrentHashMap 概述   笔者曾在《Map 综述(一):彻头彻尾理解 HashMap》一文中提到,HashMap 是 Java Collection Framework 的重要成员,也是Map族(如下图所示)中我们最为常用的一种。不过遗憾的是,HashMap不是线程安全的。也就是说,在多线程环境下,操作HashMap会导致各种各样的线程安全问题,比如在HashMap扩容重哈希时出现的死循环问题,脏读问题等。HashMap的这一缺点往往会造成诸多不便,虽然在并发场景下HashTable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个线程安全的高效版本 —— ConcurrentHashMap。在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。   如下图所示,ConcurrentHashMap本质上是一个Segment数组,而一个Segment实例又包含若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。总的来说,ConcurrentHashMap的高效并发机制是通过以下三方面来保证的(具体细节见后文阐述): 通过锁分段技术保证并发环境下的写操作; 通过 HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作;  通过不加锁和加锁两种方案控制跨段操作的的安全性。 二. HashMap 线程不安全的典型表现   我们先回顾一下HashMap。HashMap是一个数组链表,当一个key/Value对被加入时,首先会通过Hash算法定位出这个键值对要被放入的桶,然后就把它插到相应桶中。如果这个桶中已经有元素了,那么发生了碰撞,这样会在这个桶中形成一个链表。一般来说,当有数据要插入HashMap时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大HashMap的尺寸,但是这样一来,就需要对整个HashMap里的节点进行重哈希操作。关于HashMap的重哈希操作本文不再详述,读者可以参考《Map 综述(一):彻头彻尾理解 HashMap》一文。在此,笔者借助陈皓的《疫苗:JAVA HASHMAP的死循环》一文说明HashMap线程不安全的典型表现 —— 死循环。    HashMap重哈希的关键源码如下:   /**      * Transfers all entries from current table to newTable.      */     void transfer(Entry[] newTable) {          // 将原数组 table 赋给数组 src         Entry[] src = table;         int newCapacity = newTable.length;          // 将数组 src 中的每条链重新添加到 newTable 中         for (int j = 0; j < src.length; j++) {             Entry<K,V> e = src[j];             if (e != null) {                 src[j] = null;   // src 回收                  // 将每条链的每个元素依次添加到 newTable 中相应的桶中                 do {                     Entry<K,V> next = e.next;                      // e.hash指的是 hash(key.hashCode())的返回值;                     // 计算在newTable中的位置,注意原来在同一条子链上的元素可能被分配到不同的桶中                     int i = indexFor(e.hash, newCapacity);                        e.next = newTable[i];                     newTable[i] = e;                     e = next;                 } while (e != null);             }         }     } 1、单线程环境下的重哈希过程演示   单线程情况下,rehash 不会出现任何问题,如上图所示。假设hash算法就是最简单的 key mod table.length(也就是桶的个数)。最上面的是old hash表,其中的Hash表桶的个数为2, 所以对于 key = 3、7、5 的键值对在 mod 2以后都冲突在table[1]这里了。接下来的三个步骤是,Hash表resize成4,然后对所有的键值对重哈希的过程。  2、多线程环境下的重哈希过程演示   假设我们有两个线程,我用红色和浅蓝色标注了一下,被这两个线程共享的资源正是要被重哈希的原来1号桶中的Entry链。我们再回头看一下我们的transfer代码中的这个细节:  do {     Entry<K,V> next = e.next;       // <--假设线程一执行到这里就被调度挂起了     int i = indexFor(e.hash, newCapacity);     e.next = newTable[i];     newTable[i] = e;     e = next; } while (e != null);   而我们的线程二执行完成了,于是我们有下面的这个样子:   注意,在Thread2重哈希后,Thread1的指针e和指针next分别指向了Thread2重组后的链表(e指向了key(3),而next指向了key(7))。此时,Thread1被调度回来执行:Thread1先是执行 newTalbe[i] = e;然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3),如下图所示:   这时,一切安好。Thread1有条不紊的工作着:把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移,如下图所示:   在此时,特别需要注意的是,当执行e.next = newTable[i]后,会导致 key(3).next 指向了 key(7),而此时的key(7).next 已经指向了key(3),环形链表就这样出现了,如下图所示。于是,当我们的Thread1调用HashMap.get(11)时,悲剧就出现了 —— Infinite Loop。   这是HashMap在并发环境下使用中最为典型的一个问题,就是在HashMap进行扩容重哈希时导致Entry链形成环。一旦Entry链中有环,势必会导致在同一个桶中进行插入、查询、删除等操作时陷入死循环。  三. ConcurrentHashMap 在 JDK 中的定义   为了更好的理解 ConcurrentHashMap 高并发的具体实现,我们先来了解它在JDK中的定义。ConcurrentHashMap类中包含两个静态内部类 HashEntry 和 Segment,其中 HashEntry 用来封装具体的K/V对,是个典型的四元组;Segment 用来充当锁的角色,每个 Segment 对象守护整个ConcurrentHashMap的若干个桶 (可以把Segment看作是一个小型的哈希表),其中每个桶是由若干个 HashEntry 对象链接起来的链表。总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数组,而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。特别地,ConcurrentHashMap 在默认并发级别下会创建16个Segment对象的数组,如果键能均匀散列,每个 Segment 大约守护整个散列表中桶总数的 1/16。  1、类结构定义    ConcurrentHashMap 继承了AbstractMap并实现了ConcurrentMap接口,其在JDK中的定义为:  public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>         implements ConcurrentMap<K, V>, Serializable {      ... } 四. ConcurrentHashMap 的构造函数   ConcurrentHashMap 一共提供了五个构造函数,其中默认无参的构造函数和参数为Map的构造函数 为 Java Collection Framework 规范的推荐实现,其余三个构造函数则是 ConcurrentHashMap 专门提供的。  1、ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)    该构造函数意在构造一个具有指定容量、指定负载因子和指定段数目/并发级别(若不是2的幂次方,则会调整为2的幂次方)的空ConcurrentHashMap,其相关源码如下:      /**      * Creates a new, empty map with the specified initial      * capacity, load factor and concurrency level.      *      * @param initialCapacity the initial capacity. The implementation      * performs internal sizing to accommodate this many elements.      * @param loadFactor  the load factor threshold, used to control resizing.      * Resizing may be performed when the average number of elements per      * bin exceeds this threshold.      * @param concurrencyLevel the estimated number of concurrently      * updating threads. The implementation performs internal sizing      * to try to accommodate this many threads.      * @throws IllegalArgumentException if the initial capacity is      * negative or the load factor or concurrencyLevel are      * nonpositive.      */     public ConcurrentHashMap(int initialCapacity,                              float loadFactor, int concurrencyLevel) {         if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)             throw new IllegalArgumentException();          if (concurrencyLevel > MAX_SEGMENTS)                           concurrencyLevel = MAX_SEGMENTS;          // Find power-of-two sizes best matching arguments         int sshift = 0;            // 大小为 lg(ssize)          int ssize = 1;            // 段的数目,segments数组的大小(2的幂次方)         while (ssize < concurrencyLevel) {             ++sshift;             ssize <<= 1;         }         segmentShift = 32 - sshift;      // 用于定位段         segmentMask = ssize - 1;      // 用于定位段         this.segments = Segment.newArray(ssize);   // 创建segments数组          if (initialCapacity > MAXIMUM_CAPACITY)             initialCapacity = MAXIMUM_CAPACITY;         int c = initialCapacity / ssize;    // 总的桶数/总的段数         if (c * ssize < initialCapacity)             ++c;         int cap = 1;     // 每个段所拥有的桶的数目(2的幂次方)         while (cap < c)             cap <<= 1;          for (int i = 0; i < this.segments.length; ++i)      // 初始化segments数组             this.segments[i] = new Segment<K,V>(cap, loadFactor);     }   从源码中首先可以知道,ConcurrentHashMap对Segment的put操作是加锁完成的。在第二节我们已经知道,Segment是ReentrantLock的子类,因此Segment本身就是一种可重入的Lock,所以我们可以直接调用其继承而来的lock()方法和unlock()方法对代码进行上锁/解锁。需要注意的是,这里的加锁操作是针对某个具体的Segment,锁定的也是该Segment而不是整个ConcurrentHashMap。因为插入键/值对操作只是在这个Segment包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。因此,其他写线程对另外15个Segment的加锁并不会因为当前线程对这个Segment的加锁而阻塞。故而 相比较于 HashTable 和由同步包装器包装的HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。    在将Key/Value对插入到Segment之前,首先会检查本次插入会不会导致Segment中元素的数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作,然后再进行插入。重哈希操作暂且不表,稍后详述。第8和第9行的操作就是定位到段中特定的桶并确定链表头部的位置。第12行的while循环用于检查该桶中是否存在相同key的结点,如果存在,就直接更新value值;如果没有找到,则进入21行生成一个新的HashEntry并且把它链到该桶中链表的表头,然后再更新count的值(由于count是volatile变量,所以count值的更新一定要放在最后一步)。   到此为止,除了重哈希操作,ConcurrentHashMap的put操作已经介绍完了。此外,在ConcurrentHashMap中,修改操作还包括putAll()和replace()。其中,putAll()操作就是多次调用put方法,而replace()操作实现要比put()操作简单得多,此不赘述。  2、ConcurrentHashMap 的重哈希操作 : rehash()    上面叙述到,在ConcurrentHashMap中使用put操作插入Key/Value对之前,首先会检查本次插入会不会导致Segment中节点数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作。特别需要注意的是,ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希,因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同。针对段进行rehash()操作的源码如下:       void rehash() {             HashEntry<K,V>[] oldTable = table;    // 扩容前的table             int oldCapacity = oldTable.length;             if (oldCapacity >= MAXIMUM_CAPACITY)   // 已经扩到最大容量,直接返回                 return;              /*              * Reclassify nodes in each list to new Map.  Because we are              * using power-of-two expansion, the elements from each bin              * must either stay at same index, or move with a power of two              * offset. We eliminate unnecessary node creation by catching              * cases where old nodes can be reused because their next              * fields won't change. Statistically, at the default              * threshold, only about one-sixth of them need cloning when              * a table doubles. The nodes they replace will be garbage              * collectable as soon as they are no longer referenced by any              * reader thread that may be in the midst of traversing table              * right now.              */              // 新创建一个table,其容量是原来的2倍             HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1);                threshold = (int)(newTable.length * loadFactor);   // 新的阈值             int sizeMask = newTable.length - 1;     // 用于定位桶             for (int i = 0; i < oldCapacity ; i++) {                 // We need to guarantee that any existing reads of old Map can                 //  proceed. So we cannot yet null out each bin.                 HashEntry<K,V> e = oldTable[i];  // 依次指向旧table中的每个桶的链表表头                  if (e != null) {    // 旧table的该桶中链表不为空                     HashEntry<K,V> next = e.next;                     int idx = e.hash & sizeMask;   // 重哈希已定位到新桶                     if (next == null)    //  旧table的该桶中只有一个节点                         newTable[idx] = e;                     else {                             // Reuse trailing consecutive sequence at same slot                         HashEntry<K,V> lastRun = e;                         int lastIdx = idx;                         for (HashEntry<K,V> last = next;                              last != null;                              last = last.next) {                             int k = last.hash & sizeMask;                             // 寻找k值相同的子链,该子链尾节点与父链的尾节点必须是同一个                             if (k != lastIdx) {                                 lastIdx = k;                                 lastRun = last;                             }                         }                          // JDK直接将子链lastRun放到newTable[lastIdx]桶中                         newTable[lastIdx] = lastRun;                          // 对该子链之前的结点,JDK会挨个遍历并把它们复制到新桶中                         for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {                             int k = p.hash & sizeMask;                             HashEntry<K,V> n = newTable[k];                             newTable[k] = new HashEntry<K,V>(p.key, p.hash,                                                              n, p.value);                         }                     }                 }             }             table = newTable;   // 扩容完成         }   其实JDK官方的注释已经解释的很清楚了。由于扩容是按照2的幂次方进行的,所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。根据本文前面对HashEntry的介绍,我们知道链接指针next是final的,因此看起来我们好像只能把该桶的HashEntry链中的每个节点复制到新的桶中(这意味着我们要重新创建每个节点),但事实上JDK对其做了一定的优化。因为在理论上原桶里的HashEntry链可能存在一条子链,这条子链上的节点都会被重哈希到同一个新的桶中,这样我们只要拿到该子链的头结点就可以直接把该子链放到新的桶中,从而避免了一些节点不必要的创建,提升了一定的效率。因此,JDK为了提高效率,它会首先去查找这样的一个子链,而且这个子链的尾节点必须与原hash链的尾节点是同一个,那么就只需要把这个子链的头结点放到新的桶中,其后面跟的一串子节点自然也就连接上了。对于这个子链头结点之前的结点,JDK会挨个遍历并把它们复制到新桶的链头(只能在表头插入元素)中。特别地,我们注意这段代码:  for (HashEntry<K,V> last = next;      last != null;      last = last.next) {     int k = last.hash & sizeMask;     if (k != lastIdx) {         lastIdx = k;         lastRun = last;     } } newTable[lastIdx] = lastRun;   在该代码段中,JDK直接将子链lastRun放到newTable[lastIdx]桶中,难道这个操作不会覆盖掉newTable[lastIdx]桶中原有的元素么?事实上,这种情形时不可能出现的,因为桶newTable[lastIdx]在子链添加进去之前压根就不会有节点存在,这还是因为table的大小是按照2的幂次方的方式去扩展的。假设原来table的大小是2^k大小,那么现在新table的大小是2^(k+1)大小,而定位桶的方式是:  // sizeMask = newTable.length - 1,即 sizeMask = 11...1,共k+1个1。 int idx = e.hash & sizeMask;   因此这样得到的idx实际上就是key的hash值的低k+1位的值,而原table的sizeMask也全是1的二进制,不过总共是k位,那么原table的idx就是key的hash值的低k位的值。所以,如果元素的hashcode的第k+1位是0,那么元素在新桶的序号就是和原桶的序号是相等的;如果第k+1位的值是1,那么元素在新桶的序号就是原桶的序号加上2^k。因此,JDK直接将子链lastRun放到newTable[lastIdx]桶中就没问题了,因为newTable中新序号处此时肯定是空的。  3、ConcurrentHashMap 的读取实现 :get(Object key)   与put操作类似,当我们从ConcurrentHashMap中查询一个指定Key的键值对时,首先会定位其应该存在的段,然后查询请求委托给这个段进行处理,源码如下: /**      * Returns the value to which the specified key is mapped,      * or {@code null} if this map contains no mapping for the key.      *      * <p>More formally, if this map contains a mapping from a key      * {@code k} to a value {@code v} such that {@code key.equals(k)},      * then this method returns {@code v}; otherwise it returns      * {@code null}.  (There can be at most one such mapping.)      *      * @throws NullPointerException if the specified key is null      */     public V get(Object key) {         int hash = hash(key.hashCode());         return segmentFor(hash).get(key, hash);     }   我们紧接着研读Segment中get操作的源码:      V get(Object key, int hash) {             if (count != 0) {            // read-volatile,首先读 count 变量                 HashEntry<K,V> e = getFirst(hash);   // 获取桶中链表头结点                 while (e != null) {                     if (e.hash == hash && key.equals(e.key)) {    // 查找链中是否存在指定Key的键值对                         V v = e.value;                         if (v != null)  // 如果读到value域不为 null,直接返回                             return v;                            // 如果读到value域为null,说明发生了重排序,加锁后重新读取                         return readValueUnderLock(e); // recheck                     }                     e = e.next;                 }             }             return null;  // 如果不存在,直接返回null         }   了解了ConcurrentHashMap的put操作后,上述源码就很好理解了。但是有一个情况需要特别注意,就是链中存在指定Key的键值对并且其对应的Value值为null的情况。在剖析ConcurrentHashMap的put操作时,我们就知道ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。但是,此处怎么会存在键值对存在且的Value值为null的情形呢?JDK官方给出的解释是,这种情形发生的场景是:初始化HashEntry时发生的指令重排序导致的,也就是在HashEntry初始化完成之前便返回了它的引用。这时,JDK给出的解决之道就是加锁重读,源码如下:   /**          * Reads value field of an entry under lock. Called if value          * field ever appears to be null. This is possible only if a          * compiler happens to reorder a HashEntry initialization with          * its table assignment, which is legal under memory model          * but is not known to ever occur.          */         V readValueUnderLock(HashEntry<K,V> e) {             lock();             try {                 return e.value;             } finally {                 unlock();             }         } 4、ConcurrentHashMap 存取小结    在ConcurrentHashMap进行存取时,首先会定位到具体的段,然后通过对具体段的存取来完成对整个ConcurrentHashMap的存取。特别地,无论是ConcurrentHashMap的读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。  七. ConcurrentHashMap 读操作不需要加锁的奥秘   在本文第二节,我们介绍到HashEntry对象几乎是不可变的(只能改变Value的值),因为HashEntry中的key、hash和next指针都是final的。这意味着,我们不能把节点添加到链表的中间和尾部,也不能在链表的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变,这个特性可以大大降低处理链表时的复杂性。与此同时,由于HashEntry类的value字段被声明是Volatile的,因此Java的内存模型就可以保证:某个写线程对value字段的写入马上就可以被后续的某个读线程看到。此外,由于在ConcurrentHashMap中不允许用null作为键和值,所以当读线程读到某个HashEntry的value为null时,便知道产生了冲突 —— 发生了重排序现象,此时便会加锁重新读入这个value值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。总的来说,ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:  用HashEntery对象的不变性来降低读操作对加锁的需求;  用Volatile变量协调读写线程间的内存可见性;  若读时发生指令重排序现象,则加锁重读;    由于我们在介绍ConcurrentHashMap的get操作时,已经介绍到了第三点,此不赘述。下面我们结合前两点分别从线程写入的两种角度 —— 对散列表做非结构性修改的操作和对散列表做结构性修改的操作来分析ConcurrentHashMap是如何保证高效读操作的。  1、用HashEntery对象的不变性来降低读操作对加锁的需求   非结构性修改操作只是更改某个HashEntry的value字段的值。由于对Volatile变量的写入操作将与随后对这个变量的读操作进行同步,所以当一个写线程修改了某个HashEntry的value字段后,Java内存模型能够保证读线程一定能读取到这个字段更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程看到。   对ConcurrentHashMap做结构性修改时,实质上是对某个桶指向的链表做结构性修改。如果能够确保在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表,那么读/写线程之间就可以安全并发访问这个ConcurrentHashMap。在ConcurrentHashMap中,结构性修改操作包括put操作、remove操作和clear操作,下面我们分别分析这三个操作:  clear操作只是把ConcurrentHashMap中所有的桶置空,每个桶之前引用的链表依然存在,只是桶不再引用这些链表而已,而链表本身的结构并没有发生任何修改。因此,正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。  关于put操作的细节我们在上文已经单独介绍过,我们知道put操作如果需要插入一个新节点到链表中时会在链表头部插入这个新节点,此时链表中的原有节点的链接并没有被修改。也就是说,插入新的健/值对到链表中的操作不会影响读线程正常遍历这个链表。    下面来分析 remove 操作,先让我们来看看 remove 操作的源代码实现:      /**      * Removes the key (and its corresponding value) from this map.      * This method does nothing if the key is not in the map.      *      * @param  key the key that needs to be removed      * @return the previous value associated with <tt>key</tt>, or      *         <tt>null</tt> if there was no mapping for <tt>key</tt>      * @throws NullPointerException if the specified key is null      */     public V remove(Object key) {     int hash = hash(key.hashCode());         return segmentFor(hash).remove(key, hash, null);     }   同样地,在ConcurrentHashMap中删除一个键值对时,首先需要定位到特定的段并将删除操作委派给该段。Segment的remove操作如下所示:          /**          * Remove; match on key only if value null, else match both.          */         V remove(Object key, int hash, Object value) {             lock();     // 加锁             try {                 int c = count - 1;                       HashEntry<K,V>[] tab = table;                 int index = hash & (tab.length - 1);        // 定位桶                 HashEntry<K,V> first = tab[index];                 HashEntry<K,V> e = first;                 while (e != null && (e.hash != hash || !key.equals(e.key)))  // 查找待删除的键值对                     e = e.next;                  V oldValue = null;                 if (e != null) {    // 找到                     V v = e.value;                     if (value == null || value.equals(v)) {                         oldValue = v;                         // All entries following removed node can stay                         // in list, but all preceding ones need to be                         // cloned.                         ++modCount;                         // 所有处于待删除节点之后的节点原样保留在链表中                         HashEntry<K,V> newFirst = e.next;                         // 所有处于待删除节点之前的节点被克隆到新链表中                         for (HashEntry<K,V> p = first; p != e; p = p.next)                             newFirst = new HashEntry<K,V>(p.key, p.hash,newFirst, p.value);                           tab[index] = newFirst;   // 将删除指定节点并重组后的链重新放到桶中                         count = c;      // write-volatile,更新Volatile变量count                     }                 }                 return oldValue;             } finally {                 unlock();          // finally子句解锁             }         }   Segment的remove操作和前面提到的get操作类似,首先根据散列码找到具体的链表,然后遍历这个链表找到要删除的节点,最后把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。假设写线程执行remove操作,要删除链表的C节点,另一个读线程同时正在遍历这个链表,如下图所示:   我们可以看出,删除节点C之后的所有节点原样保留到新链表中;删除节点C之前的每个节点被克隆到新链表中(它们在新链表中的链接顺序被反转了)。因此,在执行remove操作时,原始链表并没有被修改,也就是说,读线程不会受同时执行 remove 操作的并发写线程的干扰。   综合上面的分析我们可以知道,无论写线程对某个链表进行结构性修改还是非结构性修改,都不会影响其他的并发读线程对这个链表的访问。 2、用 Volatile 变量协调读写线程间的内存可见性   一般地,由于内存可见性问题,在未正确同步的情况下,对于写线程写入的值读线程可能并不能及时读到。下面以写线程M和读线程N来说明ConcurrentHashMap如何协调读/写线程间的内存可见性问题,如下图所示:   假设线程M在写入了volatile变量count后,线程N读取了这个volatile变量。根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。根据 Volatile法则,B happens-before C。结合传递性,则可得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说,写线程M对链表做的结构性修改对读线程N是可见的。虽然线程N是在未加锁的情况下访问链表,但Java的内存模型可以保证:只要之前对链表做结构性修改操作的写线程M在退出写方法前写volatile变量count,读线程N就能读取到这个volatile变量count的最新值。    事实上,ConcurrentHashMap就是一个Segment数组,而每个Segment都有一个volatile变量count去统计Segment中的HashEntry的个数。并且,在ConcurrentHashMap中,所有不加锁读方法在进入读方法时,首先都会去读这个count变量。比如我们在上一节提到的get方法:      V get(Object key, int hash) {             if (count != 0) {            // read-volatile,首先读 count 变量                 HashEntry<K,V> e = getFirst(hash);   // 获取桶中链表头结点                 while (e != null) {                     if (e.hash == hash && key.equals(e.key)) {    // 查找链中是否存在指定Key的键值对                         V v = e.value;                         if (v != null)  // 如果读到value域不为 null,直接返回                             return v;                            // 如果读到value域为null,说明发生了重排序,加锁后重新读取                         return readValueUnderLock(e); // recheck                     }                     e = e.next;                 }             }             return null;  // 如果不存在,直接返回null         } 3、小结    在ConcurrentHashMap中,所有执行写操作的方法(put、remove和clear)在对链表做结构性修改之后,在退出写方法前都会去写这个count变量;所有未加锁的读操作(get、contains和containsKey)在读方法中,都会首先去读取这个count变量。根据 Java 内存模型,对同一个 volatile 变量的写/读操作可以确保:写线程写入的值,能够被之后未加锁的读线程“看到”。这个特性和前面介绍的HashEntry对象的不变性相结合,使得在ConcurrentHashMap中读线程进行读取操作时基本不需要加锁就能成功获得需要的值。这两个特性以及加锁重读机制的互相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)。  八. ConcurrentHashMap 的跨段操作   在ConcurrentHashMap中,有些操作需要涉及到多个段,比如说size操作、containsValaue操作等。以size操作为例,如果我们要统计整个ConcurrentHashMap里元素的大小,那么就必须统计所有Segment里元素的大小后求和。我们知道,Segment里的全局变量count是一个volatile变量,那么在多线程场景下,我们是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?显然不能,虽然相加时可以获取每个Segment的count的最新值,但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了。所以最安全的做法,是在统计size的时候把所有Segment的put,remove和clean方法全部锁住,但是这种做法显然非常低效。那么,我们还是看一下JDK是如何实现size()方法的吧:  /**      * Returns the number of key-value mappings in this map.  If the      * map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns      * <tt>Integer.MAX_VALUE</tt>.      *      * @return the number of key-value mappings in this map      */     public int size() {         final Segment<K,V>[] segments = this.segments;         long sum = 0;         long check = 0;         int[] mc = new int[segments.length];         // Try a few times to get accurate count. On failure due to         // continuous async changes in table, resort to locking.         for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {             check = 0;             sum = 0;             int mcsum = 0;             for (int i = 0; i < segments.length; ++i) {                 sum += segments[i].count;                    mcsum += mc[i] = segments[i].modCount;  // 在统计size时记录modCount             }             if (mcsum != 0) {                 for (int i = 0; i < segments.length; ++i) {                     check += segments[i].count;                     if (mc[i] != segments[i].modCount) {  // 统计size后比较各段的modCount是否发生变化                         check = -1; // force retry                         break;                     }                 }             }             if (check == sum)// 如果统计size前后各段的modCount没变,且两次得到的总数一致,直接返回                 break;         }         if (check != sum) { // Resort to locking all segments  // 加锁统计             sum = 0;             for (int i = 0; i < segments.length; ++i)                 segments[i].lock();             for (int i = 0; i < segments.length; ++i)                 sum += segments[i].count;             for (int i = 0; i < segments.length; ++i)                 segments[i].unlock();         }         if (sum > Integer.MAX_VALUE)             return Integer.MAX_VALUE;         else             return (int)sum;     }   size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会不断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。事实上,在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试RETRIES_BEFORE_LOCK次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。    那么,ConcurrentHashMap是如何判断在统计的时候容器的段发生了结构性更新了呢?我们在前文中已经知道,Segment包含一个modCount成员变量,在会引起段发生结构性改变的所有操作(put操作、 remove操作和clean操作)里,都会将变量modCount进行加1,因此,JDK只需要在统计size前后比较modCount是否发生变化就可以得知容器的大小是否发生变化。    至于ConcurrentHashMap的跨其他跨段操作,比如contains操作、containsValaue操作等,其与size操作的实现原理相类似,此不赘述。  九. 更多  如果读者需要要深入了解 HashMap,请移步我的另一篇博文《Map 综述(一):彻头彻尾理解 HashMap》。   更多关于哈希(Hash)和equals方法的介绍,请移步我的博文《Java 中的 ==, equals 与 hashCode 的区别与联系》。   更多关于Java内存模型与Volatile语义的介绍,请移步我的博文《Java 并发:volatile 关键字解析》。   更多关于Java Collection Framework各成员类的底层实现和原理的介绍,请见我的专栏《Java Collection Framework 源码剖析》。本专栏主要从源码角度剖析Java Collection Framework各成员类的底层实现原理和技巧,包括但不限于List、Map等经典容器类、ConcurrentHashMap等并发容器类,并说明各容器类间的区别、联系以及应用场景,以便不断加深对Java容器框架的理解。 ———————————————— 原文链接:https://blog.csdn.net/justloveyou_/article/details/72783008/ 
  • [技术干货] Hashtable与ConcurrentHashMap区别
    ConcurrentHashMap融合了hashtable和hashmap二者的优势。hashtable是做了同步的,hashmap未考虑同步。所以hashmap在单线程情况下效率较高。hashtable在的多线程情况下,同步操作能保证程序执行的正确性。但是hashtable每次同步执行的时候都要锁住整个结构。看下图:图左侧清晰的标注出来,lock每次都要锁住整个结构。ConcurrentHashMap正是为了解决这个问题而诞生的。ConcurrentHashMap锁的方式是稍微细粒度的。 ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来 只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数 据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。下面分析ConcurrentHashMap的源码。主要是分析其中的Segment。因为操作基本上都是在Segment上的。先看Segment内部数据的定义。从上图可以看出,很重要的一个是table变量。是一个HashEntry的数组。Segment就是把数据存放在这个数组中的。除了这个量,还有诸如loadfactor、modcount等变量。看segment的get 函数的实现:加上hashentry的代码:可以看出,hashentry是一个链表型的数据结构。在segment的get函数中,通过getFirst函数得到第一个值,然后就是通过这个值的next,一路找到想要的那个对象。如果不空,则返回。如果为空,则可能是其他线程正在修改节点。比如上面说的弱一致迭代器在将指针更改为新值的过程。而之前的 get操作都未进行锁定,根据bernstein条件,读后写或写后读都会引起数据的不一致,所以这里要对这个e重新上锁再读一遍,以保证得到的是正确值。readValueUnderLock中就是用了lock()进行加锁。put操作已开始就锁住了整个segment。这是因为修改操作时不能并发的。同样,remove操作也是如此(类似put,一开始就锁住真个segment)。但要注意一点区别,中间那个for循环是做什么用的呢?(截图未完全,可以自己找找代码查看一下)。从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他 所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。原文链接:https://blog.csdn.net/csdn_ds/article/details/72528283
  • [技术干货] 浅谈java集合中线程安全的类
    vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。 statck:堆栈类,先进后出 hashtable:就比hashmap多了个线程安全 Collections的synchronizedXxxx()方法包装的集合 ConcurrentXxxx:从jdk1.5提供,通过分段锁实现线程安全,使用这类方法既可以不怎么影响效率,又可以保证安全,建议使用。 具体原理可参照:http://blog.csdn.net/csdn_ds/article/details/72528283 除了这些之外,其他的都是非线程安全的类和接口。 线程安全的类其方法是同步的,每次只能一个访问。是重量级对象,效率较低,一般用的不多,只有在特殊情况下会考虑使用。 1、Vector、ArrayList、LinkList之间的区别 Vector : 基于Array的List,其实就是封装了Array所不具备的一些功能方便我们使用,它不可能走出Array的限制。性能也就不可能超越Array。所以,在可能的情况下,我们要多运用Array。另外很重要的一点就是Vector“synchronized”的,这个也是Vector和ArrayList的唯一的区别。 ArrayList:同Vector一样是一个基于数组实现的,但是不同的是ArrayList不是同步的。所以在性能上要比Vector优越一些,但是当运行到多线程环境中时,可需要自己在管理线程的同步问题。 LinkedList:LinkedList不同于前面两种List,它不是基于Array的,所以不受Array性能的限制。它每一个节点(Node)都包含两方面的内容:1.节点本身的数据(data);2.下一个节点的信息(nextNode)。所以当对LinkedList做添加,删除动作的时候就不用像基于Array的List一样,必须进行大量的数据移动。只要更改nextNode的相关信息就可以实现了。这就是LinkedList的优势。 2、HashTable跟HashMap的区别  HashTable是线程安全的,即HashTable的方法都提供了同步机制;HashMap不是线程安全的,即不提供同步机制 ;HashTable不允许插入空值,HashMap允许! 3、StringBuffer和StringBuilder的区别 StringBuffer是线程安全的,StringBuilder是线程不安全的。 题外:此处记录一下java中8中基本的数据类型:  byte、int、short、long、float、double、boolean、char ————————————————                     原文链接:https://blog.csdn.net/csdn_ds/article/details/72528300 
  • [技术干货] 2 JVM的内存管理(堆内存)
    我用思维导图对JVM的内存结构做简单的划分,如下图所示:下面我们对各个区进行说明。堆:也称heap堆区。堆是jvm内存中占用空间最大的一个区域。主要分为新生代、老年代、永久代(jdk1.8以后叫元空间,到1.9以后又被移除)新生代:在new一个对象时,会把堆新生代的内存空间进行判断,如果内存空间够则放入新生代(如果是大对象,例如数据很多的容器对象,有可能直接放入老年代)。如果内存空间不够放入该对象,则触发young gc,如果触发15次新生代空间还不够则把之前使用的数据迁移到老年代并释放新生代所有的空间。如果老年代的空间不够用,则进行full gc。如果老年代内存也不够用则抛出OOM并结束线程。gc的种类:minor GC(又叫young GC):用于收集年轻代中的非存活对象,在新生代空间不足时触发,young gc 可能会触发线程暂停。所以在一些并发比较高或者集中处理一些行为中java会出现卡顿的现象就是出于这个原因major gc:在老年代空间不足时触发。full gc的效率比较低,应尽量减少full gc的发生。目前只有CMS收集器有单独收集老年代的行为mixed GC(混合收集):主要收集年轻代和部分老年代的非存活对象。目前G1收集器采用这种行为。full gc:主要收集整个堆(新生代和老年代)中的非存活对象。System.gc()调用时候会触发full gc 。我们可以通过-XX:DisableExplicitGC来禁止System.gc()的行为。在多次young gc后把新生代存活的对象迁移到老年代(在一定条件下才会触发。更加详细的信息可参阅《hotspot实战》由于篇幅有限不在这里详述),如果老年代空间不足时会触发full gc。full gc后老年代内存还是不够用则OOM我们看下新生代的内存分配,如下图:对象的分配过程:新生成的对象在年轻代Eden区中分配内存,当Eden空间已满时,触发Minor GC,将不再被其他对象所引用的对象进行回收,存活下来的对象被转移到Survivor0区。Survivor0区满后触发Minor GC,将Survivor0区存活下来的对象转移到Survivor1区,同时,清空Survivor0区,保证总有一个Survivor区为空。经过多次Minor GC后,仍然存活的对象被转移到老年代,进入老年代的Minor GC次数可以通过参数-XX:MaxTenuringThreshold=<N>进行设置,默认为15次。当老年代已满时会触发Major GC(即:Full GC,因此执行Major GC时会先执行Minor GC)。分代收集的原因:将对象按照存活概率进行分类,主要是为了减少扫描范围和执行GC的频率,同时,对不同区域采用不同的回收算法,提高回收效率。年轻代中存在两块相同大小的Survivor区的原因:解决内存碎片化,即:保证分配对象(如:大对象)时有足够的连续内存空间。对象进入老年代的触发条件:对象的年龄达到15岁时。默认的情况下,对象经过15次Minor GC后会被转移到老年代中。对象进入老年代的Minor GC次数可以通过JVM参数:-XX:MaxTenuringThreshold进行设置,默认为15次。动态年龄判断。当一批存活对象的总大小超过Survivor区内存大小的50%时,按照年龄的大小(年龄大的存活对象优先转移)将部分存活对象转移到老年代中。大对象直接进入老年代 。当需要创建一个大于年轻代剩余空间的对象(如:一个超大数组)时,该对象会被直接存放到老年代中,可以通过参数-XX:PretenureSizeThreshold(默认值是0,即:任何对象都会先在年轻代分配内存)进行设置。Minor GC后的存活对象太多无法放入Survivor区时, 会将这些对象直接转移到老年代中。字符串常量池:字符串常量池是Java中的一个特殊的存储区域,用于存储字符串常量。在Java中,字符串常量是不可变的,因此可以被共享。这样可以减少内存的使用,提高程序的性能。在JDK8中,字符串常量池存储在堆中。静态变量:静态变量是指在类中定义的变量,它们的值在整个程序运行期间都不会改变。在JDK8中取消了永久代,方法区变成了一个逻辑上的区域,因此,静态变量的内存在堆中进行分配(JDK7及以前,静态变量的内存在永久代中进行分配)。它们的生命周期与类的生命周期相同。线程本地缓冲区:tlabTLAB(Thread Local Allocation Buffer)是Java虚拟机中的一个优化技术,主要用于提高对象的分配效率。每个线程都有自己的TLAB,用于分配对象。当一个线程需要分配对象时,它会先在自己的TLAB中分配,如果TLAB中的空间不足,则会向堆中申请空间。上面对内存的堆区进行了阐述。由于不同的jdk版本处理内存的方式不一样,会有些出入敬请谅解
  • [技术干货] 初识JVM
    想要对java虚拟机更深入的了解,可以查看《HotSpot实战》。需要电子版的请扫我头像关注我的个人号,发送000006领取电子书我们知道java程序是把java源文件编译成字节码.class文件,然后交给JVM执行。那么java到底是解释执行还是编译执行的语言呢?这个没有固定的答案,具体要要看用什么样的JVM。JVM把class文件编译成机器码执行那就是编译执行,如果JVM对class加载后由JVM解释执行就是解释执行。有的JVM即有yo解释执行也有编译执行。JVM的种类:hotspot jvm:这是最常用的JVM实现,由Oracle开发。HotSpot JVM提供了高效的执行引擎和垃圾回收机制,支持多种垃圾回收算法,如Parallel GC、CMS GC、G1 GC等。它广泛应用于服务器和桌面应用程序中‌openJ9 jvm:由IBM开发,专注于高性能和低内存消耗。OpenJ9 JVM在性能和资源利用方面表现出色,适用于需要高性能和资源优化的应用场景‌graalm: 由Oracle开发,是一个通用虚拟机,支持多种编程语言。GraalVM不仅支持Java,还支持其他语言如JavaScript、Python等,适用于需要多语言支持和高性能的应用‌zing jvm: 由Azul Systems开发,专注于低延迟和高吞吐量。Zing JVM在金融交易等对延迟要求极高的场景中表现出色‌dalvik jvm: 用于Android平台,是Android特有的JVM变体。Dalvik JVM优化了移动设备的资源利用,适用于Android应用程序的运行‌我们后面重点讲hotspot jvm也是应用最广泛的hotspot源码下载:cid:link_0犹豫篇幅限制,关于源码编译这里不再熬述。1    java虚拟机与程序的生命周期:    1.1  执行了System.exit()方法    1.2  程序正常执行结束    1.3 程序在执行过程中遇到了异常或错误异常中止(一般是主线程的异常中止)    1.4 由于操作系统的错误而导致java虚拟机进程的中止2    类加载,链接,初始化    2.1 加载:查找并加载类的二进制数据JVM规范规定类加载器在预料类将要被使用时预先加载它,有个预热的过程。类加载器在程序主动使用某一个类时才报告错误。加载完成以后进入到连接阶段    2.2 连接将已经读入到内存的二进制数据合并到虚拟机的运行环境中,然后进行一系列的验证,确保被加载类的正确性。    2.3 准备阶段在该阶段,java虚拟机为类的静态变量分配内存,并设置默认的初始值    2.4 解析该阶段java虚拟机会把类的二进制数据中的符号引用替换为直接引用    2.5 初始化为类的静态成员变量赋予正确的初始值3    java程序对类的使用主要分为两种    3.1 主动使用创建类的实例。例如:new Test()访问某个类或接口的静态变量调用类的静态方法反射初始化一个类的子类虚拟机启动时被表明为启动的类    3.2 被动使用除掉以上的情况属于被动使用,不会导致类的初始化。虚拟机实现必须在每个类或接口被java程序首次使用时才初始化。类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其方法存进运行时数据区的方法区内。然后在堆区创建一个Java.lang.Class对象,用来封装在类在方法区内的数据结构。
  • [互动交流] obs上传对象报错
    com.amazonaws.SdkClientException: Unable to execute HTTP request: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target https上传到obs报这个错,是因为我服务端没有安装obs的https证书吗
  • [技术干货] Java中的设计模式有哪些
    Java中的设计模式是一种被广泛应用于软件开发的解决方案,旨在解决常见的软件设计问题。设计模式分为三大类:创建型模式、结构型模式和行为型模式。以下是一些常见的设计模式及其原理:  1. 创建型模式 单例模式(Singleton Pattern)  原理:单例模式确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这通常用于需要全局唯一实例的场景,比如配置管理、数据库连接池等。  实现方式:  私有构造函数:防止外部直接实例化。 静态实例:类中持有一个私有的静态实例。 公共静态方法:提供一个全局访问点来获取实例,并确保线程安全(使用 synchronized 或双重检查锁定等技术)。 示例代码:  public class Singleton {     private static Singleton instance;          private Singleton() {         // 私有构造函数,防止外部实例化     }          public static synchronized Singleton getInstance() {         if (instance == null) {             instance = new Singleton(); // 创建实例         }         return instance;     } } 工厂方法模式(Factory Method Pattern)  原理:工厂方法模式定义一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式将对象的创建推迟到子类,从而实现了创建和使用的解耦。  实现方式:  抽象工厂类:定义一个工厂方法,返回抽象产品。 具体工厂类:实现工厂方法,创建具体产品。 抽象产品类:定义产品的公共接口。 具体产品类:实现产品接口。 示例代码:  // 抽象产品 interface Product {     void use(); }  // 具体产品 class ConcreteProductA implements Product {     public void use() {         System.out.println("Using Product A");     } }  class ConcreteProductB implements Product {     public void use() {         System.out.println("Using Product B");     } }  // 抽象工厂 abstract class Creator {     public abstract Product factoryMethod(); }  // 具体工厂 class ConcreteCreatorA extends Creator {     public Product factoryMethod() {         return new ConcreteProductA();     } }  class ConcreteCreatorB extends Creator {     public Product factoryMethod() {         return new ConcreteProductB();     } }  抽象工厂模式(Abstract Factory Pattern)  原理:抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。它创建多个产品对象,每个产品属于一个产品族。  实现方式:  抽象工厂:定义创建产品对象的接口。 具体工厂:实现抽象工厂接口,生成具体的产品。 抽象产品:定义产品的接口。 具体产品:实现抽象产品接口。 示例代码:  // 抽象产品A interface AbstractProductA {     void operationA(); }  // 具体产品A1 class ProductA1 implements AbstractProductA {     public void operationA() {         System.out.println("Operation A1");     } }  // 具体产品A2 class ProductA2 implements AbstractProductA {     public void operationA() {         System.out.println("Operation A2");     } }  // 抽象工厂 interface AbstractFactory {     AbstractProductA createProductA(); }  // 具体工厂1 class ConcreteFactory1 implements AbstractFactory {     public AbstractProductA createProductA() {         return new ProductA1();     } }  // 具体工厂2 class ConcreteFactory2 implements AbstractFactory {     public AbstractProductA createProductA() {         return new ProductA2();     } } 2. 结构型模式 适配器模式(Adapter Pattern)  原理:适配器模式将一个类的接口转换成客户端所期望的另一种接口,使得原本因接口不匹配而不能一起工作的类可以一起工作。它通过包装一个对象,使其符合目标接口。  实现方式:  目标接口:客户端期望的接口。 适配者类:需要适配的类,具有目标接口所不具备的接口。 适配器类:实现目标接口,并将请求转发到适配者类。 示例代码:  // 目标接口 interface Target {     void request(); }  // 适配者类 class Adaptee {     public void specificRequest() {         System.out.println("Specific request");     } }  // 适配器类 class Adapter implements Target {     private Adaptee adaptee;      public Adapter(Adaptee adaptee) {         this.adaptee = adaptee;     }      public void request() {         adaptee.specificRequest();     } } 装饰者模式(Decorator Pattern)  原理:装饰者模式动态地给一个对象添加一些额外的职责。它比生成子类更灵活,允许在运行时为对象添加功能。  实现方式:  组件接口:定义对象的接口。 具体组件:实现组件接口的类。 装饰者抽象类:持有一个组件对象,并实现组件接口。 具体装饰者:扩展装饰者抽象类,为对象添加新功能。 示例代码:  // 组件接口 interface Component {     void operation(); }  // 具体组件 class ConcreteComponent implements Component {     public void operation() {         System.out.println("Concrete Component Operation");     } }  // 装饰者抽象类 abstract class Decorator implements Component {     protected Component component;      public Decorator(Component component) {         this.component = component;     }      public void operation() {         component.operation();     } }  // 具体装饰者 class ConcreteDecorator extends Decorator {     public ConcreteDecorator(Component component) {         super(component);     }      public void operation() {         super.operation();         addedBehavior();     }      private void addedBehavior() {         System.out.println("Added Behavior");     } } 代理模式(Proxy Pattern) 原理:代理模式为其他对象提供一种代理以控制对这个对象的访问。代理对象可以在客户端和真实对象之间起到中介作用,可以增加额外的功能或控制访问权限。 实现方式:  主题接口:定义真实对象和代理对象的共同接口。 真实主题:实现主题接口,定义实际的业务逻辑。 代理对象:实现主题接口,控制对真实主题的访问。 示例代码:  // 主题接口 interface Subject {     void request(); }  // 真实主题 class RealSubject implements Subject {     public void request() {         System.out.println("Real Subject Request");     } }  // 代理对象 class Proxy implements Subject {     private RealSubject realSubject;      public void request() {         if (realSubject == null) {             realSubject = new RealSubject();         }         realSubject.request();     } } 3. 行为型模式 策略模式(Strategy Pattern) 原理:策略模式定义一系列算法,将每一个算法封装起来,并使它们可以互换。策略模式让算法独立于使用它的客户端独立变化。 实现方式:  策略接口:定义所有支持的算法的共同接口。 具体策略:实现策略接口的具体算法。 上下文:使用策略对象,调用策略的方法。 示例代码:  // 策略接口 interface Strategy {     void execute(); }  // 具体策略A class ConcreteStrategyA implements Strategy {     public void execute() {         System.out.println("Strategy A");     } }  // 具体策略B class ConcreteStrategyB implements Strategy {     public void execute() {         System.out.println("Strategy B");     } }  // 上下文 class Context {     private Strategy strategy;      public Context(Strategy strategy) {         this.strategy = strategy;     }      public void executeStrategy() {         strategy.execute();     } } **观察者模式(Observer Pattern ———————————————— 原文链接:https://blog.csdn.net/dataiyangu/article/details/140572071 
  • [技术干货] Java - 三种基本的设计模式
    目录  一、单例模式 二、工厂模式 三、代理模式 1、现有业务层存在的问题 2、代理的开发 3、静态代理的开发 (1)静态代理类 (3)调用代理类的方法 4、动态代理开发 (1)前置知识:通过反射调用对象的方法 (2)通过proxy类,动态的为现有业务生成代理对象 一、单例模式 1、只创建一个实例对象的设计模式称为单例模式 2、单例模式的优点:可以节省创建对象的时间和对象占用的空间 3、单例模式的对象必须是无状态的 4、无状态的条件(满足任意之一即可): (1)类本身是没有非静态的成员属性 (2)有非静态的成员属性,但是这些属性是无状态的 5、单例的两种实现: (1)饿汉式: 一开始就创建对象  (2)懒汉式: 等到用到时再创建对象 二、工厂模式 1、使用工厂函数而不是直接通过new来创建对象 2、工厂模式的优点:可以根据不同创建条件创建不同对象,并且可以在启动工厂时,预先创建对象  3、基于配置文件的单例工厂实现  (1)原理:根据类的原先定名创建对象(使用反射) 三、代理模式 1、现有业务层存在的问题 (1)业务层中控制事务的代码和业务逻辑代码出现了耦合 假设运来的代码使用方法A控制十五,后来使用方法B去控制事务,此时可能不慎修改了业务代码,同理,也可能在修改业务代码时不慎修改了控制事务的代码  解决方案: 开发一个代理类,也可以实现接口中的所有业务方法。再实现是,除了控制事务的代码外,直接调用真正的业务层的相应业务方法即可 (2)业务层中控制事务的代码中出现了大量的冗余  2、代理的开发 代理对象可以在客户和目标对象之间起到中介作用,从而为对象目标增添额外的功能 之前的处理方式: action(controller)调用service,service调用dao 使用代理后的处理方式: action(controller)调用proxy,proxy调用service,service调用dao 代理类和目标类必须实现同样的接口,且代理对象会依赖目标对象 3、静态代理的开发 (1)静态代理类  (2)更改目标实现类 (3)调用代理类的方法 4、动态代理开发 (1)前置知识:通过反射调用对象的方法 (2)通过proxy类,动态的为现有业务生成代理对象 ————————————————            原文链接:https://blog.csdn.net/XHW0901/article/details/126316012 
  • [技术干货] java常用的设计模式
    文详细介绍了Java中三种常用的设计模式:单例模式、工厂模式和代理模式。对于单例模式,讲解了其概念、优缺点及五种常见的实现方式;在工厂模式部分,通过简单工厂、工厂方法和抽象工厂展示了如何创建不同类型的对象;最后,通过静态和动态代理模式的实践,阐述了如何在实际操作中进行类的代理操作。 一、单例模式 1.概述 单例模式的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。属于设计模式三大类中的创建型模式。 单例模式具有典型的三个特点:  只有一个实例。 自我实例化。 提供全局访问点。  2.优缺点 优点:由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统效率,同时也能够严格控制客户对它的访问。 缺点:也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难。 3.常见实现方式 常见的单例模式实现方式有五种:饿汉式、懒汉式、双重检测锁式、静态内部类式和枚举单例。而在这五种方式中饿汉式和懒汉式又最为常见。下面将一一列举这五种方式的实现方法:  饿汉式:线程安全,调用效率高。但是不能延时加载。示例: public class SingletonDemo1 {  //线程安全的 //类初始化时,立即加载这个对象 private static SingletonDemo1 instance = new SingletonDemo1();  private SingletonDemo1() { }  //方法没有加同步块,所以它效率高 public static SingletonDemo1 getInstance() {     return instance; } } 由于该模式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,且是线程安全的。  懒汉式:线程不安全。示例: public class SingletonDemo2 {  //线程不安全的  private static SingletonDemo2 instance = null;  private SingletonDemo2() { }  //运行时加载对象 public static SingletonDemo2 getInstance() {     if (instance == null) {         instance = new SingletonDemo2();     }     return instance; } } 由于该模式是在运行时加载对象的,所以加载类比较快,但是对象的获取速度相对较慢,且线程不安全。如果想要线程安全的话可以加上synchronized关键字,但是这样会付出惨重的效率代价。  懒汉式(双重同步锁) public class SingletonDemo3 {  private static volatile SingletonDemo3 instance = null;  private SingletonDemo3() { }  //运行时加载对象 public static SingletonDemo3 getInstance() {     if (instance == null) {         synchronized(SingletonDemo3.class){              if(instance == null){                  instance = new SingletonDemo3();              }         }     }     return instance; } } 由于剩下的几种实现方式暂没有接触过,可暂时参考一张图搞定Java设计模式,单例模式。  注:注意单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。 (这句话表述的有点问题,单例类一般情况只想内部保留一个实例对象,所以会选择将构造函数声明为私有的,这才使得单例类无法被继承。单例类与继承没有强关联关系。)  4.常见应用场景 网站计数器。 项目中用于读取配置文件的类。 数据库连接池。因为数据库连接池是一种数据库资源。 Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。 Servlet中Application Windows中任务管理器,回收站。  二、工厂模式 1.对工厂模式的理解 简单工厂:通过工厂类生成不同的类。工厂类返回一个父类型的类,通过if或者switch判断用户给的数据,通过不同的数据返回不同的类。 工厂方法:比较重要的就是抽象类里面的一个抽象方法,所有继承了抽象类的类都必须实现该方法,之后在调用的时候利 ————————————————           原文链接:https://blog.csdn.net/qq_42674061/article/details/109735623 
  • [技术干货] Java中的设计模式
    1.什么是设计模式?         设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的实验和错误总结出来的。         设计模式是一套被反复使用的,多数人知晓的,经过分类编目的,代码设计经验的总结 2.设计模式的作用是什么?         使用设计模式就是为了重用代码,让代码更容易被他人理解,保证代码可靠性。 3.常见的设计模式         常见的设计模式又23种。         3.1单例模式         单例模式---保证一个类仅有一个实例         当类被频繁地创建与销毁的时候,我们使用单例模式,这样可以减少了内存的开销,避免对资源的多重占用         单例模式条件:                 1.构造方法私有         2.提供一个静态方法【公共】返回创建号的当前类对象         两种表示方式         懒汉式         例如: package com.test1; /**  * 懒汉式  * @author zxc  *  */ public class SingleObject1 {     private static SingleObject1 sobj=null;     private SingleObject1(){}     public static SingleObject1 getSinleObject1(){         if(sobj==null){             sobj=new SingleObject1();         }         return sobj;     } }  package com.test1;   public class TestMain {       public static void main(String[] args) {         SingleObject1 s1=SingleObject1.getSinleObject1();         SingleObject1 s2=SingleObject1.getSinleObject1();         System.out.println(s1.hashCode());         System.out.println(s2.hashCode());         if(s1==s2){             System.out.println("是同一对象");         }     }   } 饿汉式 例如:  package com.test1; /**  * 饿汉式  * @author zxc  *  */ public class SingleObject2 {     private static SingleObject2 sobj=new SingleObject2();     private SingleObject2(){}     //当在多线程情况下使用是为了保证当前类对象只有一个我们就需要添加synchornized     public static synchronized SingleObject2 getSinleObject1(){         return sobj;     } } package com.test1;   public class TestMain {       public static void main(String[] args) {                           SingleObject2 s1=SingleObject2.getSinleObject1();         SingleObject2 s2=SingleObject2.getSinleObject1();         System.out.println(s1.hashCode());         System.out.println(s2.hashCode());         if(s1==s2){             System.out.println("是同一对象");         }     }   }  懒汉式与饿汉式的区别          相同点:保证当前类的对象只有一个          书写上:1.构造方法私有                          2. 提供一个静态方法【公共】返回创建好的当前类对象          不同点:          书写上:懒汉式中保存当前类的对象变量初始为null                          饿汉式中保存当前类的对象初始变量为new好的当前类对象          运行速度上:懒汉式比饿汉式稍微差一些          资源利用率:饿汉式比懒汉式稍微差一些          3.2工厂模式          工厂模式---有一个专门的java类充当当前生产对象的工厂          使用工厂模式的条件:1.需求量大        2.牵一发,动全身。          工厂模式中的角色:工厂角色---生产对象        抽象产品角色---【抽象类/接口】        具体产品-----【抽象类/接口子类】          例如:有农场生产各种水果,有西瓜,有苹果,有香蕉                  工厂角色---农场                  抽象产品角色---水果                  西瓜、苹果、香蕉---具体产品  在项目目录下创建出一个菜单文件  苹果=com.test2.PingGuo 西瓜=com.test2.XiGua package com.test2;   import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.util.HashMap;   public class NongChang {     public static ShuiGuo maishuiguo(String name){         ShuiGuo sg=null;         //读取菜单         HashMap<String, String> menuMap=readMenu();         //根据键得到值         String calssName=menuMap.get(name);         try{         //利用反射机制创建对象         Class classobj=Class.forName(calssName);         sg=(ShuiGuo)classobj.newInstance();         }catch(Exception e){             e.printStackTrace();         }         return sg;     }     /**      * 读取菜单      */     private static HashMap<String, String> readMenu() {         HashMap<String, String> menuMap=new HashMap<String, String>();         try {             BufferedReader buff=new BufferedReader(new FileReader(new File("menu.txt")));             String menuitem=null;             while((menuitem=buff.readLine())!=null){                 String muenuarray[]=menuitem.split("=");                 menuMap.put(muenuarray[0],muenuarray[1]);                             }             buff.close();         }catch (Exception e) {             // TODO Auto-generated catch block             e.printStackTrace();         }          return menuMap;         } }  package com.test2;   public interface ShuiGuo {     void eat(); } package com.test2;   public class PingGuo implements ShuiGuo{       @Override     public void eat() {         System.out.println("我是苹果,削皮吃");              }   } package com.test2;   public class XiGua implements ShuiGuo{       @Override     public void eat() {         System.out.println("我是西瓜");              }   } package com.test2;   import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;   public class TestMain {       public static void main(String[] args) {         BufferedReader buff=new BufferedReader(new InputStreamReader(System.in));         System.out.println("买水果");         String info;         try {             info = buff.readLine();                          ShuiGuo sg=NongChang.maishuiguo(info);             sg.eat();         } catch (IOException e) {             // TODO Auto-generated catch block             e.printStackTrace();         }       }   }          3.3代理模式          代理模式---为其他对象提供一种代理以控制对这个对象的访问。          买火车票不一定在火车站买,也可以去代售点          代理模式被分为兄弟模式和父子模式          缺点:需要额外提供业务功能实现相似的子类。【 工作量大】  兄弟模式---同一个接口的两个子类  例如:  package com.test3;   public interface SellPiao {     void maipiao(); } package com.test3;   public class HuoCheZhan implements SellPiao{       @Override     public void maipiao() {         System.out.println("火车站买票");              }   } package com.test3;   public class DaiShouDian implements SellPiao{       @Override     public void maipiao() {         System.out.println("代售点买票");              }   } package com.test3;   public class TestMain {       public static void main(String[] args) {         HuoCheZhan hcz=new HuoCheZhan();         hcz.maipiao();         DaiShouDian dsd=new DaiShouDian();         dsd.maipiao();       }   } 父子模式---继承关系  例如:  package com.test4;   public class HuoCheZhan {     public void maipao(){         System.out.println("火车站买票");     } } package com.test4;   public class DaiShouDian extends HuoCheZhan{     public void maipao(){         System.out.println("代售点买票");     } } package com.test4;   public class TestMain {       public static void main(String[] args) {         HuoCheZhan hcz=new HuoCheZhan();         hcz.maipao();         DaiShouDian dsd=new DaiShouDian();         dsd.maipao();     }   } 动态代理---由一个java类来负责创建代理类对象  JDK动态代理---通过java.lang.reflect包Class Proxy类来创建代理类对象  例如:  package com.test5;   public interface SellPiao {     void maipiao(); } package com.test5;   public class HuoCheZhan implements SellPiao{       @Override     public void maipiao() {         System.out.println("火车站买票");              }   } package com.test5;   import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;   public class ProxyObject implements InvocationHandler{     //定义目标对象     private Object targetObject;     public ProxyObject(Object targetObject){         this.targetObject=targetObject;     }          //得到代理对象     public Object getProxy(){         //java.lang.reflect包 Class Proxy类         //ClassLoader loader---类加载器         ClassLoader loader=this.getClass().getClassLoader();         //Class<?>[] interfaces---接口反射对象         Class interfaces[]=this.targetObject.getClass().getInterfaces();         //InbocationHander  h-this         return Proxy.newProxyInstance(loader, interfaces, this);     }                         @Override     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {         // TODO Auto-generated method stub         return method.invoke(targetObject, args);     }   }  package com.test5;   public class TestMain {       public static void main(String[] args) {         HuoCheZhan hcz=new HuoCheZhan();                  ProxyObject proxyobj=new ProxyObject(hcz);         //代理类对象         SellPiao dsd=(SellPiao)proxyobj.getProxy();         dsd.maipiao();     }   } 【只能为实现过某个接口的java类提供代理类对象】          CGlib代理---CGlib是一个第三发的开发包,用的时候需要自己实现下载导入到项目中  例如:  package com.test6;   public class HuoCheZhan {     public void maipiao(){         System.out.println("火车站买票");     } } package com.test6;   import java.lang.reflect.Method;   import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy;   public class ProxyObject implements MethodInterceptor{     //定义目标对象     private Object targetObject;     public ProxyObject(Object targetObject){         this.targetObject=targetObject;     }          //得到代理对象     public Object getPrxoy(){         Enhancer enhancer=new Enhancer();         enhancer.setSuperclass(targetObject.getClass());         enhancer.setCallback(this);         return enhancer.create();     }       @Override     public Object intercept(Object proxy, Method arg1, Object[] params, MethodProxy methodProxy) throws Throwable {         // TODO Auto-generated method stub         return methodProxy.invokeSuper(proxy, params);     } }  package com.test6;   public class TestMain {       public static void main(String[] args) {     HuoCheZhan hcz=new HuoCheZhan();     ProxyObject proxy=new ProxyObject(hcz);     HuoCheZhan dsd=(HuoCheZhan)proxy.getPrxoy();     dsd.maipiao();     }   } 【所有的java类提供代理类对象】 比较:         1.静态代理是通过在代码中显式定义一个业务实现类一个代理,在代理类中对同名的业务方法进行包装,用户通过代理类调用被包装过的业务方法;         手动创建一个与目标类相同的接口的子类,包装目标类。         2.JDK动态代理是通过接口中的方法名,在动态生成的代理类中调用业务实现类的同名方法;【兄弟模式】         通过jdk提供的反射保重Proxy这个类,动态的创建一个与目标类实现相同接口的子类对象,包装目标。          3.CGlib提供的Enhancer这个类,动态的创建一个目标类的子类对象,包装目标类。 ———————————————— 原文链接:https://blog.csdn.net/weixin_52821030/article/details/121773921 
  • [技术干货] 学习Java?你得了解这些经典的设计模式
    (注:本文思想主要来源于哈工大计算学部王忠杰教授的《软件构造》) 一、创造型模式 1.工厂方法模式 工厂方法:也被称为“虚拟构造器”。定义用于创建对象的接口,但让子类决定实例化哪个类。工厂方法允许类将实例化延迟到子类。当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。 优点:消除了将特定于应用程序的类绑定到代码的需要。 体现了OCP原则(对扩展的开放,对修改已有代码的封闭)  缺点:客户端可能需要创建一个Creator的子类,这样他们就可以创建一个特定ConcreteProduct。  二、结构化模式 1.适配器模式 适配器Adapter:目的: 将某个类/接口转换为client期望的其他形式,适配器允许类一起工作,否则由于接口不兼容而无法工作。通过增加一个接口,将已存在的子类封装起来,client面向接口编程,从而隐藏了具体子类。对象:将旧组件重用到新系统(也称为“包装器”) 比如,可以举个例子: 2.装饰器模式 为对象增加不同侧面的特性 ,对每一个特性构造子类,通过委派机制增加到对象上,例如,下图为一个装饰器模式对应的类图。 可以举以下例子:  三、行为类模型 1.策略模式 针对特定的任务存在不同的算法,但客户端可以在运行时根据动态上下文在算法之间切换。 比如,对客户列表进行排序(冒泡排序、合并排序、快速排序),为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例。 例如,你想实现购物时的两种支付方法,类图绘制如下: 创建不同的子类对象,能够实现不同的结算方式。 2.模板模式 做事情的步骤一样,但具体方法不同 。比如打开、读取、编写不同类型的文档。我们可以让共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。  不难想到,通常使用继承和重写实现模板模式。而策略模式使用委托来改变整个算法。  3.迭代器 客户端希望遍历被放入容器/集合类的一组ADT对象,无需关心容器的具体类型。也就是说,不管对象被放进哪里,都应该提供同样的遍历方式。  通过迭代的策略模式,我们能够隐藏底层容器的内部实现,支持统一接口的多种遍历策略,易于更改容器类型且促进程序各部分之间的交流。  迭代模式:让自己的集合类实现Iterable接口,并实现自己的独特Iterator迭代器(hasNext, next, remove),允许客户端利用这个迭代器进行显式或隐式的迭代遍历:  4.visitor         对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类 。为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码 可以在不改变ADT本身的情况下通过delegation接入ADT。   本质上:将数据和作用于数据上的某种/些特定操作分离开来。 Strategy vs visitor:  二者都是通过delegation建立两个对象的动态联系,但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。 而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。 visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。 ———————————————— 原文链接:https://blog.csdn.net/m0_73695112/article/details/139197206 
  • [技术干货] java 5种设计模式
    设计模式总结:  5种创建型,7种结构型,11种行为型1.1  工厂方法模式    工厂方法模式(根据参数不同,工厂方法返回不同的产品对象),    多个工厂方法模式(创建不同产品不需要参数),    静态工厂方法模式(将工厂方法改为静态的,不需要实例化类)1.2  抽象工厂:适合产品多变的情况,要生产新的产品就必须在工厂类里面加生产的方法,违背开闭原则。抽象工厂,       增加一个 工厂类接口,一个接口方法; 各种产品实现这个工厂接口,生产自己对应的产品。1.3   单例模式 :Singleton  (懒汉式&饿汉式)特殊的工厂方法模式,一个类只有一个实例:                           1. 拥有一个私有的静态实例;                           2. 拥有 私有的默认构造函数;                           3. 静态工厂方法,如果是懒汉式的必须同步的,防止多线程同时操作;                           4.重写clone()函数,返回当前实例对象,默认clone()创建新实例;public class SingletonFactory{           //1.私有的防止外部变量引用;             private static SingletonFactory _instance=null;            //2.私有的默认构造函数,防止使用构造函数实例化;             private SingletonFactory(){                      }            //3.单例静态工厂方法,同步方式多线程同时执行;synchronized public  static SingletonFactory getInstance(){            if(_instance==null){                    _instance=new SingletonFactory();                        }                        retrun _instance;            }        //4.重写克隆函数public SingletonFactory  clone(){        return getInstance();                        }}    1.4  建造者模式    StringBuilderpublic class Client {    public static void main(String[]args){        Builder  builder = new  ConcreteBuilder();//接口buider,实现接口的具体建造者ConcreteBuilder,导演者Director,        Director director = new  Director(builder);        director.construct();        Product product =  builder.retrieveResult();        System.out.println(product.getPart1());        System.out.println(product.getPart2());    }}1.5 原型模式: 复制一个已经存在的实例来返回一个新的实例,而不是新建------------------------------------------------------------------------------------------------------------2.1   适配器模式:类的适配器:对类进行适配。A继承B,实现接口C。对象的适配器:对对象进行包装;A实现接口C,不继承B,包含一个B的对象;接口的适配器:对接口抽象化。A实现所有接口,为每个接口提供一个默认实现;缺省适配器模式:      (A extends AbstractB implements interfaceC,那么A即可以选择实现(@Override)接口interfaceC中的方法,也可以选择不实现;A即可以选择实现(@Override)抽象类AbstractB中的方法,也可以选择不实现)2.2  装饰器模式:(io流)一个接口A,一个接口A的实现类B,一个装饰器C。C实现了A,并且有一个私有的类型为A的成员,构造函数初始化它。适配器模式是将一个类A转换为另一个类B;装饰器模式是为一个类A增强新的功能,从而变成B;代理模式是为一个类A转换为操作类B;2.3  代理模式        Proxy实现接口Sourcable,含有一个Source对象2.4  外观模式 jdbc代理模式(一对一)发展而来的外观模式(一对多)客户端访问子系统中的各个类,高度耦合。中间加一个统一接口,降低耦合度,客户端访问该类,该类调用子系统中的各个类。2.5  桥接模式  jdbc客户端通过访问 桥 访问目标实现类,桥抽象化类包含目标接口类对象,可以赋值 目标接口类的实现类的实例化给他,通过桥调用不同的目标实现类对象。2.6 组合2.7 享元-----------------------------------------父类与子类之间:1.策略模式:【实现C中函数指针的功能,不同的比较策略】  (comparable & Comparator);2.模板方法模式两个类之间:3.观察者模式4.迭代子模式 (容器与容器遍历)5.责任链模式 (责任链模式) ( 串的替换---可以使用链式操作)6.命令模式类的状态:7.备忘录模式8.状态模式通过中间类:9.访问者模式10.中介模式11.解释器模式原文链接:https://blog.csdn.net/farphone/article/details/70324649
  • [技术干货] Java中常用的设计模式
    Java软件设计中的设计模式主要分为三大类:创建型模式、结构型模式和行为型模式。这些模式为解决常见设计问题提供了通用的解决方案,提升代码的可复用性、可维护性和可扩展性。  创建型模式 创建型模式(Creational Patterns)主要处理对象创建的方式,目的是将对象的创建与使用分离,并避免硬编码构造过程。通过使用创建型模式,可以提高代码的灵活性、可扩展性和可维护性。以下是五种常见的创建型模式的详细说明。  1. 单例模式(Singleton Pattern) 定义 确保一个类只有一个实例,并提供一个全局访问点来访问该实例。  动机 在某些情况下,整个应用程序中只需要一个类的实例,如日志管理器、数据库连接池、配置管理等。通过单例模式可以确保只有一个实例被创建,避免重复创建带来的资源浪费,并且全局访问点确保所有客户端都能访问到同一个实例。  结构 Singleton类:持有自己的静态实例,并提供一个静态方法来返回这个唯一实例。  构造方法:私有,防止外部类直接实例化对象。  示例 public class Singleton {     // 私有的静态实例     private static Singleton instance;          // 私有构造方法,防止外部实例化     private Singleton() {} ​     // 静态方法,提供全局访问点     public static Singleton getInstance() {         if (instance == null) {             instance = new Singleton();         }         return instance;     } } 应用场景 日志记录器  数据库连接池  配置管理器  优点 控制实例数量,节省资源  提供全局访问点  缺点 不支持多线程(如果不进行同步处理,可能会导致多个实例的创建)  单例对象难以扩展和测试(例如,在单元测试中需要使用多个不同实例时可能会有困难)  多线程改进(双重检查锁定) public class Singleton {     private static volatile Singleton instance;     private Singleton() {} ​     public static Singleton getInstance() {         if (instance == null) {             synchronized (Singleton.class) {                 if (instance == null) {                     instance = new Singleton();                 }             }         }         return instance;     } } 2. 工厂方法模式(Factory Method Pattern) 定义 定义一个用于创建对象的接口,但由子类决定具体要实例化的类。工厂方法让一个类的实例化延迟到子类。  动机 当代码中需要创建对象,但不想暴露具体类的构造过程,或者为了扩展方便,希望可以灵活地指定子类来实例化具体对象时,工厂方法模式提供了解决方案。  结构 抽象产品类:定义产品的接口。  具体产品类:实现抽象产品的接口,表示不同类型的产品。  抽象工厂类:定义一个抽象的工厂方法,返回抽象产品类型。  具体工厂类:实现工厂方法,返回具体的产品实例。  示例 // 产品接口 abstract class Product {     abstract void use(); } ​ // 具体产品类 class ConcreteProduct extends Product {     void use() {         System.out.println("Using ConcreteProduct");     } } ​ // 工厂接口 abstract class Creator {     abstract Product createProduct(); } ​ // 具体工厂类 class ConcreteCreator extends Creator {     Product createProduct() {         return new ConcreteProduct();     } }  应用场景 日志记录系统:根据不同的日志级别创建不同的日志处理器。  数据库访问层:使用不同的数据库(如MySQL、Oracle等)时,通过工厂方法灵活选择数据库驱动。  优点 客户端不需要知道具体产品的创建逻辑,只依赖工厂接口。  符合“开放-封闭”原则:可以通过子类扩展新产品,而无需修改现有代码。  缺点 增加了系统的复杂度,需要创建额外的工厂类。  3. 抽象工厂模式(Abstract Factory Pattern) 定义 提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。  动机 如果系统需要多个相互关联的对象,且不希望在客户端直接依赖具体类时,抽象工厂模式通过提供统一的接口,来生成相关的对象族,从而保证了对象之间的一致性和可扩展性。  结构 抽象工厂接口:定义创建一系列相关产品的接口。  具体工厂类:实现抽象工厂接口,负责生成具体的产品。  抽象产品接口:定义产品的通用接口。  具体产品类:实现抽象产品接口,表示不同的产品。  示例 // 抽象产品 interface Button {     void paint(); } ​ interface Checkbox {     void paint(); } ​ // 具体产品 class WinButton implements Button {     public void paint() {         System.out.println("Render a button in Windows style.");     } } ​ class MacButton implements Button {     public void paint() {         System.out.println("Render a button in MacOS style.");     } } ​ // 抽象工厂 interface GUIFactory {     Button createButton();     Checkbox createCheckbox(); } ​ // 具体工厂 class WinFactory implements GUIFactory {     public Button createButton() {         return new WinButton();     } ​     public Checkbox createCheckbox() {         return new WinCheckbox();     } } ​ class MacFactory implements GUIFactory {     public Button createButton() {         return new MacButton();     } ​     public Checkbox createCheckbox() {         return new MacCheckbox();     } }  应用场景 跨平台的GUI工具包:为不同操作系统(如Windows、Mac)创建风格一致的UI组件。  数据库访问:为不同数据库(如MySQL、PostgreSQL)生成一组相关的操作对象。  优点 客户端不需要直接实例化对象,易于扩展。  保证了相关产品之间的一致性。  缺点 代码复杂度增加,需要维护更多的类。  当需要扩展新产品族时,可能需要修改抽象工厂接口。  4. 建造者模式(Builder Pattern) 定义 将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示。  动机 有些对象的构建过程非常复杂,包含多个步骤(如配置文件的解析、复杂UI界面的生成)。建造者模式允许分步骤创建对象,且同样的构造过程可以构建不同的对象。  结构 Builder接口:定义构建复杂对象的步骤。  ConcreteBuilder类:实现Builder接口,完成具体步骤。  Director类:负责按顺序调用建造者的构建步骤。  Product类:复杂对象,由多个部分组成。  示例 // 产品类 class Product {     private String partA;     private String partB; ​     public void setPartA(String partA) {         this.partA = partA;     } ​     public void setPartB(String partB) {         this.partB = partB;     } ​     public void showProduct() {         System.out.println("Product with " + partA + " and " + partB);     } } ​ // 抽象建造者 interface Builder {     void buildPartA();     void buildPartB();     Product getResult(); } ​ // 具体建造者 class ConcreteBuilder implements Builder {     private Product product = new Product(); ​     public void buildPartA() {         product.setPartA("Part A");     } ​     public void buildPartB() {         product.setPartB("Part B");     } ​     public Product getResult() {         return product;     } } ​ // 指挥者 class Director {     public void construct(Builder builder) {         builder.buildPartA();         builder.buildPartB();     } }  应用场景 复杂对象的创建,如车辆、房屋、文档生成器。 当构造过程需要按照一定的顺序时,如餐厅订单的构建(前菜、主菜、甜点)。 优点 将构造过程分离,允许同样的构造过程创建不同的对象。 更加灵活,构造过程更容易扩展和维护。  缺点 增加了代码的复杂性,尤其是当产品本身并不复杂时。  5. 原型模式(Prototype Pattern) 定义 通过复制现有对象来创建新对象,而不是通过实例化类。  动机 有时创建对象的成本很高或者很复杂(如加载大量数据或依赖外部资源),直接克隆现有对象可以有效提升性能,避免冗余计算或复杂的初始化过程。  结构 Prototype接口:定义一个clone()方法,用于复制对象。  ConcretePrototype类:实现Prototype接口,负责具体的克隆操作。  示例 // 原型接口 interface Prototype {     Prototype clone(); } ​ // 具体原型 class ConcretePrototype implements Prototype {     private String state; ​     public ConcretePrototype(String state) {         this.state = state;     } ​     @Override     public Prototype clone() {         return new ConcretePrototype(state);     } }  结构型模式 结构型模式是设计模式中的一种类型,关注于如何将类或对象组合成更大的结构,以形成更复杂的功能。结构型模式帮助我们简化设计、提高系统的灵活性和可复用性,并有助于在不同的类之间建立良好的关系。  以下是几种常见的结构型模式的详细说明:  1. 适配器模式(Adapter Pattern) 定义 适配器模式允许将一个类的接口转换成客户端所期望的另一种接口,使得原本因接口不兼容而无法一起工作的类可以一起工作。 动机 在系统需要与多个接口不兼容的类交互时,适配器模式提供了一种解决方案。  实现 适配器可以是类适配器或对象适配器。类适配器通过继承来实现,而对象适配器通过组合来实现。  示例 // 目标接口 interface Target {     void request(); } ​ // 适配者类 class Adaptee {     void specificRequest() {         System.out.println("Called specificRequest");     } } ​ // 适配器类 class Adapter implements Target {     private Adaptee adaptee; ​     public Adapter(Adaptee adaptee) {         this.adaptee = adaptee;     } ​     @Override     public void request() {         adaptee.specificRequest();     } }  优点 使得接口不兼容的类可以协同工作。 提高系统的可复用性和灵活性。  缺点 可能会增加系统的复杂性,过多的适配器会导致代码难以理解。  2. 桥接模式(Bridge Pattern) 定义 桥接模式通过将抽象部分与其实现部分分离,从而使它们可以独立变化。即在抽象类和具体实现之间引入一个桥接接口。 动机 当一个类的抽象和实现之间有多个变化维度时,桥接模式能够将这些变化分离,减少系统的复杂性。  结构 抽象类:定义了高层接口。 实现类:实现了低层接口。 桥接:将抽象类与实现类连接。 示例 // 实现接口 interface Implementor {     void operationImpl(); } ​ // 具体实现 class ConcreteImplementorA implements Implementor {     @Override     public void operationImpl() {         System.out.println("ConcreteImplementorA operation");     } } ​ // 抽象类 abstract class Abstraction {     protected Implementor implementor; ​     public Abstraction(Implementor implementor) {         this.implementor = implementor;     } ​     public abstract void operation(); } ​ // 具体抽象类 class RefinedAbstraction extends Abstraction {     public RefinedAbstraction(Implementor implementor) {         super(implementor);     } ​     @Override     public void operation() {         implementor.operationImpl();     } }  优点 通过分离抽象和实现,提高了系统的灵活性。 可以在不改变抽象和实现的情况下扩展系统。 缺点 可能会增加系统的复杂性,特别是在类的数量较多时。  3. 组合模式(Composite Pattern) 定义 组合模式将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式允许客户端以统一的方式对待单个对象和组合对象。 动机 当需要表示对象的部分和整体的关系时,组合模式提供了一种简单的方法来管理这些对象。 结构 组件:定义了叶子和组合对象的公共接口。 叶子:实现了组件接口,表示树的叶子节点。 组合:实现了组件接口,表示树的组合节点。 示例 // 组件接口 interface Component {     void operation(); } ​ // 叶子类 class Leaf implements Component {     @Override     public void operation() {         System.out.println("Leaf operation");     } } ​ // 组合类 class Composite implements Component {     private List<Component> children = new ArrayList<>(); ​     public void add(Component component) {         children.add(component);     } ​     @Override     public void operation() {         for (Component child : children) {             child.operation();         }     } }  优点 客户端可以一致地使用单个对象和组合对象。 可以方便地添加新的叶子或组合。  缺点 设计上比较复杂,可能会引入不必要的复杂性。  4. 装饰模式(Decorator Pattern) 定义 装饰模式允许在不改变对象自身的情况下,动态地给一个对象添加一些额外的职责。装饰模式提供了比子类更灵活的替代方案。 动机 当需要在运行时扩展对象的功能时,装饰模式提供了一种灵活的方法。  结构 组件接口:定义了对象的接口。 具体组件:实现了组件接口的基本对象。 装饰者抽象类:实现了组件接口,持有一个组件的引用。 具体装饰者:扩展了装饰者抽象类,添加额外的职责。 ————————————————  原文链接:https://blog.csdn.net/u012108607/article/details/142852438 
总条数:764 到第
上滑加载中