这是我参与更文挑战的第1天,活动详情查看: 更文挑战
1. Java同步机制
1.1 Java做到线程同步方法
-
在需要同步的方法的方法签名中加上synchronized关键字(锁方法)
-
使用synchronized关键字对需要进行同步的代码块进行同步 (锁代码块)
-
使用
java.util.concurrent.lock
包中Lock对象(JDK1.8)---JDK 1.5出现 -
使用volatile关键字(不能替代synchronized)
a. volatile关键字为域变量的访问提供了一种免锁机制 b. 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新 c. 因此每次使用该域就要重新计算,而不是使用寄存器中的值 d. volatile不会提供任何原子操作(因此不能替换synchronized),它也不能用来修饰final类型的变量
1.2 synchronized使用时需要注意的一些地方:
被synchronized关键字修饰的代码块在被线程执行之前,首先要拿到被同步对象的锁,并且一个对象仅仅是只有一个锁,比如上面被synchronized代码,首先那个方法需要拿到当前对象的锁,如果当前的锁已经被其它线程拿走了,那么还没抢到锁的线程将从可运行状态转变为阻塞状态,只有当拿到锁的线程执行完同步块的代码后,就释放锁,让给别的线程的、这样就可以保证数据的完整性!
1.3 关于Lock对象和synchronized关键字的选择:
(1)最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。 (2)如果synchronized关键字能够满足用户的需求,就用synchronized,他能简化代码。 (3)如果需要使用更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally中释放锁。
1.4 volatile与synchronized的区别:
参考链接:blog.csdn.net/suifeng3051…
全面了解推荐链接:blog.csdn.net/suifeng3051… JMMJava内存模型
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
volatile就是基于内存屏障实现
1.5 happens-before
从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。
在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系,这两个操作即可以在同一个线程,也可以在不同的线程中。
- happens-before原则定义
- 如果一个操作happens-before另一个操作,那么第一个操作的结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
- happens-before原则规则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
happens-before与JMM的关系图(摘自《Java并发编程的艺术》)
2. HashMap (Java 8系列)
2.1 Map家族
- HashMap:根据键的hashCode值存储数据。非线程安全,可以用synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap
- Hashtable:遗留类,线程安全,但是不建议使用
- LinkedHashMap:保存了记录的插入顺序
- TreeMap:实现了SortedMap接口,能够把它保存的记录根据键排序,默认是升序排序
2.2 HashMap存储结构
数组+链表+红黑树
当put一个键值对时,首先获取key值的hashCode值,在通过hash算法(高位运算和取模运算)
来定位该剪值对对对应的数组下标,然后再找链表或红黑树值(具体见下面)。
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码可知,构造函数就是对下面几个字段进行初始化,源码如下:
int threshold; // 所能容纳的key-value对极限
final float loadFactor; // 负载因子
int modCount;
int size;
Load factor为负载因子(默认值是0.75)
threshold是HashMap所能容纳的最大数据量的Node(键值对)个数,threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
modCount字段主要用来记录HashMap内部结构发生变化的次数
size:HashMap中实际存在的键值对数量
哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
2.3 HashMap的put方法
(具体见链接)
3. ConcurrentHashMap 1.7和1.8的区别
出现场景
在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug,于是出现了ConcurrentHashMap
1. 数据结构
- 1.7:Segment+HashEntry数组+链表
ConcurrentHashMap
初始化时,计算出Segment
数组的大小ssize
和每个Segment
中HashEntry
数组的大小cap
,并初始化Segment
数组的第一个元素;其中
ssize
大小为2的幂次方,默认为16,cap
大小也是2的幂次方,最小值为2,最终结果根据初始化容量initialCapacity
进行计算因为
Segment
继承了ReentrantLock,所有segment是线程安全的
- 1.8:Node数组+链表/红黑树+CAS+synchronized
移除Segment,使锁的粒度更小
2. put()
-
1.7:先定位到Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。
-
1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,再到first节点后进行判断:
- 为空则CAS插入
- 为-1则说明在扩容,则跟着一起扩容
- 两者都不是,则加锁put
3. get()
基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁
4. size()
- 1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和
- 1.8:用baseCount(用volatile修饰)来存储当前的节点个数,这就涉及到baseCount并发环境下修改的问题
5. resize()
- 1.7:跟HashMap步骤一样,不过是搬到单线程中执行,避免了HashMap在1.7中扩容是死循环的问题,保证线程安全
- 1.8:支持并发扩容,HashMap扩容在1.8中由头插改成尾插(为了避免死循环),ConcurrentHashMap也一样,迁移也是从尾部开始,扩容前在统的头部防止一个hash值为-1的节点,这样别的线程访问是就能判断是否该桶已经被其他线程处理过了
4. ArrayList和LinkedList区别
1. List概括
2. ArrayList和LinkedList区别
- ArrayList实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构(双向链表,它同样可以被当做栈,队列或双端队列来使用);
- 对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;
- 对于添加和删除操作add和remove,一般大家都会说LinkedList要比ArrayList快,因为ArrayList要移动数据。但是实际情况并非这样,对于添加或删除,LinkedList和ArrayList并不能明确说明谁快谁慢;
- 从源码可以看出,ArrayList想要get(int index)元素时,直接返回index位置上的元素,而LinkedList需要通过for循环进行查找,虽然LinkedList已经在查找方法上做了优化,比如index < size / 2,则从左边开始查找,反之从右边开始查找,但是还是比ArrayList要慢。这点是毋庸置疑的。
- ArrayList想要在指定位置插入或删除元素时,主要耗时的是System.arraycopy动作,会移动index后面所有的元素;LinkedList主耗时的是要先通过for循环找到index,然后直接插入或删除。(这就导致了两者并非一定谁快谁慢)
所以当插入的数据量很小时,两者区别不太大,当插入的数据量大时,大约在容量的1/10之前,LinkedList会优于ArrayList,在其后就劣与ArrayList,且越靠近后面越差。所以个人觉得,一般首选用ArrayList,由于LinkedList可以实现栈、队列以及双端队列等数据结构,所以当特定需要时候,使用LinkedList。当然咯,数据量小的时候,两者差不多,视具体情况去选择使用;当数据量大的时候,如果只需要在靠前的部分插入或删除数据,那也可以选用LinkedList,反之选择ArrayList反而效率更高。
5. Java线程安全的集合
1. Vector
Vector和ArrayList类似,是长度可变的数组,但是Vector是线程安全的。它给几乎所有的pubic方法都加了synchronized
关键字。由于加锁导致性能降低,所以Vector已经被弃用。
2. HashTable
HashTable和HashMap类似,不同点是HashTable是线程安全的,它给几乎所有public方法都加上了synchronized
关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了
3. ConcurrentHashMap
- 在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
- JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率
4. CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行
5. 其他
ConcurrentSkipListMap
、ConcurrentSkipListSet
、ConcurrentLinkedQueue
、ConcurrentLinkedDeque
等,没有ConcurrentArrayList