文章目录
Java_多线程那点事

Java内多线程函数含义
synchronized
- Java中每一个对象都可以成为一个监视器
(Monitor), 该Monitor由一个锁(lock), 一个等待队列(waiting queue), 一个入口队列(entry queue)组成。 - 对于一个对象的方法, 如果没有synchronized关键字修饰, 该方法可以被任意数量的线程,在任意时刻调用。
对于添加了synchronized关键字的方法,任意时刻只能被唯一的一个获得了对象实例锁的线程调用。 - synchronized用于实现多线程的同步操作
Wait
wait(),notify(),notifyAll()和synchronized需要搭配使用, 用于线程同步wait()总是在一个循环中被调用,挂起当前线程来等待一个条件的成立。 Wait调用会一直等到其他线程调用notifyAll()时才返回。wait()在其他条件不满足时,将获得的锁释放掉- 当一个线程在执行
synchronized的方法内部,调用了wait()后, 该线程会释放该对象的锁, 然后该线程会被添加到该对象的等待队列中(waiting queue), 只要该线程在等待队列中, 就会一直处于阻塞状态,不会被调度执行。 要注意wait()方法会强迫线程先进行释放锁操作,所以在调用wait()时, 该线程必须已经获得锁,否则会抛出异常。但是由于wait()在synchronized的方法内部被执行, 锁一定已经获得, 就不会抛出异常了。
Notify
- 当一个线程调用一个对象的
notify()方法时, 调度器会从所有处于该对象等待队列(waiting queue)的线程中取出任意一个线程, 将其添加到入口队列(entry queue)中. 然后在入口队列中的多个线程就会竞争对象的锁, 得到锁的线程就可以继续执行。 如果等待队列中(waiting queue)没有线程,notify()方法不会产生任何作用 notifyAll()和notify()工作机制一样, 区别在于notifyAll()会将等待队列(waiting queue)中所有的线程都添加到入口队列中(entry queue)notifyAll()比notify()更加常用, 因为notify()方法只会唤起一个线程,且无法指定唤醒哪一个线程
多线程的几种状态
-
新建
- 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
- 此时还只是Java对象,还未与操作系统底层线程关联
-
可运行
- 调用了 start 方法,就会由新建进入可运行
- 此时与底层线程关联,由操作系统调度执行
-
终结
- 线程内代码已经执行完毕,由可运行进入终结
- 此时会取消与底层线程关联
-
阻塞
- 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
-
等待
- 当获取锁成功后,但由于条件不满足,调用了 wait()方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
-
有时限等待
- 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
- 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
- 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态
注意:
-
操作系统层面将阻塞 等待 有限时等待都作为阻塞
-
一个线程调用了
wait()之后, 必然需要由另外一个线程调用notify()来唤醒该线程
锁的种类
-
独占锁:所谓独占锁,就是某一时刻,该锁只能被一个线程占有的
-
共享锁:共享锁则是某一时刻,该锁可以被多个线程同时占有
-
公平锁:获取锁的顺序,和请求锁的顺序是一致的
-
非公平锁:获取锁的顺序,和请求锁的顺序不一致,存在抢占机制
线程池

