同步容器类
同步容器类的问题
对于已经实现同步的容器类来说,这个类本身是安全的,然而在使用同步类的多个操作来进行一个混合操作,那么这个混合操作就会变得不安全了。所以要进行额外的加锁使其复合操作安全
迭代器与ConcurrentModificationException
对容器类的标准迭代方式都是使用迭代器,迭代器采用的是一种“及时失败”的策略,这便意味着当容器在迭代过程中被修改,那么就会抛出CME异常。这种及时失败的策略并不是一种完备的处理机制,而只是起到提醒的作用。然而,如果是在没有同步的情况下运行的,可能会看到失效的计数值(缺少可见性),而迭代器可能并没有意识到容器已经发生了修改。这其实是一种设计上的权衡,从而降低并发修改操作的监测代码对程序性能带来的影响。
CME异常的两种解决方案
- 在迭代的过程中加锁。当然这是一种非常粗鲁的办法。因为如果容器的规模很大,那么有些线程将长时间的等待,可能会存饥饿的风险。更可怕的是长时间的对容器进行加锁会降低程序的可伸缩性,那么将极大的降低吞吐量和Cpu的利用率。
- “克隆容器”。将容器进行克隆,并在副本上进行迭代。由于副本被封闭在线程内,因此其他线程不会对其进行修改。但是在克隆容器的过程中系统的开销是非常大的。
隐式的迭代器
上面的输出语句是将set集合中的所有数据输出,但是其隐含了一个迭代的过程,将set遍历输出。那么这就可能产生一个线程安全的问题。所以,我们从这里得到的教训是,如果状态与保护他的同步代码之间相隔越远,那么开发人员就越容易忘记在访问状态时使用正确的同步。(我的理解:访问某个状态时,状态需要同步的代码与调用该同步代码之间相隔远就越容易被程序员忽视。例如,syso输出容器,默认是调用容器的tostring方法,但是没有写出来,会让程序员误以为输出set是一个原子的操作方式)
定义:正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略(个人理解:如果在set集合的tostring方法中就实施了同步操作,那么在输出set集合的时候,就不在需要在客户端加锁,从而不存在该隐式的非线程安全操作,确保实施同步策略)
并发容器
在java5.0中提供了许多的并发容器来改进之前的同步容器的性能。同步容器是将对容器状态的访问变成了串行化。从而大大降低了程序的性能。通过并发容器来替代同步容器,可以极大的提高程序的可伸缩性并降低风险。
ConcurrenHashMap
该种容器是采用一种“分段锁”的机制,该种方式在多线程的情况下极大的挺高了程序的性能,在单线程的情况下可能会损耗非常小的性能。
CHM容器的迭代具有弱一致性,而并非及时失败。若一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以(不保证,因为没有去保证其可见性)在迭代器被构造后将修改操作反映给容器。像CHM容器中的size方法减弱了反应容器的并发特性(个人理解:减弱的原因可能是因为如果要获得map的总体大小,就要将各个分段的大小相加,而在相加各个分段的大小时,如果要保证并发特性,那么就要加一个总锁,防止各个分段上有新的更改,那么这将极大的损失性能)。所以通过size的得到的容器总大小实际上只是一个估计值,因而不是一个精确值。
CopyOnWriteArrayList
该容器在迭代期间不需要对容器进行加锁或者复制。“写入时复制”容器的安全性在于,只要正确的发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。(个人说明:通过观察该并发类的源码可以了解,在每次对容器中的内容进行更新操作(增删改)时,会将底层的容器进行复制得到一份副本,并对副本进行更新操作,然后将底层容器的引用指向副本。当时产生了一个疑惑:为什么不直接在底层的容器进行操作,底层的容器已经用volatile关键字标明了并且在进行更新操作都会使用ReentrantLock锁,应该会保证可见性和原子性。查阅了大量资料发现使用volatile关键字只是保证引用的可见性,当数组中的数据更新时并不会将更新结果反应给主存)。
使用环境:仅当迭代操作远远多余修改操作时,才应该使用写入时复制容器。类似于事件的监听
重点
- 伸缩性:指的是当用户量上升时,系统性能是否会被影响