精华内容
下载资源
问答
  • java多线程中并发集合和同步集合有哪些? Java多线程之同步集合和并发集合 另一个介绍 hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装: 包装类Collections.synchronizedMap()...

    java多线程中并发集合和同步集合有哪些?

    Java多线程之同步集合和并发集合
    另一个介绍

    hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装:

    包装类Collections.synchronizedMap()和Collections.synchronizedList()提供了一个基本的有条件的线程安全的Map和List实现。

    ConcurrentHashMap和Hashtable之间的区别
    那么Hashtable和ConcurrentHashMap之间的区别是什么,可以在多线程环境中使用,但一旦Hashtable的大小变得相当大的性能降低,因为迭代它必须被锁定更长的时间。

    由于ConcurrentHashMap引入了分段的概念,所以它只有一部分被锁定才能提供线程安全性.

    总而言之,ConcurrentHashMap仅锁定Map的某些部分,而Hashtable在执行迭代时锁定完整映射。

    ConcurrentHashMap和Collections.synchronizedMap之间的区别
    ConcurrentHashMap旨在实现并发性能,提高性能,而通过使用synchronized Map应用包装器,可以同步自然不同步的HashMap。

    以下是ConcurrentHashMap和Java中的同步映射之间的一些常见区别
    ConcurrentHashMap 不允许空值或空值同步,但是HashMap允许一个空键。

    Synchronized vs Concurrent Collections
    不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他们并发的实现(比如:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。比如ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。

    同样的,CopyOnWriteArrayList 允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。

    如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。

    顺便说一下,集合类是Java API的核心,所以我觉得明智而审慎地使用它们是一门艺术。这是我的个人经验,我通过使用ArrayList替换遗留代码里使用的Vector来提高性能。JDK 1.5引入一些很好的并发集合能高效地开发高容量,低延迟的Java应用程序。

    Synchronized Collections vs Concurrent Collections in Java
    同步集合类,Hashtable 和 Vector 还有同步集合包装类,Collections.synchronizedMap()和Collections.synchronizedList() 提供了一个基本的有条件的线程安全的Map和List的实现。

    所以Hashtable和ConcurrentHashMap的区别是什么,他们都可以在多线程环境中使用,但一旦Hashtable 的大小变得相当大的时候,其性能会降低,因为迭代的时候会被长时间锁定。

    然而有一些因素导致他们不适合在高并发应用程序中使用,最主要的原因是在他们的‘集合范围’的锁是程序可伸缩性的一个阻碍。它往往成为必须在相当长一段时间内对集合加锁以防止在迭代期间出现ConcurrentModificationException。

    ConcurrentHashMap和CopyOnWriteArrayList不一定在所有地方都是有用的,一般情况下你只需要使用HashMap或ArrayList。

    因为ConcurrentHashMap 引入了分片的概念,所以无论集合有多大,她都只对特定的片段进行加锁以保障线程安全,其他的读线程仍然可以访问map而不用等待正在访问的线程遍历结束。

    java.util.concurrent包中包含的并发集合类如下:

    ConcurrentHashMap

    CopyOnWriteArrayList

    CopyOnWriteArraySet

    展开全文
  • java多线程中并发集合和同步集合有哪些? hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装: 包装类Collections.synchronizedMap()和Collections.synchronizedList()提供了...

    java多线程中并发集合和同步集合有哪些?

     

       hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装:

           包装类Collections.synchronizedMap()和Collections.synchronizedList()提供了一个基本的有条件的线程安全的Map和List实现。

       

    ConcurrentHashMap和Hashtable之间的区别

    那么Hashtable和ConcurrentHashMap之间的区别是什么,可以在多线程环境中使用,但一旦Hashtable的大小变得相当大的性能降低,因为迭代它必须被锁定更长的时间。

    由于ConcurrentHashMap引入了分段的概念,所以它只有一部分被锁定才能提供线程安全性. 

    总而言之,ConcurrentHashMap仅锁定Map的某些部分,而Hashtable在执行迭代时锁定完整映射。 
     

    ConcurrentHashMap和Collections.synchronizedMap之间的区别

    ConcurrentHashMap旨在实现并发性能,提高性能,而通过使用synchronized Map应用包装器,可以同步自然不同步的HashMap。
     
    以下是ConcurrentHashMap和Java中的同步映射之间的一些常见区别
        ConcurrentHashMap 不允许空值或空值同步,但是HashMap允许一个空键。
     
     

    Synchronized vs Concurrent Collections

    不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能可扩展性,还有他们如何实现的线程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他们并发的实现(比如:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)会慢得多。造成如此慢的主要原因是, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。比如ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。

    同样的,CopyOnWriteArrayList 允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。

    如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。

    顺便说一下,集合类是Java API的核心,所以我觉得明智而审慎地使用它们是一门艺术。这是我的个人经验,我通过使用ArrayList替换遗留代码里使用的Vector来提高性能。JDK 1.5引入一些很好的并发集合能高效地开发高容量,低延迟的Java应用程序。

    Synchronized Collections vs Concurrent Collections in Java

    同步集合类,Hashtable 和 Vector 还有同步集合包装类,Collections.synchronizedMap()和Collections.synchronizedList() 提供了一个基本的有条件的线程安全的Map和List的实现。

    所以Hashtable和ConcurrentHashMap的区别是什么,他们都可以在多线程环境中使用,但一旦Hashtable 的大小变得相当大的时候,其性能会降低,因为迭代的时候会被长时间锁定。

    然而有一些因素导致他们不适合在高并发应用程序中使用,最主要的原因是在他们的‘集合范围’的锁是程序可伸缩性的一个阻碍。它往往成为必须在相当长一段时间内对集合加锁以防止在迭代期间出现ConcurrentModificationException。

     

    ConcurrentHashMap和CopyOnWriteArrayList不一定在所有地方都是有用的,一般情况下你只需要使用HashMap或ArrayList。

    因为ConcurrentHashMap 引入了分片的概念,所以无论集合有多大,她都只对特定的片段进行加锁以保障线程安全,其他的读线程仍然可以访问map而不用等待正在访问的线程遍历结束。

     

    java.util.concurrent包中包含的并发集合类如下:

       ConcurrentHashMap

       CopyOnWriteArrayList

       CopyOnWriteArraySet

    展开全文
  • 并发编程-同步集合和并发集合java并发编程volatile互斥锁sychronized公平锁/非公平锁可重入锁独享锁/共享锁乐观锁/悲观锁偏向锁/轻量级锁/重量级锁自旋锁锁消除同步集合和并发集合性能比较并发集合的实现原理并发...

    java并发编程

    三种性质

    • 可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存可导致可见性问题
    • 原子性:一个或多个CPU执行操作不被中断。线程切换可导致原子性问题
    • 有序性:编译器优化可能导致指令顺序发生改变。编译器优化可能导致有序性问题。

    三个问题

    • 安全性问题:线程安全
    • 活跃性问题:死锁、活锁、饥饿
    • 性能问题:
      使用无锁结构:TLS线程局部存储,Copy-On-Write,乐观锁;Java的原子类,Disruptor无锁队列
      减少锁的持有时间:让锁细粒度。如ConcurrentHashmap;再如读写锁,读无锁写有锁

    volatile

    C语言中的原意:禁用CPU缓存,从内存中读出和写入。Java语言的引申义:Java会将变量立刻写入内存,其他线程读取时直接从内存读(普通变量改变后,什么时候写入内存是不一定的)、禁止指令重排序

    解决问题:

    • 保证可见性
    • 保证有序性
    • 不能保证原子性

    是一种轻量级的线程安全处理机制

    互斥锁sychronized

    锁对象:非静态this,静态Class,括号Object参数

    预防死锁:
    互斥:不能破坏
    占有且等待:同时申请所有资源
    不可抢占:sychronized解决不了,Lock可以解决
    循环等待:给资源设置id字段,每次都是按顺序申请锁
    等待通知机制:wait、notify、notifyAll

    重要说明
    在JDK 1.6之前,synchronized是重量级锁,效率低下
    从JDK 1.6开始,synchronized 做了很多优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销
    synchronized 同步锁一共包含四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。synchronized 同步锁可以升级但是不可以降级,目的是为了提高获取锁和释放锁的效率

    synchronized 修饰的代码块: 通过反编译.class文件,通过查看字节码可以得到:在代码块中使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指明同步代码块的结束位置

    synchronized 修饰的方法:查看字节码可以得到:在同步方法中会包含 ACC_SYNCHRONIZED 标记符。该标记符指明了该方法是一个同步方法,从而执行相应的同步调用

    公平锁/非公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁。

    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

    对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
    在这里插入图片描述

    对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁

    可重入锁

    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取该锁。

    对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。

    对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
    特殊的是ReentrantReadWriteLock可重入读写锁:
    读锁属于共享锁,写锁属于独占锁
    一个线程持有读锁获取写锁时互斥
    持有写可以获取读
    ReentrantReadWriteLock是Lock的另一种实现方式,ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。

    编程实现:
    //创建重入读写锁对象,其中可以引入参数boolean用于设定是否使用公平锁,默认非公平锁
    private static final ReentrantReadWriteLock lock=new ReentrantReadWriteLock();

    lock.readLock().lock();//申请读锁
    lock.readLock().unlock();//释放所拥有的读锁,如果没有拥有读锁,则释放时报异常IllegalMonitorStateException

    //申请获取写锁
    lock.writeLock().lock();//由于写锁属于独占锁,所以必须释放读锁后才能申请
    lock.writeLock().unlock();

    lock.getHoldCount():int获取所拥有的锁个数

    在一个线程中读锁和读锁、写锁和写锁不互斥----可重入的概念,在一个线程中持有读不能申请写,但是持有写可以申请读

    独享锁/共享锁

    独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
    对于Java ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

    读锁的共享锁可保证并发读是非常高效的,多线程中读写\写读\写写的过程是互斥的。

    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
    对于synchronized而言,当然是独享锁。

    乐观锁/悲观锁

    乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
    悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
    乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 -CAS

    悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
    悲观锁在Java中的使用,就是利用各种锁。
    乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

    偏向锁/轻量级锁/重量级锁

    这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    自旋锁

    在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

    自旋锁的本质:执行几个空方法,稍微等一等,也许是一段时间的循环,也许是几行空的汇编指令。

    tryLock 是防止自锁的一个重要方式。
    tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

    while(!lock.tryLock()){ // lock.lock();
    //计算1+2+…+1000=?
    }

    锁消除

    即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,依据来源于逃逸分析的数据支持,那么是什么是逃逸分析?对于虚拟机来说需要使用数据流分析来确定是否消除变量底层框架的同步代码,因为有许多同步的代码不是自己写的

    同步集合和并发集合

    同步集合类:Hashtable、Vector 方法上有同步约束 (jdk1.0)

    同步集合包装类:Collections.synchronizedMap(new HashMap<>())和
    Collections.synchronizedList(new ArrayList<>()) —使用的是全局锁

    并发集合类:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteHashSet

    性能比较

    同步集合比并发集合会慢得多,主要原因是锁,同步集合会对整个Map或List加锁

    并发集合的实现原理

    ConcurrentHashMap[jdk1.7]把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。

    CopyOnWriteArrayList允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。

    并发集合的使用建议

    一般不需要多线程的情况,只用到HashMap、ArrayList,只要真正用到多线程的时候就一定要考虑同步。所以这时候才需要考虑同步集合或并发集合

    解决方法1: 给整个集合上添加一个锁

      List<Integer> list=Collections.synchronizedList(new ArrayList<>());
    

    解决方法2:适用于读多写少的场景下

      List<Integer> list=new CopyOnWriteArrayList<>();
    

    ConcurrentHashMap实现原理

    ConcurrentHashMap (JDK1.7) 是由Segment数组结构和HashEntry数组结构组成。

    Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。

    一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁

    1.7的具体存储结构
    在这里插入图片描述

    JDK1.7版本的CurrentHashMap的实现原理

    在JDK1.7中ConcurrentHashMap采用了【数组+Segment分段锁】的方式实现。
    1、Segment(分段锁) ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
    2.内部结构。 ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

    坏处:这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长

    好处:写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。所以通过这种结构,ConcurrentHashMap的并发能力可以大大的提高。
    在这里插入图片描述

    JDK8中ConcurrentHashMap采用了【数组+链表+红黑树】的实现方式来设计,内部大量采用CAS操作。

    JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。

    Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。

    在JDK8中ConcurrentHashMap的结构,由于引入了红黑树,使得ConcurrentHashMap的实现非常复杂,红黑树是一种性能非常好的二叉查找树,其查找性能为O(log2N),但是其实现过程也非常复杂,而且可读性也非常差,早期完全采用链表结构时Map的查找时间复杂度为O(N),JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。

    CAS是compare and swap的缩写,即比较交换。cas是一种基于锁的操作,而且是乐观锁。

    在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
    CAS操作包含三个操作数 —— 内存位置V、预期原值A和新值B。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    总结

    数据结构:1.8取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。

    保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全

    锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁Node

    链表转化为红黑树:链表定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储

    查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(log2N)。

    CopyOnWrite容器

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    CopyOnWriteArrayList的实现原理

    可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList

    CopyOnWrite的应用场景

    CopyOnWrite并发容器用于读多写少的并发场景。

    比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索

    注意两点:
    减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销

    使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。
    不要按照原理进行开发,建议内部使用CopyOnWriteArrayList

    CopyOnWrite的缺点

    1、内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

    针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

    2、数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

    展开全文
  • 并发编程-同步集合和并发集合java并发编程volatile互斥锁sychronized公平锁/非公平锁可重入锁独享锁/共享锁乐观锁/悲观锁偏向锁/轻量级锁/重量级锁自旋锁锁消除同步集合和并发集合性能比较并发集合的实现原理并发...

    java并发编程

    三种性质

    • 可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存可导致可见性问题
    • 原子性:一个或多个CPU执行操作不被中断。线程切换可导致原子性问题
    • 有序性:编译器优化可能导致指令顺序发生改变。编译器优化可能导致有序性问题。

    三个问题

    • 安全性问题:线程安全
    • 活跃性问题:死锁、活锁、饥饿
    • 性能问题:
      使用无锁结构:TLS线程局部存储,Copy-On-Write,乐观锁;Java的原子类,Disruptor无锁队列
      减少锁的持有时间:让锁细粒度。如ConcurrentHashmap;再如读写锁,读无锁写有锁

    volatile

    C语言中的原意:禁用CPU缓存,从内存中读出和写入。Java语言的引申义:Java会将变量立刻写入内存,其他线程读取时直接从内存读(普通变量改变后,什么时候写入内存是不一定的)、禁止指令重排序

    解决问题:

    • 保证可见性
    • 保证有序性
    • 不能保证原子性

    是一种轻量级的线程安全处理机制

    互斥锁sychronized

    锁对象:非静态this,静态Class,括号Object参数

    预防死锁:
    互斥:不能破坏
    占有且等待:同时申请所有资源
    不可抢占:sychronized解决不了,Lock可以解决
    循环等待:给资源设置id字段,每次都是按顺序申请锁
    等待通知机制:wait、notify、notifyAll

    重要说明
    在JDK 1.6之前,synchronized是重量级锁,效率低下
    从JDK 1.6开始,synchronized 做了很多优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销
    synchronized 同步锁一共包含四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。synchronized 同步锁可以升级但是不可以降级,目的是为了提高获取锁和释放锁的效率

    synchronized 修饰的代码块: 通过反编译.class文件,通过查看字节码可以得到:在代码块中使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指明同步代码块的结束位置

    synchronized 修饰的方法:查看字节码可以得到:在同步方法中会包含 ACC_SYNCHRONIZED 标记符。该标记符指明了该方法是一个同步方法,从而执行相应的同步调用

    公平锁/非公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁。

    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

    对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
    在这里插入图片描述

    对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁

    可重入锁

    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取该锁。

    对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。

    对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
    特殊的是ReentrantReadWriteLock可重入读写锁:
    读锁属于共享锁,写锁属于独占锁
    一个线程持有读锁获取写锁时互斥
    持有写可以获取读
    ReentrantReadWriteLock是Lock的另一种实现方式,ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。

    编程实现:
    //创建重入读写锁对象,其中可以引入参数boolean用于设定是否使用公平锁,默认非公平锁
    private static final ReentrantReadWriteLock lock=new ReentrantReadWriteLock();

    lock.readLock().lock();//申请读锁
    lock.readLock().unlock();//释放所拥有的读锁,如果没有拥有读锁,则释放时报异常IllegalMonitorStateException

    //申请获取写锁
    lock.writeLock().lock();//由于写锁属于独占锁,所以必须释放读锁后才能申请
    lock.writeLock().unlock();

    lock.getHoldCount():int获取所拥有的锁个数

    在一个线程中读锁和读锁、写锁和写锁不互斥----可重入的概念,在一个线程中持有读不能申请写,但是持有写可以申请读

    独享锁/共享锁

    独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
    对于Java ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

    读锁的共享锁可保证并发读是非常高效的,多线程中读写\写读\写写的过程是互斥的。

    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
    对于synchronized而言,当然是独享锁。

    乐观锁/悲观锁

    乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
    悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
    乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 -CAS

    悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
    悲观锁在Java中的使用,就是利用各种锁。
    乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

    偏向锁/轻量级锁/重量级锁

    这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    自旋锁

    在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

    自旋锁的本质:执行几个空方法,稍微等一等,也许是一段时间的循环,也许是几行空的汇编指令。

    tryLock 是防止自锁的一个重要方式。
    tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

    while(!lock.tryLock()){ // lock.lock();
    //计算1+2+…+1000=?
    }

    锁消除

    即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,依据来源于逃逸分析的数据支持,那么是什么是逃逸分析?对于虚拟机来说需要使用数据流分析来确定是否消除变量底层框架的同步代码,因为有许多同步的代码不是自己写的

    同步集合和并发集合

    同步集合类:Hashtable、Vector 方法上有同步约束 (jdk1.0)

    同步集合包装类:Collections.synchronizedMap(new HashMap<>())和
    Collections.synchronizedList(new ArrayList<>()) —使用的是全局锁

    并发集合类:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteHashSet

    性能比较

    同步集合比并发集合会慢得多,主要原因是锁,同步集合会对整个Map或List加锁

    并发集合的实现原理

    ConcurrentHashMap[jdk1.7]把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。

    CopyOnWriteArrayList允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。

    并发集合的使用建议

    一般不需要多线程的情况,只用到HashMap、ArrayList,只要真正用到多线程的时候就一定要考虑同步。所以这时候才需要考虑同步集合或并发集合

    解决方法1: 给整个集合上添加一个锁

      List<Integer> list=Collections.synchronizedList(new ArrayList<>());
    

    解决方法2:适用于读多写少的场景下

      List<Integer> list=new CopyOnWriteArrayList<>();
    

    ConcurrentHashMap实现原理

    ConcurrentHashMap (JDK1.7) 是由Segment数组结构和HashEntry数组结构组成。

    Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。

    一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁

    1.7的具体存储结构
    在这里插入图片描述

    JDK1.7版本的CurrentHashMap的实现原理

    在JDK1.7中ConcurrentHashMap采用了【数组+Segment分段锁】的方式实现。
    1、Segment(分段锁) ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
    2.内部结构。 ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

    坏处:这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长

    好处:写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。所以通过这种结构,ConcurrentHashMap的并发能力可以大大的提高。
    在这里插入图片描述

    JDK8中ConcurrentHashMap采用了【数组+链表+红黑树】的实现方式来设计,内部大量采用CAS操作。

    JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。

    Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。

    在JDK8中ConcurrentHashMap的结构,由于引入了红黑树,使得ConcurrentHashMap的实现非常复杂,红黑树是一种性能非常好的二叉查找树,其查找性能为O(log2N),但是其实现过程也非常复杂,而且可读性也非常差,早期完全采用链表结构时Map的查找时间复杂度为O(N),JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。

    CAS是compare and swap的缩写,即比较交换。cas是一种基于锁的操作,而且是乐观锁。

    在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
    CAS操作包含三个操作数 —— 内存位置V、预期原值A和新值B。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    总结

    数据结构:1.8取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。

    保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全

    锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁Node

    链表转化为红黑树:链表定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储

    查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(log2N)。

    CopyOnWrite容器

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    CopyOnWriteArrayList的实现原理

    可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList

    CopyOnWrite的应用场景

    CopyOnWrite并发容器用于读多写少的并发场景。

    比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索

    注意两点:
    减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销

    使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。
    不要按照原理进行开发,建议内部使用CopyOnWriteArrayList

    CopyOnWrite的缺点

    1、内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

    针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

    2、数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

    展开全文
  • 同步集合和并发集合

    2021-03-22 20:07:00
    同步集合和并发集合 同步集合类:Hashtable、Vector 方法上有同步约束 (jdk1.0) 同 步 集 合 包 装 类 : Collections.synchronizedMap(new HashMap<>()) 和Collections.synchronizedList(new ArrayList<&...
  • 并发集合
  • 同步集合和并发集合的区别在于性能和可扩展性,以及他们如何实现线程安全 同步集合类 HashTable Vector 同步集合包装类:Collections.synchronizedMap Collections.synchronizedList 其中HashTable相当于加了...
  • 一、并发集合 List(ArrayList|LinkedList)、Set(HashSet|TreeSet)、Map(HashMap|TreeMap)集合,这些集合只适合在单线程情况下使用。在Collecionts工具类中有synchronized开头方法可以把单线程集合转成支持...
  • Java多线程之同步集合和并发集合 不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全。 同步集合类 Hashtable Vector 同步集合包装类,...
  • Java多线程之同步集合和并发集合 不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全。 同步集合类 Hashtable Vector 同步集合...
  • 在Java中,有普通集合、同步的集合(即线程安全的集合)、并发集合并发集合常见的有ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等。并发集合位于java.util.concurrent包下,是在JDK1.5...
  • Synchronized vs Concurrent Collections不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能可扩展性,还有他们如何实现的线程安全。同步HashMap, Hashtable, HashSet, Vector, ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 886
精华内容 354
关键字:

并发集合和同步集合