问题一:线程安全问题的由来?
在JVM里面有一个工作内存和主内存,而每个线程都有自己的工作内存,当一个线程在对一个变量进行操作的时候,都要在自己的工作内存里对主内存进行拷贝一份(Load),然后再工作内存里对变量进行操作,操作完在把东西写回主内存(Save)。所以在多个线程对同一个变量进行操作时,就会出现线程安全问题,但是加上Synchronized关键字实际上就是建立一个monitor,然后通过monitor加锁来实现线程安全,只有当一个线程拿到这个monitor的时候才可以对这个变量或者方法进行操作。操作完后再加载到工作内存,然后再写回主内存,只有写回到主内存时才会释放锁。
问题二:线程安全对象和线程不安全对象有哪些?
线程安全:Vector,HashTable,StringBuffer
线程不安全:ArrayList,LinkedList,HashMap,HashSet,TreeMap,TreeSet,StringBuilder
问题三:如何保证容器是线程安全的?
在早期线程安全的集合是Vector和HashTable
Vector:他是长度可变的数组,和ArrayList一样。但是与ArrayList不一样的是,Vector是线程安全的。因为Vector几乎给所有的public方法上都加了synchronized关键字,因此由于加锁导致性能降低,在不需要并发访问时这种同步机制就显得多余。
HashTable:HashTable 与HashMap类似,但是HashTable是线程安全的它几乎给所有的public方法上都加了Synchronized关键字,而且hashTable 的key和value的值都不能为空
因为使用Synchronized加锁,粒度比较粗所以:,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:
具体保证线程安全的方式,包括有从简单的 synchronized 方式,到基于更加精细化的,比如基于分离锁实现的 ConcurrentHashMap 等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现
Hashtable :
HashMap
解决:使用ConcurrentHashMap来代替HashMap
解决: 在能预估存放元素个数的前提下传入适当的初始化参数来尽量避免resize()
小tips:在resize过程中若发现桶下的红黑树节点<=UNTREEIFY_THRESHOLD,会将红黑树解除树化还原为链表结构。
线程安全:使用ReentrantLock保证相应Segment下的线程安全
思路:通过锁细粒度化,将整表锁拆分为多个锁进行优化。
实现思路:
JDK7的ConcurrentHashMap:哈希表
将原先的16个桶设计改为16个Segment,每个Segment都有独立的一把锁。拆分后的每个Segment都相当于原先的一个HashMap(double-hash设计).并且Segment在初始化后无法扩容,每个Segment对应的哈希表可以扩容,扩容只扩容相应Segment下面的哈希表。Segment之间相互不影响
过程:先判断我在哪个segment下面,然后再hash一次判断我在哪个segment的哪个具体的桶里面,然后进行链表存储。
1.ConcurrentHashMap 会获入锁,以保证数据一致性,Segment 本身就是基于ReentrantLock 的扩展实现,所以,在并发修改期间,相应 Segment 是被锁定的
2.在最初阶段,进行重复性的扫描,以确定相应 key 值是否已经在数组里面,进而决定是更新还是放置操作。重复扫描、检测冲突是ConcurrentHashMap 的常见技巧
3.在 ConcurrentHashMap中扩容同样存在.不过有一个明显区别,就是它进行的不是整体的扩容,而是单独对 Segment 进行扩容
1.结构上的变化:
2.线程安全:
现版本的sychronized已经经过不断优化,性能上与ReentrantLock基本没有差异,
并且相对于ReentrantLock,使用Sychronized可以节省大量内存空间(原来ReentrantLock下的segment都得加入同步队列,都得继承AQS下的Node,而synchronized只是锁住头结点,头结点下边的节点都不会加入同步队列里,所以 节省了空间),这是非常大的优势所在。