关于java:Java-多线程同步容器与并发容器

1次阅读

共计 3281 个字符,预计需要花费 9 分钟才能阅读完成。

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

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 返回的迭代用具有弱一致性,而并非“及时失败”。弱一致性的迭代器能够容忍并发的批改,当创立迭代器时会遍历已有的元素,并能够在迭代器被结构后将批改操作反映给容器。

正文完
 0