一、HashMap 是不是线程安全?
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,
在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的
(多线程会导致 HashMap 的 node 链表形成环状的数据结构产生死循环)
1.1、如何变得安全:
Hashtable:通过 synchronized 来保证线程安全的,独占锁,悲观策略。吞吐量较低,性能较为低下
ConcurrentHashMap:JUC 中的线程安全容器,高效并发。ConcurrentHashMap 的 key、value 都不允许为 null
1.2、jdk1.8相对于jdk1.7的优化
由 数组+链表 的结构改为 数组+链表+红黑树。
拉链过长会严重影响hashmap的性能,所以1.8的hashmap引入了红黑树,当链表的长度大于8时,转换为红黑树的结构
优化了高位运算的hash算法:h^(h>>>16) 将hashcode无符号右移16位,让高16位和低16位进行异或。
二、ConcurrentHashMap 的实现方式
1.7
ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁
put将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
1. 计算键所对应的 hash 值;2. 如果哈希表还未初始化,调用 initTable() 初始化,否则在 table 中找到 index 位置,并通过 CAS 添加节点。如果链表节点数目超过 8,则将链表转换为红黑树。如果节点总数超过,则进行扩容操作
get将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上(get():无需加锁,直接根据 key 的 hash 值遍历 node),由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值
ConcurrentHashMap的get方法采用了unsafe方法,来保证线程安全
ConcurrentHashMap迭代器是强一致性,hashmap强一直性(ConcurrentHashMap可以支持在迭代过程中,向map添加新元素,而HashMap则抛出了ConcurrentModificationException)
1.1、jdk1.8相对于jdk1.7的区别
jdk1.7:Segment+HashEntry来进行实现的;
jdk1.8:放弃了Segment臃肿的设计,采用Node+CAS+Synchronized来保证线程安全;
jdk1.8的实现降低锁的粒度,jdk1.7锁的粒度是基于Segment的,包含多个HashEntry,而jdk1.8锁的粒度就是Node(将 1.7 中存放数据的 HashEntry 改 为 Node,但作用都是相同的)
数据结构:jdk1.7 Segment+HashEntry;jdk1.8 数组+链表+红黑树+CAS+synchronized
三、CountDownLatch 和 CyclicBarrier
CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作
countdownlatch中有个计数器,当计数器减少到0的时候,释放所有等待的线程,coutDown()会让计数器的值减少,await() 进入阻塞状态,直到count为0为止,所有等待的线程都会开始执行。
而且CountDownLatch只有一次的机会,只会阻塞线程一次
CyclicBarrier:是回环栅栏,只有等待线程积累到一定的数量的时候才会释放屏障,在释放屏障的时候还可以使用接口初始化 是可以重重复使用的
四、怎么控制线程,尽可能减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程
无锁并发编程:多线程处理数据时,可以使用一些方法来避免使用锁。如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据(currenthashmap分段锁思想)
CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
五、乐观锁和悲观锁
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现
1.1、乐观锁的ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能 的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
六、并发特性 - 原子性、有序性、可见性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行,不进行指令重排列
1.原子性:提供互斥访问,串行线程(atomic,synchronized);
2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则 A操作一定在B操作之前,而是A操作的影响能被操作B观察到)
七、synchronized
synchronized锁可以修饰在 普通方法中、静态方法中、代码块,
synchronized是内置的语言实现,jvm编译器(monitor)去保证锁的加锁和释放 ,synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生
解决多线程并发访问共享数据的竞争问题
synchronized使用的锁对象是存储在Java对象头的Mark Word内,Mark Word存储对象的HashCode、分代年龄、锁
其中锁分为:偏向锁、轻量级锁、自旋锁
jdk1.6以后对synchronized做了优化, 如自旋锁、偏向锁、轻量级锁、自旋锁等技术来减少锁操作的开销
一个线程获得了锁,进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁,但锁竞争比较激烈的场合,偏向锁就失效
转换为轻量级锁,不存在竞争, 轻量级锁失败后
转换为自旋锁,若干次循环后 去竞争锁
八、volatile
变量定义为 volatile 之后 具备两种特性:保证此变量对所有的线程的可见性;禁止指令重排序优化
volatile变量通过内存屏障是一个CPU指令,指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么
九、JMM
JMM 规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序
一方面提供足够强的内存可见性保证
一方面计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排
在并发编程模式中 线程安全考虑会有3个概念:
1、可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
对于串行程序来说,可见性是不存在的在多线程环境中线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中共享变量x进行操作。
2、有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的
3、原子性
一个操作或者多个操作要么全部执行要么全部不执行。
通过 volatile、synchronized、final、concurrent 包等 实现。
十、队列 AQS 队列同步器
AQS 是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等), 包含了实现同步器的细节(获取同步状态、FIFO 同步队列)。AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。
维护一个同步状态 state。当 state > 0时,表示已经获取了锁;当state = 0 时,表示释放了锁。
1、如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程
2、当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态
AQS 内部维护的是** CLH 双向同步队列**
十一、锁的特性
可重入锁:指的是在一个线程中可以多次获取同一把锁。 ReentrantLock 和 synchronized 都是可重入锁。
可中断锁:顾名思义,就是可以相应中断的锁。synchronized 就不是可中断锁,而 Lock 是可中断锁。
公平锁:即尽量以请求锁的顺序来获取锁。synchronized 是非公平锁,ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
十二、ReentrantLock
ReentrantLock可重入锁、显示锁ReentrantLock 提供了比synchronized 更强大、灵活的锁机制,可以减少死锁发生的概率
ReentrantLock 实现 Lock 接口,基于内部的 Sync 实现
Sync 实现 AQS ,提供了 FairSync(公平锁) 和 NonFairSync(非公平锁) 两种实现
Condition 和 Lock 一起使用以实现等待/通知模式,通过 await()和singnal() 来阻塞和唤醒线程。
十三、ReentrantReadWriteLock
读写锁维护着一对锁,一个读锁和一个写锁。分离读锁和写锁
在同一时间,可以允许多个读线程同时访问,但是,在写线程访问时,所有读线程和写线程都会被阻塞
十四、Synchronized 和 Lock 的区别
synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。Lock 是一个接口 JDK自带
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行ReentrantLock 提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票
ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而 synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized 而言,ReentrantLock 会不容易产生死锁些。
ReentrantLock 支持中断处理
十五、Java 中线程同步的方式
sychronized 同步方法或代码块
volatile、Lock、ThreadLocal、阻塞队列(LinkedBlockingQueue)、使用原子变量(java.util.concurrent.atomic)
十六、多线程下为什么不使用 int 而使用 AtomicInteger。
Concurrent 包下的类的源码时,发现无论是 ReentrantLock 内部的 AQS,还是各种 Atomic 开头的原子类,内部都应用到了 CAS
CAS 中有三个参数:内存值 V、旧的预期值 A、要更新的值 B ,当且仅当内存值 V 的值等于旧的预期值 A 时,才会将内存值 V 的值修改为 B,否则什么都不干
Unsafe 是 CAS 的核心类,Java 无法直接访问底层操作系统,而是通过本地 native` 方法来访问。不过尽管如此,JVM 还是开了一个后门:Unsafe ,它提供了硬件级别的原子操作
valueOffset 为变量值在内存中的偏移地址,Unsafe 就是通过偏移地址来得到数据的原值的
在多线程环境下,int 类型的自增操作不是原子的,线程不安全
十七、线程池
使用线程池目的:
1、创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率
2、对线程进行一些简单的管理(延时执行、定时循环执行的策略等)
3、线程并发数量过多,运用线程池能有效的控制线程最大并发数,防止抢占系统资源从而导致阻塞
线程池有五种状态:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。
参数:
corePoolSize线程池中核心线程的数量
maximumPoolSize 线程池中允许的最大线程数
keepAliveTime线程空闲的时间,线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime
unit:keepAliveTime 的单位
workQueue:用来保存等待执行的任务的阻塞队列 (可选ArrayBlockingQueue、LinkedBlockingQueue 等)
handler:线程池的拒绝策略 (向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务)
来源:https://www.cnblogs.com/webster1/p/12300801.html