《Java并发编程实战》学习笔记--同步容器类

我们平时使用的一些容器,例如ArrayList其实不是线程安全的。如果我们在多线程的环境之下在没有保证线程安全的情况之下使用它们,就有可能会发生意想不到的错误。那我们该如何解决这个问题呢?别着急,Java自早期开始,就为我们提供了同步容器类:

  • VectorHashtable以及继承自Vector的Stack
  • Collections.synchronizedXxx等工厂方法创建的类。

那么它们是如何实现线程安全的呢?
很简单,这些同步容器类将它们所有的成员变量都设为私有的(进行状态封装),并且对每个公有方法都进行同步(在方法头部使用Synchronized进行声明)从而实现每一次只有一个线程能够访问该同步容器类的实例。

Vector类中的某几个公有方法

既然如此,那么在使用这些同步容器类的时候是不是就高枕无忧,万事大吉了呢?

让我们来看看下面这两个程序:

  public static Object getLast(Vector list){
      int lastIndex = list.size() - 1;
      return list.get(lastIndex);
  }
  
  public static void deleteLast(Vector list){
      int lastIndex = list.size() - 1;
      list.remove(lastIndex);
  }

上面两个方法看起来没有一点问题,它们都会执行“先检查再运行”的操作。每个方法都是先获得数组的大小,然后通过结果来获取或者删除最后一个元素。表面上看起来无论多少个线程同时调用它们,也不会破坏Vector。但从调用者的角度来看,情况就不同了:

交替调用getList和deleteList时将抛出ArrayIndexOutOfBoundsException

如果线程A在包含10个元素的Vector上调用getLast,同时线程B在此Vector上调用deleteLast,这些操作的交替执行如上图所示。getLast将抛出ArrayIndexOutOffBoundsException异常。在调用size与调用getLast这两个操作之间,Vector变小了,因此在调用size时得到的索引值将不再有效。

虽然这种情况很好地遵循了Vector的规范:如果请求一个不存在的元素,那么将抛出一个异常。但这并不是Vector调用者所期望的(即使在并发修改的情况下也不希望看到),除非Vector一开始就是空的。

我们可以使用同步策略,即使用客户端加锁来保证操作的原子性:

  public static Object getLast(Vector list){
      synchronized(this){
      int lastIndex = list.size() - 1;
      return list.get(lastIndex);
      }
  }
  
  public static void deleteLast(Vector list){
    synchronized(this){
      int lastIndex = list.size() - 1;
      list.remove(lastIndex);
    }
  }

类似的还有下面这个例子:

在调用size和相应的get之间,Vector的长度可能发生变化,这种风险在对Vector中的元素进行迭代时仍然会出现。

for(int i = 0 ;i < vector.size(); i++){
    doSome(vector.get(i));
}

这种迭代方法的正确性完全依赖于运气:我们无法保证在调用size与get直接按有没有其他线程对所操作的这个Vector进行了修改。但是这并不代表Vector就不是线程安全的。Vector仍然是线程安全的,而抛出的异常也与其规范保持一致。然而,像读取最后一个或者迭代等这样简单的操作中抛出异常并不是我们所期待的。

改进方法:

synchronized(vector){
for(int i = 0 ;i < vector.size(); i++){
    doSome(vector.get(i));
    }
}
迭代器与ConcurrentModificationException

Vector是一个“古老”的容器类。然而,许多“现代”的容器类也没有消除复合操作中的问题。无论在直接迭代还是使用for-each循环语法中,对容器类进行迭代的标准方式都是使用Iterator。然而,如果有其他线程并发地修改容器,那么即使是使用迭代器也无法避免在迭代期间对容器进行加锁。许多同步容器类在被设计的时候并没有考虑到被并发修改的问题,它们所表现出的行为是****“及时失败”(fail - fast)****的。具体的可以参考我关于ArrayList源码的博客,里面有对用于及时失败机制中modCount的介绍

我们并不希望出现并发修改的问题,同时也不希望在迭代的过程中对容器进行加锁 -- 因为持有两个锁可能会导致死锁的问题,并且持有锁的时间过长,那么在锁上的竞争就会非常激烈,从而将极大降低吞吐量以及CPU的利用率。

如果不希望在迭代的过程中加锁,那么一种替代的方法就是对容器进行克隆,并在副本上进行迭代。副本将被封闭在线程内部,因此其他线程不会在迭代期间对其进行修改。这样就避免抛出ConcurrentModificationException(但是在克隆容器的过程中仍需要对容器进行加锁)。但是在克隆容器的过程中存在着显著的性能开销。这种方式的好坏取决于多个因素:容器的大小、在每个元素上执行的工作、迭代操作相对于容器其他操作的调用频率以及在响应时间和吞吐量等方面的需求。

隐藏的迭代器

我们看看下面这个程序:

public class HiddenIterator{
    @GuardedBy(this)
    private final Set<Integer> set = new HashSet<Integer>();
    
    public synchronized void add(Integer i ){ set.add(i); }
    public synchronized void remove(Integer i ){ set.remove(i); }
    
    public void addTenThings(){
        Random r = new Random();
        for(int i = 0 ; i < 10; i++)
            add(r.nextInt());
        System.out.println("DEBUG : added ten elements to" + set);
    }
}

表面上看起来十分的安全,add和remove两个方法都加上了锁。但是其实这里面隐藏了对容器的迭代操作:编译器将字符串的连接操作转换为调用StringBuilder.append(Object),而这个方法又会调用容器的toString方法,标准容器的toString方法将迭代容器,并在每个元素上调用toString来生成容器的格式化表示。

addTenThings可能会抛出ConcurrentModificationException,因为在生成调试消息的过程中,toString将对容器进行迭代。当然真正的问题在于HiddenIterator不是线程安全的。在使用println中的set之前必须首先获取HiddenIterator的锁。如果HiddenIterator用synchronizedSet来包装HashSet,并且对同步代码进行封装,那么就不会发生这种错误。

正如封装对象的状态有利于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略

其实,容器的hashCode和equals等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或者是键值时,就会出现这种情况。同样,containsAll、removeAll和retainAll等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代操作都会导致ConcurrentModificationException。

标准容器的toString方法将迭代容器
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容

  • 《Java并发编程实战》学习笔记--同步容器类前面介绍了同步容器类,下面来说说并发容器类。 同步容器类:将所有对容...
    EakonZhao阅读 1,893评论 0 11
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,221评论 11 349
  • 并发与多线程是每个人程序员都头疼的内容,幸好Java库所提供了丰富并发基础模块,这些多线程安全的模块作为并发工具类...
    登高且赋阅读 1,222评论 0 8
  • 我们总能听到关于高中取消分文理科的各路评论,其中很多人都在反对取消文理分科的改革,原因很明显嘛,因为你搞文理合二为...
    依旧艾斯兰阅读 438评论 0 4
  • 雨涵: 宝贝,夜又深了,你已在自己的小床上熟睡,昨晚你说啥也要挤在爸爸妈妈的床上,呵呵,其实我小时候也喜欢这样,当...
    方怡阅读 188评论 0 0