15-同步容器

同步容器

为什么会出现同步容器

在Java的集合框架中,主要有四大类别:List、Set、Queue、Map。

List、Set、Queue接口分别继承了Collection接口,Map本身是一个接口。注意Collection和Map是一个顶层接口,而List、Set、Queue则继承了Collection接口,分别代表数组、集合和队列这三大类容器。像ArrayList、LinkedList都是实现了List接口,HashSet实现了Set接口,而Deque(双向队列:允许在队首、队尾进行入队和出队操作)继承了Queue接口,PriorityQueue实现了Queue接口。另外LinkedList(实际上是双向链表)实现了了Deque接口。

像ArrayList、LinkedList、HashMap这些容器都是非线程安全的。如果有多个线程并发地访问这些容器时,就会出现问题。因此,在编写程序时,必须要求程序员手动地在任何访问到这些容器的地方进行同步处理,这样导致在使用这些容器的时候非常地不方便。所以,Java提供了同步容器供用户使用。

java中的同步容器类

在Java中,同步容器主要包括2类:

1)Vector、Stack、HashTable

2)Collections工具类中提供的静态工厂方法创建的类

Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。

Stack也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类。

HashTable实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。

Collections是一个工具类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类:

 public static Collection synchronizedCollention(Collection c)
 public static List synchronizedList(list l)
 public static Map synchronizedMap(Map m)
 public static Set synchronizedSet(Set s)
 public static SortedMap synchronizedSortedMap(SortedMap sm)
 public static SortedSet synchronizedSortedSet(SortedSet ss)

Collections同步工具类

为了创建线程安全且由ArrayList支持的List,可以使用如下代码:

List list = Collection.synchronizedList(new ArrayList());

注意,ArrayList实例马上封装起来,不存在对未同步的ArrayList的直接引用(即直接封装匿名实例)。这是一种最安全的途径。如果另一个线程可以直接引用ArrayList实例,它可以执行非同步修改。

下面给出一段多线程中安全遍历集合元素的示例。我们使用Iterator逐个扫描List中的元素,在多线程环境中,当遍历当前集合中的元素时,一般希望阻止其他线程添加或删除元素。安全遍历的实现方法如下:

public class Test {
    
    public static void main(String[] args) {
        // 为了安全起见,仅使用同步列表的一个引用,这样可以确保控制了所有访问  
        // 集合必须同步化,这里是一个List  
        List<String> wordList = Collections.synchronizedList(new ArrayList<String>());  
  
        //wordList中的add方法是同步方法,会获取wordList实例的对象锁  
        wordList.add("Iterators");  
        wordList.add("require");  
        wordList.add("special");  
        wordList.add("handling");  
  
        // 获取wordList实例的对象锁,  
        // 迭代时,阻塞其他线程调用add或remove等方法修改元素  
        synchronized (wordList) {  
            Iterator<String> iter = wordList.iterator();  
            while (iter.hasNext()) {
                String s = iter.next();  
                System.out.println("found string: " + s + ", length=" + s.length());  
            }
        }
    }
}

同步容器的缺陷

从同步容器的具体实现源码可知,同步容器中的方法采用了synchronized进行同步,那么很显然,这必然会影响到执行性能,另外,同步容器就一定是真正地完全线程安全吗?不一定,这个在下面会讲到。

我们首先来看一下传统的非同步容器和同步容器的性能差异,我们以ArrayList和Vector为例:

性能问题

我们先通过一个例子看一下Vector和ArrayList在插入数据时性能上的差异:

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        Vector<Integer> vector = new Vector<Integer>();
        
        long start = System.currentTimeMillis();
        for(int i=0; i<100000; i++)
            list.add(i);
        long end = System.currentTimeMillis();
        System.out.println("ArrayList进行100000次插入操作耗时:" + (end - start) + "ms");
        
        start = System.currentTimeMillis();
        for(int i=0; i<100000; i++)
            vector.add(i);
        end = System.currentTimeMillis();
        System.out.println("Vector进行100000次插入操作耗时:" + (end - start) + "ms");
    }
}

执行结果:

image.png

进行同样多的插入操作,Vector的耗时是ArrayList的两倍。这只是其中的一方面性能问题上的反映。

另外,由于Vector中的add方法和get方法都进行了同步,因此,在有多个线程进行访问时,如果多个线程都只是进行读取操作,那么每个时刻就只能有一个线程进行读取,其他线程便只能等待,这些线程必须竞争同一把锁。

同步容器真的是安全的吗

也有人认为Vector中的方法都进行了同步处理,那么一定就是线程安全的,事实上这可不一定。看下面这段代码:

public class Test {
    
    public static void main(String[] args) {
        
        final Vector<Integer> vector = new Vector<Integer>();
        
        while(true) {
            for(int i=0; i<10; i++)
                vector.add(i);
            
            Thread thread1 = new Thread() {
                public void run() {
                    for(int i=0; i<vector.size(); i++)
                        vector.remove(i);
                };
            };
            Thread thread2 = new Thread() {
                public void run() {
                    for(int i=0; i<vector.size(); i++)
                        vector.get(i);
                };
            };
            
            thread1.start();
            thread2.start();
            
            while(Thread.activeCount() > 10)   {
                 
            }
        }
    }
}

执行结果:

image.png

正如大家所看到的,这段代码报错了:数组下标越界。

也许有朋友会问:Vector是线程安全的,为什么还会报这个错?很简单,对于Vector,虽然能保证每一个时刻只能有一个线程访问它,但是不排除这种可能:

当某个线程在某个时刻执行这句时:

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

假若此时vector的size方法返回的是10,i的值为9。然后另外一个线程执行了这句:

for(int i=0; i<vector.size(); i++)
    vector.remove(i);

将下标为9的元素删除了。那么通过get方法访问下标为9的元素肯定就会出问题了。

因此为了保证线程安全,必须在方法调用端做额外的同步措施,如下面所示:

public class Test {
    
    public static void main(String[] args) {
        
        final Vector<Integer> vector = new Vector<Integer>();
        
        while(true) {
            for(int i=0; i<10; i++)
                vector.add(i);
            
            Thread thread1 = new Thread() {
                public void run() {
                    synchronized (Test.class) { // 额外的同步
                        for(int i=0; i<vector.size(); i++)
                            vector.remove(i);
                    }
                };
            };
            Thread thread2 = new Thread() {
                public void run() { 
                    synchronized (Test.class) { // 额外的同步
                        for(int i=0; i<vector.size(); i++)
                            vector.get(i);
                    }
                };
            };
            
            thread1.start();
            thread2.start();
            
            while(Thread.activeCount() > 10)   {
                 
            }
        }
    }
}

这个例子也说明了:由两个原子操作组成的复合操作不再是原子操作,如果需要保持原子性,则需要进行额外的同步。

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,573评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,178评论 11 349
  • 1.骆驼猫 2.胸椎水平面旋转(躯干旋转臂不动,也是检查胸椎活动度方法,全幅度,左右旋幅度差异) 3.泡沫轴梳理肌...
    我是林瓜瓜阅读 5,779评论 0 0
  • 一只小狗 把它的童年安放在 女孩温软的胳肢窝 一根长长的狗尾巴草 成为它戏耍的伙伴 因此,我想起孩提时候 你信吗?...
    阿非_阅读 156评论 0 3
  • 初冬,一棵树站在风口 诗/刘锋 寒露,霜降,立冬 站在风口的那棵树不动声色 她只沿着节令的阶梯 一点点御妆,一点点...
    四川刘锋阅读 402评论 6 56