wait vs sleep
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
- 方法归属不同
- sleep(long) 是 Thread 的静态方法
- 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
- 醒来时机不同
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
- 它们都可以被打断唤醒
- 锁特性不同(重点)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
- 其他
-
wait必须拥有锁才可以使用,而sleep可以不需要锁
-
lock vs synchronized
-
语法层面
- synchronized 是关键字,源码在 jvm中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁(使用final进行解锁)
-
功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
-
性能层面
-
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
-
在竞争激烈时,Lock 的实现通常会提供更好的性能
-
公平锁
- 公平锁的公平体现
- 已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出
- 公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待
- 非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的
- 公平锁会降低吞吐量,一般不用
条件变量
持有锁,但是其他条件不满足,先放弃锁,在condition队列中等待
- ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构
- 与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制
volatile
线程安全需要考虑三个方面:可见性、有序性、原子性
- **可见性:**一个线程对共享变量进行修改,另一个线程可以看到最新的结果
- **有序性:**一个线程内代码按编写顺序执行
- **原子性:**一个线程内多行代码以一个整体运行,期间不能有其他线程的代码插队
volatile 可以保证可见性与有序性,但是不能保证原子性,因此,volatile 在多线程并发修改某个变量时依然会出现并发问题,并非完全安全的。首先推荐使用锁来解决问题。
-
**保证可见性:**用volatile修饰的共享变量,能够防止编译器等jit优化发生,让一个线程对共享变量的修改对另一个线程可见
jit 是 just in time 的缩写, 也就是即时编译编译器,用来进行优化程序,使得程序运行效率提高
-
**保证有序性:**用volatile修饰共享变量会在读、写共享变量时加入不同的内存屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的结果
使用内存屏障来解决指令重排序的问题,使得指令按顺序执行。
对于有序性,此处有需要注意的点:
-
volatile变量写加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
-
volatile 变量读加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
-
volatile 读写加入的屏障只能防止同一线程内的指令重排
悲观锁 VS 乐观锁
-
悲观锁的代表是 synchronized 和 Lock 锁
- 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
- 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
- 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
-
乐观锁的代表是 AtomicInteger,使用 CAS 来保证原子性
- 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
- 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
- 它需要多核 cpu 支持,且线程数不应超过 cpu 核数
【补】CAS:Compare And Swap(比较并交换),底层是有三个操作数,分别是内存值、预期值、更新值。只有当内存值和预期值相等的时候,才会去修改为更新值。
-
优点:避免死锁的产生,没有锁竞争
-
缺点:
-
底层是通过do-while来完成锁自旋,开销较大
-
会导致ABA问题(所谓的ABA问题就是A线程修改B中的原本值修改为新值,另一个A线程又将新值修改为原本值,B无法得知这一变化)
-
只能保证单个变量
-
Hashtable 与 ConcurrentHashMap
-
Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
-
Hashtable 与 ConcurrentHashMap他们都键值都不能为空
-
Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
-
ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
细说ConcurrentHashMap
在JDK1.7中:
- 数据结构:
Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突 - **并发度:**Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
- 索引计算:
- 假设大数组长度是 2的m次方,key 在大数组内的索引是 key 的二次 hash 值的高 m 位
- 假设小数组长度是 2的n次方,key 在小数组内的索引是 key 的二次 hash 值的低 n 位
- **扩容:**每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
- Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准
在JDK1.8中:
- 数据结构:
Node 数组 + 链表 + 红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 CAS而非 syncronized,进一步提升性能 - **并发度:**Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容
- **扩容条件:**Node 数组满 3/4 时就会扩容
- **扩容单位:**以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode
- 扩容时并发 get:
- 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
- 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变
- 如果链表最后几个元素扩容后索引不变,则节点无需复制
- 扩容时并发 put:
-
如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
-
如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
-
如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
-
ThreadLocal
-
ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
-
ThreadLocal同时实现了线程内的资源共享
-
多个不同线程来时可以完成线程隔离,多个相同线程来时可以避免重复
ThreadLocalMap
-
key 的 hash值统一分配
-
初始容量为16,扩容因子为2/3,扩容容量翻倍
-
key索引冲突后用开放寻址法解决冲突
-
get了不存在的key,会添加此key 值为null
【补充】开放寻址法:解决哈希冲突的一种办法,它和拉链法有所相似,只是处理冲突的方式不一样。拉链法通过在冲突位置开链表解决,开放寻址法通过往后顺次找空位置解决
ThreadLocalMap中的Key
ThreadLocalMap中的Key被设计为弱引用,原因如下:
Thread可能需要长时间运行(如线程池中的线程),如果Key不再使用,需要在内存不足时释放占用的内存
ThreadLocalMap的内存释放
-
被动 GC 释放 key:
- 仅是让 key 的内存释放,关联 value 的内存并不会释放( value会停留在内存)
-
懒惰被动释放 value:
- get key 时,发现是 null key,则释放其 value 内存
- set key 时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关
-
主动 remove 释放 key,value:
-
会同时释放 key,value 的内存,也会清除临近的 null key 的 value 内存
-
推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收
-

182




被折叠的 条评论
为什么被折叠?



