本博客内容是博主学习时自己整理的,如有错误,欢迎大家积极指出
参考博客地址:
了解 Java 中的锁 Lock
Java 并发基础(一):synchronized 锁同步
基本名词解释
- 线程和进程
- 线程:
- 系统运算调度的最小单位
- 同一进程之间的多线程共享虚拟空间和资源
- 进程:
- 操作系统中分配资源的最小单位
- 多个进程之间的虚拟空间和资源互相隔离
- 具有一定独立功能的程序关于某个数据集合的一次运行活动
- 线程:
- 并行和并发
- 并行
- 两个CPU同时执行自己的进程,互不影响
- 物理上的同时进行
- 并发
- 同一时间,有多个指令在一个CPU上执行,CPU进行时间分片给各个指令
- 逻辑上的同时进行
- 并行
- CPU线程:
- 一般情况下,一个核心对应一个线程。在intel的超线程技术下,一个核心可以对应两个以上的线程(硬件线程)
- 在某一个时间分片上,一个线程对应Java一个线程
实现方式
- 实现Runnable接口:实现run()方法
- 实现Callable接口:实现call()方法,可以执行返回结果和抛出异常
- 继承Thread类:重写run()方法
- 线程池:
生命周期
- 初始化(NEW):new thread()之后,start()之前
- 可运行(RUNNABLE):
- RUNNABLE-READY:等待CPU分配资源
- RUNNABLE-RUNNING:正在运行
- 被阻塞(BLOCKED):进入Synchronized锁保护代码,未抢到monitor锁
- 等待(WAIT)
- 没有设置参数的Object.wait()/Thread.join()方法
- LockSupport.park()方法(除了Synchronized锁以外,其他的都是这种状态)
- 超时等待(TIMEOUT_WAIT)
- 方法Thread.sleep()/Object.wait(long times)/Thread.join(long times)
- 设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法
- 终止(TERMINATED)
- run()方法执行完毕
- 出现了未捕获的异常
补充:
- run()和start()方法:
run()方法实际是执行线程的逻辑代码,普通方法
start()方法是创建一个线程,进入RUNNABLE状态
线程池
- 基本介绍
- 核心类Executor,使用ThreadPoolExecutor
- 优点
- 复用线程,避免线程创建、销毁的性能消耗
- 有效控制最大并发数,避免大量线程抢夺资源造成线程的阻塞
- 对线程进行简单的管理
- 线程池基本参数
- CorePoolSize
- 核心线程数量,如果不设置核心线程超时时间的,那么核心线程一直存在,不会销毁
- MaximumPoolSize
- 最大线程数,超过后的线程会阻塞
- KeepAliveTime
- 非核心线程存活时间
- Unit
- KeepAliveTime时间单位
- WorkQueue
- 线程池的任务队列大小,execute方法提交的Runnable对象储存在当中,属于BlockQueue类型
- ThreadFactory
- 线程工厂,为线程池提供新线程
- 拒绝策略:任务队列和线程池都满了采取的策略
- AbordPolicy:默认,抛出异常
- CallerRunsPolicy:用调用者所在线程来处理任务
- DiscardPolicy:直接丢弃
- DiscardOldestPolicy:丢弃最老的任务,并执行当前任务
- CorePoolSize
锁
- Synchronized
-
使用位置
- 普通方法:锁对象是当前实例
- 静态方法:锁对象是类的Class对象
- 方法块:锁对象是括号内的
-
原理
- Synchronized用的锁存在于Java对象头里的Mark Word,数据随着位置的变化而变化
- 通过进入和退出Monitor对象机制实现的锁机制
- Monitorenter插入开始位置,Monitorexit插入结束位置,成对出现
- Monitorenter尝试获取锁,如果对象没有被锁定或者当前线程持有锁,计数器+1,Monitorexit执行,计数器-1,直到计数器=0,释放锁
- JDK1.6之前Monitor依靠操作系统内部互斥锁实现,会阻塞用户态和内核态的切换,所以是一个无差别的重量级锁
- JDK1.6之后,为了避免上述情况,会在阻塞线程之前加入自旋操作。同时还实现了3中不同的Monitor:偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)、重量级锁。性能不比ReentrantLock差,只是不够灵活
- 偏向锁:
- 大多数情况下,锁不存在多线程竞争,总是同一线程获得锁。轻量锁通过CAS来进行加锁和解锁,而偏向锁只需要测试Mark Word中是否存在指向当前线程的偏向锁,偏向锁只有置换ThreadID需要依靠一次CAS原子指令。但是一旦出现多线程竞争的情况,就需要撤销偏向锁
- 偏向锁的获取:
- 锁对象第一次被获取,对象头标志位设为01,偏向模式设为1,进入偏向模式
- 再次请求锁对象,看ID是否指向当前线程,如是,执行同步代码块,如果不是执行接下来操作
- 使用CAS操作把当前线程的ID记录在锁对象的Mark Word中。如果成功,执行代码块,如果失败,说明其他线程持有过该偏向锁,开始尝试获取偏向锁
- 当达到全局安全点时(没有字节码执行),会暂停拥有偏向锁的线程,检查线程状态。如果线程已经结束,则将对象头设置成无锁状态(标志位为01),然后重新偏向新的线程。如果线程还存活,则撤销偏向锁,升级为轻量锁(标志位00),此时,轻量锁由原偏向锁线程持有,继续执行同步代码,而竞争线程进入自旋等待该轻量锁
- 偏向锁其他:
- 偏向锁采用惰性释放机制;只有等到竞争线程出现时才会释放
- 偏向锁的撤销操作时一个比较重的行为,所以不适合在多线程竞争的场景,所以偏向锁使用有一定的争议
- 如果确定锁通常处于多线程竞争情况下可以在JVM参数中设置关闭偏向锁-XX:-UseBiasedLocking=false
- 轻量锁
- 作用:在线程不是很多得情况下,减少重量锁利用操作系统互斥产生的性能消耗
- 适用场景:线程交替执行同步代码块
- 锁获取
- 如果按当前锁对象无锁(标志位01,偏向锁0),虚拟机在当前线程的栈帧中建立Lock Record空间,用于记录锁对象的Mark Work拷贝信息
- 当拷贝成功后,虚拟机尝试使用CAS操作将锁对象Mark Word指针指向Lock Record,并且将Lock Record中的owner指针执行Mark Work
- 如果执行成功,则Mark Word的锁标志为设为00,此时处于轻量级锁定状态
- 如果执行失败,检查Mark Word指针是否指向当前线程,如果是,则执行同步代码
- 如果否,说明多线程竞争锁,如果只有一个线程等待,则等待线程进行自旋,如果自旋超过一定次数或者又加入一个线程,则升级成为重量级锁,防止CPU空转,锁标志位状态10,Mark Work中储存的就是指向重量级锁的指针,后面的线程进入阻塞状态
- 解锁
- 同步代码执行完成,解锁
- 尝试将线程中复制的Displaced Mark Word对象 替换为当前的Mark Word
- 如果成功则同步完成,失败则说明存在竞争,膨胀成为重量级锁。释放锁的同时,唤起被挂起的线程
- 重量级锁
- 场景:同一时间访问相同锁对象,第一个线程持有锁,第二个线程自旋超过一定次数,则自动膨胀为重量级锁,锁标志为10,Mark Word指向互斥量(重量级锁)
- 偏向锁:
-
锁消除:
- 解释:虚拟机即时编译器在运行过程中,发现不会存在共享资源竞争的情况,就会删除锁
- 依据:锁消除的依据是逃逸分析。逃逸分析就是分析对象的动态作用域
- 不逃逸:对象的作用域只在本线程本方法
- 方法逃逸:对象在方法内定义后,被外部方法调用
- 线程逃逸:对象在方法内定义后,被外部线程调用
- 即时编译器优化:
- 对象栈上分配:直接在栈上创建对象
- 标量替换:在对象不会逃出方法范围的前提下,将对象拆散,直接创建被方法使用的成员变量。
- 同步消除:对象不会逃逸出线程,就锁消除
- 例子:
-
锁粗化:
- 原则上加锁的代码块越小越好,但是连续的加锁会出现频繁的互斥,导致不必要的性能损耗,此时虚拟机就会将锁粗化,将锁范围扩大到外部。比如上述StringBuilder的连续append
-
适应性自旋: 一般情况下共享数据的锁定时间很多,但是线程从用户态到内核态的切换比较耗时。所以可以让后面的线程执行一个忙循环,每循环一圈就尝试获取锁
锁升级流程
锁升级Mark Down数据变化:
-
Lock
-
什么是Lock
- Synchronized是一个关键字,属于JVM层面的锁,且不能查看源码,所以他是一个隐式锁
- Lock是一个接口,提供无条件的、可轮询的、定时的、可以中断的锁获取操作,所有加锁和释放锁都是显性的,因此称为显性锁
- Lock只是一个接口,具体功能主要靠子类实现,其中常见的可重入锁ReentrantLock和读写锁ReentrantReadWriteLock
-
Lock的常用API
-
名词解释
- 独占锁:又称排他锁,就是一个线程对资源加上锁后,其他线程不能再给该资源加任何锁,该线程获得资源的读取和修改权(Synchronized、Lock实现类)
- 共享锁:可以被多个线程持有,一个线程对资源上锁后,其他线程也可以上共享锁,但不能上排他锁,获得共享锁的线程可以读取数据,不能修改(ReentrantReadWriteLock)
- 独享和共享锁都通过AQS来实现
-
ReentrantLock
- 可重入互斥锁(获得锁的线程可以继续多次申请该锁的使用权),和Synchronized相似,但更灵活
- 可以打断,lockInterruptibly()方法中断,线程1在持有锁的时候,线程二再获取锁,没有获取到锁可以进行打断而不是进入阻塞状态
- 可超时,设置超时时间后,在这段时间,都可以一直获取锁
-
ReentrantReadWriteLock
- 读写锁,允许多个读线程访问,但是写线程访问时,所有读线程和其他写线程全部被阻塞
- 持有一个读锁一个写锁,适用于大部分是读操作,少量写操作的资源
- Java5之前是Synchronized+通知机制实现,使用读写锁更加简单明了,而且读写锁性能优于排他锁
-
Condition接口
- Object类提供了wait()、wait(long timeunit)、notify()、notifyAll()方法,配合Synchronized实现等待/通知模式(Lock和Condition也一样)
-
使用详情
-
-
Synchronized和Lock选择
- Synchronized在JVM层面直接处理(释放锁);Lock是一个接口,有丰富的API,更加灵活
- Synchronized可以锁方法和代码块,Lock只能锁代码块
- Synchronized是非公平锁,Lock可以控制是否公平
- Lock可以使用读写锁提高效率
-
Volatile
- 和JMM(Java Memory Model)相关联,具体了解Java内存模型
- 当写一个Volatile变量时,JMM会把该线程本地内存的值刷新回主内存中
- 当读一个Volatile变量时,JMM会把该线程本地内存置为无效,直接读取主内存
- 三大特性
- 保证可见性
- 相比Synchronized加锁方式实现共享变量可见,volatile更加轻量,没有上下文切换的额外开销
- 保证有序性(禁止指令重排)
- 存在数据依赖性禁止指令重排
- 不保证原子性
- 保证可见性