Java 多线程,同步容器与并发容器

码界逐光者
• 阅读 623

一、为什么这种方式不能实现线程安全性?
分析一段代码:

package com.guor.util;

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

public class ListHelper<E> {

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

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

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

public boolean putIfAbent(E x){

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

}

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

package com.guor.util;

import java.util.List;

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

private final List<T> list;

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

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

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

...

}

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

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

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

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

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

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
踩坑了!熬夜整理小米Android面试题
一、Java初中级面试题1.容器(HashMap、HashSet、LinkedList,HashSet等)2.内存模型3.JVM、Davilk、ART三者的原理和区别4.垃圾回收机制5.类加载方案6.说说你对Java反射的理解7.说说你对动态代理的理解8.什么是线程池,如何使用?为什么要使用线程池?9.在多线程运行过程中,解决安全性问题?10.设计模式(
Wesley13 Wesley13
3年前
Java中对jsonArray的排序,使用的是Gson
使用Gson对json解析字符串,转化为json对象.先上代码:下面是main方法里面的代码packagetestJava;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
Java使用线程池
多线程主函数packageUnitTest;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.;@SuppressWarnings("unchecked")public
Stella981 Stella981
3年前
Noark入门之线程模型
0x00单线程多进程单线程与单进程多线程的目的都是想尽可能的利用CPU,减少CPU的空闲时间,特别是多核环境,今天咱不做深度解读,跳过...0x01线程池锁最早的一部分游戏服务器是采用线程池的方式来处理玩家的业务请求,以达最大限度的利用多核优势来提高处理业务能力。但线程池同时也带来了并发问题,为了解决同一玩家多个业务请求不被
Wesley13 Wesley13
3年前
Java中的多线程
线程安全和同步线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:voidtransferMoney(U
Wesley13 Wesley13
3年前
JUC
Java5.0在java.util.concurrent包中提供了多种并发容器类来改进同步容器的性能。CountDownLatch一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一
Wesley13 Wesley13
3年前
2万字Java并发编程面试题整理(含答案,建议收藏)
Java并发编程1、在java中守护线程和本地线程区别?2、线程与进程的区别?3、什么是多线程中的上下文切换?4、死锁与活锁的区别,死锁与饥饿的区别?5、Java中用到的线程调度算法是什么?6、什么是线程组,为什么在Java中不推荐使用?7、为什么使用Executor框架?8、在Java
Wesley13 Wesley13
3年前
Java线程与多线程
1线程与多线程1.1线程是什么?线程(Thread)是一个对象(Object)。用来干什么?Java线程(也称JVM线程)是Java进程内允许多个同时进行的任务。该进程内并发的任务成为线程(Thread),一个进程里至少一个线程。Java程序采用多线程方式来支持大量的并发请求处理,程序如果在