基本语法
1. java访问权限控制
java中,针对类、成员方法和属性提供了四种访问级别,分别是private、default、protected和public。
- private(当前类访问级别):对于私有成员变量和方法,只有在本类中创建对象时,这个对象才能访问自己的私有成员变量和方法。
- default(包访问级别):类的成员变量和方法什么修饰词也没有,又叫包修饰符,只有类本身成员和当前包下的成员可以访问。
- protected(子类访问级别):用protected修饰的成员变量和方法能被该类的成员以及其子类成员访问,还可以被包内的其他类的成员访问。
- public(公共访问级别):被public修饰的成员能够被所有的类访问,不管访问类与被访问类是否在同一个包中。
| 修饰符\作用域 | 同一个类中 | 同一个包中 | 子类中 | 任何地方 |
|---|---|---|---|---|
| private | 可以 | |||
| default | 可以 | 可以 | ||
| protected | 可以 | 可以 | 可以 | |
| public | 可以 | 可以 | 可以 | 可以 |
2. ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组+链表+红黑树)的结构来存储元素。相比于同样线程安全的HashTable来说,效率等各方面都有极大的提升。
2.1 相关概念
-
synchronized:java中的关键字,内部实现为监视器锁,主要通过对象监视器在对象头中的字段来表明的。synchronized从旧版本到现在已经做了很多优化了,在运行时会有三种存在方式:偏向锁、轻量级锁和重量级锁。
- 偏向锁:是指一段同步代码一直被一个线程访问,那么这个线程会自动获取锁,降低获取锁的代价。
- 轻量级锁:是指当锁是偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。
- 重量级锁:是指当锁是轻量级锁时,当自旋的线程自旋了一定的次数后,还没有获得锁,就会进入阻塞状态,该锁升级为重量级锁,重量级锁会使其他线程阻塞,性能降低。
-
CAS(Compare And Swap):它是一种乐观锁,认为对于同一个数据的并发操作不一定会发生修改,在更新数据库的时候,尝试去更新数据,如果失败就不断尝试。
-
volatile(非锁):java中的关键字,当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
-
自旋锁:是指尝试获取锁的线程不会阻塞,而是循环的方式不断尝试,这样做的好处是减少线程的上下文切换带来的开锁,提高性能,缺点是循环会消耗CPU。
-
分段锁:是一种锁的设计思路,它细化了锁的粒度,主要运用在ConcurrentHashMap中,实现高效的并发操作,当操作不需要更新整个数组时,就只锁数组中的一项就可以了。
-
ReentrantLock:可重入锁,是指一个线程获取锁之后能够再次获得该对象的锁,可重入锁的优点是避免死锁(Test对象中有两个需要加锁的方法A和B,A中调用了B。如果我们先执行A(进行了加锁),A中调用B时。如果不是可重入锁,就发生A等待B执行完,B等待A释放锁的循环等待。可重入锁可以避免此类情况发生)。synchronized也是可重入锁。
2.2 定义
java中,HashMap是非安全的,如果想在多线程下安全的操作map,有以下几种解决方式:
- 使用Hashtable线程安全类;
- 使用Collections.synchronizedMap方法,对方法进行加同步锁;
- 使用并发包中的ConcurrentHashMap类。 HashTable是一个线程安全的类,HashTable几乎所有的添加、删除、查询方法都加了synchronized同步锁。相当于给整个哈希表加了一大把锁,多线程访问的时候,只要一个线程访问或操作该对象,那其他线程只能阻塞,等待需要的锁被释放,在竞争激烈的多线程场景中性能会非常差,所以HashTable不推荐使用。
Collections.synchronizedMap里面使用对象锁来保证多线程场景下,操作安全,本质上也是对HashMap进行全表锁。使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然非常差,所以不推荐使用。
ConcurrentHashMap类锁采用的是分段锁的思想,将HashMap进行切割,把HashMap中的哈希数组切分成小不同的Segment,它继承自ReentrantLock(可重入锁)。
对于ConcurrentHashMap,jdk1.8和jdk1.7有很大的不同。
jdk1.8对HashMap做了改造,当冲突链表长度大于8时,会将链表转换成红黑树结构。jdk1.8中的ConcurrentHashMap类取消了Segment分段锁,采用CAS+synchronized来保证并发安全,数据结构跟jdk1.8中的HashMap结构类似,都是数组+链表(当链表长度大于8时,链表结构转为红黑二叉树)结构。并且ConcurrentHashMap中synchronized只锁定当前链表或红黑二叉树的首节点,只要节点hash不冲突,就不会产生冲突,相比jdk1.7的ConcurrentHashMap效率又提升了N倍。
jdk1.7segment本身就相当于一个HashMap对象。一个ConcurrentHashMap集合中包含2的N次方个segment。原理是通过对每个Segment持有一把锁,保证线程安全的同时降低了锁的粒度,让并发操作效率更高。 jdk1.7中的ConcurrentHashMap并发读写的几种情况:
- 不同的Segment的并发写入(可以同时进行)
- 同一Segment的一读一写(可以并发执行)
- 同一Segment的并发写入(阻塞)
jdk1.7的ConcurrentHashMap读写的过程:
- get方法:为输入的key计算hash值;通过hash值定位到segment对象;在此通过hash值,定位到segment当中的具体位置。
- put方法:为key计算hash值;通过hash值定位到对应的segment对象;获得可重入锁(segment继承可重入锁);再次通过hash值定位到数组中的具体位置;插入或覆盖HashEntry对象;释放锁;
jdk1.8相比于jdk1.7中的ConcurrentHashMap采用了链表和红黑树结合的技术(冲突的链表长度大于8时,会将链表转化为红黑二叉树结构)。相比于jdk1.7的使用分段锁Segment,jdk1.8采用了CAS+synchronized保证并发的安全性。
jdk1.8中对节点Node类中的共享变量,和jdk1.7一样,使用volatile关键字,保证多线程操作时,变量的可见性。
- put方法:判断key、value是否为空,如果为空就抛异常;接着会判断容器数组是否为空,如果为空就初始化数组;进一步判断要插入的f,在当前数组下标是否第一次插入,如果是就通过CAS方式插入;再接着判断f.hash==-1是否成立,如果成立,说明当前f是ForwardingNode节点,表示其他线程正在扩容,则一起进行扩容操作;其他的情况,就是把新的Node节点按链表或红黑树的方式插入到合适的位置;节点插入完成后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;最后,插入完成之后,进行扩容判断。
- initTable初始化数组:使用CAS锁控制只有一个线程初始化tab数组;sizeCtl在初始化后存储的是扩容门槛;扩容门槛写死的是数组tab数组大小的0.75倍,tab数组大小即map的容量,也就是最多存储多少个元素。
- helpTransfer协助扩容:对table、node节点、node节点的nextTable,进行数据校验;根据数组的length得到一个表示符号;进一步校验nextTab、tab、sizeCtl值、如果nextTab没有被并发修改并且tab也没有被并发修改,同时sizeCtl<0,说明还在扩容;对sizeCtl参数值进行分析判断,如果不满足任何一个判断,将sizeCtl+1,增加了一个线程帮助其扩容。
- addCount扩容判断:利用CAS将方法更新baseCount的值;检查是否需要扩容,默认check=1,需要检查;如果满足扩容条件,判断当前是否正在扩容,如果是正在扩容就一起扩容;如果不在扩容,将sizeCtl更新为负数,并进行扩容处理。 以上就是整个put方法的流程,可以发现,里面大量使用了CAS方法,CAS表示比较与替换,里面有3个参数,分别是目标内存地址、旧值、新值,每次判断的时候,会将旧值与目标内存地址中的值进行比较,如果相等,就将新值更新到内存地址里,如果不相等,就继续循环,直到操作成功为止。
- get方法:判断数组是否为空,通过key定位到数组下标是否为空;判断node节点第一个元素是不是找到,如果是直接返回;如果是红黑树结构,就从红黑树里面查询;如果是链表结构,循环遍历判断。
- remove方法:循环遍历数组,接着校验参数;判断是否有别的线程正在扩容,如果是一起扩容;用synchronized同步锁,保证并发时元素移除安全;
- 因为check=-1,所以不会进行扩容操作,利用CAS操作修改baseCount值。 但是多个线程同时调用unsafeUpdatae()方法,ConcurrentHashMap不能保证线程安全。