Java多线程理解:线程安全的集合对象


1、概念介绍

  • 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
  • 线程不安全就是不提供数据访问保护,多线程先后更改数据会产生数据不一致或者数据污染的情况。
  • 一般使用synchronized关键字加锁同步控制,来解决线程不安全问题。

2、线程安全的集合对象

  • ArrayList线程不安全,Vector线程安全;
  • HashMap线程不安全,HashTable线程安全;
  • StringBuilder线程不安全,StringBuffer线程安全。

3、代码测试

  • ArrayList线程不安全:
    在主线程中新建100个子线程,分别向ArrayList中添加100个元素,最后打印ArrayList的size。
public class Test {

  public static void main(String [] args){
      // 用来测试的List  
      List<String> data = new ArrayList<>();
      // 用来让主线程等待100个子线程执行完毕  
      CountDownLatch countDownLatch = new CountDownLatch(100);
      // 启动100个子线程  
      for(int i=0;i<100;i++){
          SampleTask task = new SampleTask(data,countDownLatch);
          Thread thread = new Thread(task);
          thread.start();
      }
      try{
          // 主线程等待所有子线程执行完成,再向下执行  
          countDownLatch.await();
      }catch (InterruptedException e){  
          e.printStackTrace();  
      } 
      // List的size  
      System.out.println(data.size());
  }
}
class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
        this.data = data;
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        // 每个线程向List中添加100个元素  
        for(int i = 0; i < 100; i++)  
        {  
            data.add("1");
        }  
        // 完成一个子线程  
        countDownLatch.countDown();
    }
}

7次测试输出():

9998
10000
10000
ArrayIndexOutOfBoundsException
10000
9967
9936
  • Vector线程安全:
    在主线程中新建100个子线程,分别向Vector中添加100个元素,最后打印Vector的size。
public class Test {

  public static void main(String [] args){
      // 用来测试的List  
      List<String> data = new Vector<>();
      // 用来让主线程等待100个子线程执行完毕  
      CountDownLatch countDownLatch = new CountDownLatch(100);
      // 启动100个子线程  
      for(int i=0;i<100;i++){
          SampleTask task = new SampleTask(data,countDownLatch);
          Thread thread = new Thread(task);
          thread.start();
      }
      try{
          // 主线程等待所有子线程执行完成,再向下执行  
          countDownLatch.await();
      }catch (InterruptedException e){  
          e.printStackTrace();  
      } 
      // List的size  
      System.out.println(data.size());
  }
}
class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
        this.data = data;
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        // 每个线程向List中添加100个元素  
        for(int i = 0; i < 100; i++)  
        {  
            data.add("1");
        }  
        // 完成一个子线程  
        countDownLatch.countDown();
    }
}

7次测试输出():

10000
10000
10000
10000
10000
10000
10000
  • 使用synchronized关键字来同步ArrayList:
public class Test {

  public static void main(String [] args){
      // 用来测试的List  
      List<String> data = new ArrayList<>();
      // 用来让主线程等待100个子线程执行完毕  
      CountDownLatch countDownLatch = new CountDownLatch(100);
      // 启动100个子线程  
      for(int i=0;i<100;i++){
          SampleTask task = new SampleTask(data,countDownLatch);
          Thread thread = new Thread(task);
          thread.start();
      }
      try{
          // 主线程等待所有子线程执行完成,再向下执行  
          countDownLatch.await();
      }catch (InterruptedException e){  
          e.printStackTrace();  
      } 
      // List的size  
      System.out.println(data.size());
  }
}
class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
        this.data = data;
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        // 每个线程向List中添加100个元素  
        for(int i = 0; i < 100; i++)  
        {  
            synchronized(data){
                data.add("1");
            }
        }  
        // 完成一个子线程  
        countDownLatch.countDown();
    }
}

7次测试输出():

10000
10000
10000
10000
10000
10000
10000

3、原因分析

  • ArrayList在添加一个元素的时候,它会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
    在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
    而如果是在多线程情况下,比如有两个线程,线程 A 先将元素1存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B向此 ArrayList 添加元素2,因为此时 Size 仍然等于 0 (注意,我们假设的是添加一个元素是要两个步骤,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值,结果Size都等于1。
    最后,ArrayList中期望的元素应该有2个,而实际元素是在0位置,造成丢失元素,故Size 等于1。导致“线程不安全”。
    ArrayList源码:
@Override public boolean add(E object) {
        Object[] a = array;
        int s = size;
        if (s == a.length) {
            Object[] newArray = new Object[s +
                    (s < (MIN_CAPACITY_INCREMENT / 2) ?
                     MIN_CAPACITY_INCREMENT : s >> 1)];
            System.arraycopy(a, 0, newArray, 0, s);
            array = a = newArray;
        }
        a[s] = object;
        size = s + 1;
        modCount++;
        return true;
    }
  • Vector的所有操作方法都被同步了,既然被同步了,多个线程就不可能同时访问vector中的数据,只能一个一个地访问,所以不会出现数据混乱的情况,所以是线程安全的。
    Vector源码:
@Override
    public synchronized boolean add(E object) {
        if (elementCount == elementData.length) {
            growByOne();
        }
        elementData[elementCount++] = object;
        modCount++;
        return true;
    }

4、线程安全的集合并不安全

分析以下场景:

synchronized(map){
Object value = map.get(key);
if(value == null)
{
    value = new Object();
    map.put(key,value);
}
return value;}

由于线程安全的集合对象是基于单个方法的同步,所以即使map是线程安全的,也会产生不同步现象。
在非单个方法的场景下,我们仍然需要使用synchronized加锁才能保证对象的同步。

代码测试:

public class Test {

  public static void main(String [] args){
      // 用来测试的List  
      List<String> data = new Vector<>();
      // 用来让主线程等待100个子线程执行完毕  
      CountDownLatch countDownLatch = new CountDownLatch(100);
      // 启动100个子线程  
      for(int i=0;i<1000;i++){
          SampleTask task = new SampleTask(data,countDownLatch);
          Thread thread = new Thread(task);
          thread.start();
      }
      try{
          // 主线程等待所有子线程执行完成,再向下执行  
          countDownLatch.await();
      }catch (InterruptedException e){  
          e.printStackTrace();  
      } 
      // List的size  
      System.out.println(data.size());
  }
}
class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
        this.data = data;
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        // 每个线程向List中添加100个元素  
        int size = data.size();
        data.add(size,"1"); 
        // 完成一个子线程  
        countDownLatch.countDown();
    }
}
997
993
995
996
997
998
997

5、总结

  • 如何取舍
    线程安全必须要使用synchronized关键字来同步控制,所以会导致性能的降低
    当不需要线程安全时,可以选择ArrayList,避免方法同步产生的开销;
    多个线程操作同一个对象时,可以选择线程安全的Vector;
  • 线程不安全!=不安全
    有人在使用过程中有一个不正确的观点:我的程序是多线程的,不能使用ArrayList要使用Vector,这样才安全。
    线程不安全并不是多线程环境下就不能使用
    注意线程不安全条件:多线程操作同一个对象。比如上述代码就是在多个线程操作同一个ArrayList对象。
    如果每个线程中new一个ArrayList,而这个ArrayList只在这一个线程中使用,那么是没问题的。
  • 线程‘安全’的集合对象
    较复杂的操作下,线程安全的集合对象也无法保证数据的同步,仍然需要我们来处理。

[2015-08]

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,510评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,142评论 11 349
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,032评论 0 62
  • 这被瓜分,被挤占,被压扁的时间水滴一样地被偷偷蒸发了,无数的分子挤成团团的云雾,有些粘在空中,有些跌落海里,有些徜...
    小陶李阅读 235评论 0 0
  • 这个世界,你没想到事情何其多。就好比现在是六月,一个月前我写下与我没什么关系的书包少年,一个月后我俩已经可以肩...
    丹青小跑前进阅读 586评论 0 2