文章目录
多线程
在聊Java的多线程技术之前,我们需要知道线程及其相关的一些概念,如进程、线程、线程生命周期等概念。那么何为进程?进程的定义如下:
进程:程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
所谓的进程,其实就是程序的一次执行过程。相比于程序的静态概念来说,进程是动态概念。那么线程又是什么呢?线程其实就是进程中一个负责程序执行的控制单元,也可以称之为一条执行路径。需要知道的是,一个进程中至少有一个线程。引入进程是为了使多个程序能并发执行,而引入线程是为了减少程序在并发执行中所付出的开销。
在线程上衍生出的单线程以及多线程其实区别就在于一个进程是否存在一条或多条执行路径。
将基本的概念了解了以后,就可以开始学习Java中涉及到的多线程。
多线程初接触
首先需要知道的是,线程是依赖于进程而存在的,而进程是由系统创建的,因此应该调用系统功能去创建一个进程。而Java是不能直接去调用系统功能的,因此没有办法直接实现多线程的程序,但是Java可以调用C/C++写好的程序来实现多线程。
那么就在Java中提供了两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口。
这里稍微提一下,为什么会出现两种不同的方式来实现多线程。其实特别简单,Java中的继承只支持单继承,如果是继承Thread类来实现多线程,那么这个子类就无法继承其他的类。也就是说,产生了局限性,可能无法实现更多复杂的功能。因此,为了避免出现这个局限性,就产生了实现Runnable接口的方式。
实际上,实现Runnable接口的方式很好地体现了面向对象设计的思想,增强了代码的健壮性,使线程、代码、数据三者有效地分离。
接下分别演示一下这两种方式,首先是继承Thread类,步骤如下所示:
1. 自定义类继承Thread类
2. 自定义类重写run()方法
3. 创建步骤3的自定义类对象
4. 启动线程
代码如下所示:
public class MyThread extends Thread {
public MyThread() {
super();
}
// 传入的参数代表线程的名称
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// getName()方法为Thread类自带方法,用于获取线程名称
System.out.println(getName() + ":" + i);
}
}
}
public class Demo01 {
public static void main(String[] args) {
method1();
}
public static void method1() {
// 传入参数即为线程名称
MyThread myThread1 = new MyThread("线程1");
MyThread myThread2 = new MyThread("线程2");
// myThread1.run();
// 线程调度 Java默认线程优先级为5,范围1~10
myThread1.setPriority(Thread.MAX_PRIORITY);
myThread2.setPriority(5);
myThread1.start();
myThread2.start();
}
}
结果如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/3d28a9bc6b673be8c7a3574adae6c25b.png)
这里需要提到的一点:start方法和run方法的区别。run方法中只是封装了被线程执行的代码,直接调用则是普通方法。而start方法是启动线程然后调用该线程的run方法。
接着是实现Runnable接口,步骤如下所示:
1. 自定义类实现Runnable接口
2. 重写run方法
3. 创建自定义类对象
4. 创建Thread类的对象,并把步骤3的对象作为构造参数传递
代码如下所示:
public class MyThread02 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Demo01 {
public static void main(String[] args) {
method2();
}
public static void method2() {
Thread thread1 = new Thread(new MyThread02());
Thread thread2 = new Thread(new MyThread02());
thread1.start();
thread2.start();
}
结果如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/b08cd4f7443f01908d6a0253ef1d31e9.png)
对Java中的多线程有了一个初步了解后,就可以往更深的层次继续挖掘。
线程控制
在讲线程的控制之前先说一下线程的生命周期。在JDK5以后,在java.lang.Thread.State中明确定义了线程状态,如下所示:
public enum State {
/**
* 新建状态:
* 表示线程被创建出来但还未启动的状态
*/
NEW,
/**
* 就绪状态:
* 表示线程需要等待JVM线程调度器的调度
*/
RUNNABLE,
/**
* 阻塞状态:
* 表示线程在等待Monitor Lock的状态
* 具体可以分为:等待阻塞、同步阻塞以及其他阻塞
*/
BLOCKED,
/**
* 等待状态:
* 表示正在等待其他线程采取某些操作
* 常见场景:生产者消费者模式
*/
WAITING,
/**
* 计时等待状态:
* 和等待状态类似但是调用的方法是带时间参数的wait等方法
*/
TIMED_WAITING,
/**
* 终止状态(死亡状态):
* 线程完成任务或者因其他条件终止的状态
*/
TERMINATED;
}
接着再来看相关的状态转换图,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/b3216fd44bd6a8ecd12d303e7d6050f2.jpeg)
从上面的状态转换图就可以得到几个重要的方法,如下所示:
// 判断线程是否处于活动状态
public final boolean isAlive()
// 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),该线程不丢失任何监视器的所属权
public static void sleep(long millis)
// 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),该线程不丢失任何监视器的所属权
public static void sleep(long millis, int nanos)
// 等待该线程终止的时间最长为 millis 毫秒
public final void join(long millis)
// 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒
public final void join(long millis, int nanos)
// 暂停当前正在执行的线程对象,并执行其他线程
public static void yield()
// 使该线程开始执行;Java 虚拟机调用该线程的 run 方法
public void start()
//Object|使当前线程等待
public final void wait()
//Object|唤醒在此对象监视器上等待的单个线程
public final void notify()
//Object|唤醒在此对象监视器上等待的所有线程
public final void notifyAll()
这些方法本质上就是提供了对Monitor的获取和释放的能力,其实也就是线程间通信方式。实际上这些方法在实际应用中是容易出现错误的,而在JDK5以后就引入了并发包。
讲完线程的状态转换后,要提及一个概念:守护线程
所谓的守护线程就是有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程
在Java中通过Thread类定义的setDaemon方法来设置线程为守护线程。需要注意的是:必须在线程启动之前设置其为守护线程。
线程安全
在前面的时候就说过,线程是一条执行任务的路径。那么当多条执行路径访问同一个数据的时候(这里的访问行为包括了修改),就会导致读取时产生不正确的结果。何为不正确,即没有正常地完成自己的任务,就好比产生的结果是错误的。
因此线程安全的概念就因此而提出,线程安全是一个多线程环境下正确性的概念,也就是说,保证多线程环境下共享的、可修改的数据的正确性。
这里以一段代码为例,如下所示:
public class Demo01 implements Runnable{
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("守护线程" + "---" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
});
t1.setDaemon(true);
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("用户线程" + "---" + i);
try {
Thread.sleep(100);
if (i == 10)
break;
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
}).start();
t1.start();
new Thread(new Demo01()).start();
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("用户线程main" + "---" + i);
try {
Thread.sleep(100);
if (i == 15)
break;
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
}
结果如下所示:
用户线程---0
用户线程main---0
守护线程---0
用户线程---1
用户线程main---1
用户线程---2
用户线程main---2
用户线程main---3
用户线程---3
用户线程main---4
用户线程---4
用户线程main---5
用户线程---5
守护线程---1
用户线程main---6
用户线程---6
用户线程---7
用户线程main---7
用户线程---8
用户线程main---8
用户线程main---9
用户线程---9
用户线程main---10
用户线程---10
守护线程---2
用户线程main---11
用户线程main---12
用户线程main---13
用户线程main---14
用户线程main---15
守护线程---3
从结果上不难发现,当主线程的任务完成后,守护线程t1就自行停止。当JVM发现只有守护线程的时候就将进程结束。
基本特性
这里先稍微提一下线程安全中需要保证的几个基本特性,后续过程会证明。
- 原子性:相关操作的中途不会被其他线程干扰,通过同步机制实现
- 可见性:一个线程修改了共享变量,其状态能够立即被其他线程知晓,volatile就是负责保证这个性质
- 有序性:保证线程内串行语义,避免指令出现重排等现象
同步机制
在讲同步机制前,先看一个经典的例子——售票:
public class SellTickets01 implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
} else {
System.exit(0);
}
}
}
}
public class Demo02 {
public static void main(String[] args) {
SellTickets01 st = new SellTickets01();
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行结果如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/34473af9105686f3632e790a6fad37e3.png)
从结果上来看,其并没有好好地完成自己的任务,而这就是多线程中的线程安全问题。其产生的原因就是:当一个线程在执行操作共享数据的多段代码时,其他线程也参与了运算,就会导致线程安全问题的产生。
而为了解决这个线程安全问题,就产生了同步机制synchronized。
synchronized机制
synchronized机制提供了互斥的语义性以及可见性,当一个线程获取当前锁时,其他试图获取锁的线程只能是等待状态亦或者阻塞状态。
那解决思路其实就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些线程代码的时候,其他线程不可以参与运算。也就是说,必须要当前线程完成自己的任务,其他线程才能开始参与运算。那在Java中其实就是以下列格式来体现的(即同步代码块):
synchronized(obj) {
//...操作共享数据的代码
}
其中的obj则是代表锁对象。除此之外还能够用synchronized修饰方法。如下所示:
public synchronized void method() { }
知道了用法以后,我们再对上面的售票例子中对共享数据操作的部分进行修改,如下所示:
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
} else {
System.exit(0);
}
}
结果如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/d5cf66e7034f9855ec07af015bcc7c31.png)
synchronized修饰非静态方法的时候其锁为当前对象,即this。而synchronized同步代码块的锁对象为任意对象;那么如果synchronized修饰的是静态方法,则静态方法的锁对象为该函数所属字节码文件对象,即等同于
synchronized (this.getClass()) {}
synchronized机制我们并没有看到如何操作锁,而在JDK5以后,将同步和锁封装成了对象,并将操作锁的隐式方式定义到了对象中,将隐式行为变成了显式行为。
ReentrantLock机制
ReentrantLock,译为再入锁,是JDK5提供的锁接口的实现类,其提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。比如说,其可以控制公平性。所谓的公平性即在对资源相互竞争的场景中,公平性为真时,会倾向于将锁赋予等待时间最久的线程。需要说明的是, 如果没有特殊需求,尽量不要设置公平性。因为为了保证公平性会引入额外的开销,吞吐量自然会下降。
那么到底什么是再入?其实就是表示当一个线程试图获取一个其已经获取过的锁时,这个获取动作就自动成功。所谓的自动完成就是成功返回这个锁。
那其实这样就是说明锁的持有是以线程为单位,而不是基于调用次数的。
接下来演示一下如何使用这个ReentrantLock,代码如下所示:
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 需要处理的代码
} finally {
lock.unlock();
}
代码中需要注意的是,为了保证锁的释放,需要在finally块中调用unlock()方法。
在上面说过,ReentrantLock相比synchronized具有更广泛、更精细的同步操作。比如说:带超时的获取锁尝试、响应中断请求等操作。但是更需要了解的是在ReentrantLock中涉及到的一个条件变量conditionObject,其将线程状态转换(wait、notify)等操作转化为相应的对象。
查看API,可以看到Condition类有以下方法:
// 造成当前线程在接到信号或被中断之前一直处于等待状态。
void await();
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
boolean await(long time, TimeUnit unit)
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout)
// 造成当前线程在接到信号之前一直处于等待状态。
void awaitUninterruptibly()
// 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
boolean awaitUntil(Date deadline)
// 唤醒一个等待线程。
void signal()
// 唤醒所有等待线程。
void signalAll()
接下来就用ReentrantLock以及Condition分别实现等待通知机制以及阻塞队列。
代码如下所示:
public class MyThread03 implements Runnable {
private ReentrantLock lock;
private Condition condition;
public MyThread03(ReentrantLock lock) {
this.lock = lock;
condition = lock.newCondition();
}
public Condition getCondition() {
return condition;
}
@Override
public void run() {
lock.lock();
try {
condition.signal();
System.out.println("等待此锁的线程数:" + lock.getQueueLength());
System.out.println(Thread.currentThread().getName() + "线程唤醒");
} finally {
lock.unlock();
}
}
}
public class Demo03 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread03 thread = new MyThread03(lock);
new Thread(thread).start();
new Thread(thread).start();
System.out.println("主线程等待唤醒");
try {
thread.getCondition().await();
System.out.println("main等待此锁的线程数:" + lock.getQueueLength());
} finally {
lock.unlock();
}
System.out.println("主线程唤醒");
}
}
结果如下所示:
主线程等待唤醒
等待此锁的线程数:2
Thread-0线程唤醒
等待此锁的线程数:1
Thread-1线程唤醒
main等待此锁的线程数:0
主线程唤醒
首先先了解一下阻塞队列的特点:
当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。
根据特点,编写代码如下:
public class BlockedQueue<E> {
// 队列最大容量
private int size;
// 底层结构
private LinkedList<E> list;
// 再入锁
private ReentrantLock lock = new ReentrantLock();
// 队列为满时的等待条件
private Condition isFull = lock.newCondition();
// 队列为空时的等待条件
private Condition isEmpty = lock.newCondition();
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public BlockedQueue(int size) {
super();
list = new LinkedList<E>();
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
while (list.size() == size)
isFull.await();
System.out.println("入队元素:" + e);
list.addLast(e);
isEmpty.signal();
} finally {
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
E e;
lock.lock();
try {
while (list.size() == 0)
isEmpty.await();
e = list.removeFirst();
System.out.println("出队元素:" + e);
isFull.signal();
return e;
} finally {
lock.unlock();
}
}
}
演示代码如下所示:
public class Demo04 {
public static void main(String[] args) {
//一类线程负责put
final BlockedQueue<Integer> queue = new BlockedQueue<Integer>(3);
for (int i = 0; i < 10; i++) {
final int ele = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.enqueue(ele);
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}).start();
}
// 一类线程负责take
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.dequeue();
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}).start();
}
}
}
结果如下所示:
入队元素:0
入队元素:4
入队元素:1
出队元素:0
出队元素:4
出队元素:1
入队元素:6
入队元素:3
入队元素:2
出队元素:6
入队元素:9
出队元素:3
入队元素:5
出队元素:2
出队元素:9
出队元素:5
入队元素:8
出队元素:8
入队元素:7
出队元素:7
这里只是普通模拟了一下阻塞序列,在java.util.concurrent包下就有官方自己实现的阻塞序列,有兴趣可以去研究。
多线程中的单例模式
众所周知,单例模式的实现有“懒汉式”和“饿汉式”两种,而饿汉式在多线程中是不会出现线程安全问题的。
接下来回顾一下懒汉式的实现方式,代码如下所示:
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
// 注意这里
if (instance == null)
instance = new Singleton();
return instance;
}
}
注意一下注释的地方,当有多个线程调用getInstance的时候,在判断的时候可能就会出现多个线程通过判断而分别创建了不同的对象,这已经破坏了单例模式的特点。
因此,懒汉式是存在着线程安全问题。那么该怎么解决呢?首先需要知道的是,不能直接在getInstance()方法上直接使用synchronized机制。当每一次调用该方法的时候都要判断锁,加锁是会增加开销导致影响效率。
因此又有了两个不同的加锁方式:DCL式和登记式,而在Java的实现中需要掌握的只有后者——登记式。
这里顺带提一下DCL吧,所谓的DCL(ouble-checked locking)就是双重检验锁,即代码如下所示:
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 注意这里,因为是静态方法,没有this
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
看着好像把问题解决了,其实并不然。因为对于JVM来说,其不一定是先初始化了堆内的实例化的对象,再将初始化后的实例化对象赋值给左边。其也有可能是先将堆内实例化的对象先赋值给左边,再对实例化的对象进行初始化。
那么这里就会产生一个问题:当一个线程发生了后者的顺序时并且还未初始化对象就释放了锁,那么当第二个线程访问该方法时就会直接返回不为空的instance,但是此时并没有初始化对象,也就是说,调用时就会产生错误。
接下来就需要讲最关键的登记式,所谓的登记式其实就是使用静态内部类的方式来解决单例模式的多线程安全问题。
先来看实现代码,如下所示:
class Singleton {
private Singleton() {}
private static class SingletonInner {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonInner.instance;
}
}
从代码上可以发现,只有当getInstance方法被调用时才会加载SingletonInner类,才会实例化instance。更加需要知道的是,这其实是利用了类加载器的机制,当一个类被加载的时候,这个类的加载过程是线程互斥的,也就是说,这样就可以保证instance只被创建一次,并且保证了堆内对象初始化完毕后才会赋值给instance。
这样就成功地实现了懒汉式加载又将线程安全问题解决了。
死锁
所谓的死锁,即是一种特定的程序状态,大多数是指两个或者两个以上的线程之间,由于互相持有对方需要的锁而处于堵塞状态的情况。
接下来用代码演示一下何为死锁现象:
public class DeadLock extends Thread {
private String lockA;
private String lockB;
public DeadLock(String name, String lockA, String lockB) {
super(name);
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(this.getName() + " get " + lockA);
try {
Thread.sleep(1000);
synchronized (lockB) {
System.out.println(this.getName() + " get " + lockB);
}
} catch (InterruptedException e) {
}
}
}
}
public class Demo05 {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
DeadLock t1 = new DeadLock("线程1", lockA, lockB);
DeadLock t2 = new DeadLock("线程2", lockB, lockA);
t1.start();
t2.start();
}
}
结果如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/218573b2d79be35fc21c82fd4b19450a.png)
当运行了上面的代码会发现和结果显示一样并且代码无法正常执行完毕,而这样的情况就是死锁的现象。线程1需要锁A和锁B的时候,发现锁B被线程2持有着,那么就会造成死锁。
介绍完死锁后,那么在实践中又如何定位产生死锁的位置呢?
最常用的方式就是通过Java中自带的jstack工具,亦或者通过Java中的API——ThreadMXBean,其内部提供了一个定位死锁线程的方法。
接下来分别演示这两种方法。首先是jstack工具,使用格式如下所示:
jstack pid
其中,pid为命令参数,代表了进程的ID。对于进程ID的获取,可以通过任务管理器(Windows系列)亦或者ps命令(Linux)来达成。
接着在命令行中调用jstack获取线程栈,结果如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/e49aa2016f3d2763dbfd2f4febbb001f.png)
需要注意的是结果中划红色下划线的部分,可以看到线程当前状态是锁住的(后面跟着的圆括号中的内容到源码分析的时候讲),并且能够知道是哪个代码产生了死锁现象。
总结一下定位死锁的步骤其实就是:根据结果来区分线程状态,从状态中找到发生死锁的线程,查看该线程正等待的锁对象,最后再对比持有状态来定位问题。
接下来演示一下利用Java中的API——ThreadMXBean来完成死锁的定位。
在这之前先来看一个叫ScheduledExecutorService的接口,其作用是将线程池和定时任务结合使用,一般可以通过Executors这个工厂类来创建线程池,如下所示:
// 创建保存corePoolSize个线程的线程池,它可安排在给定延迟后运行命令或者定期地执行。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
在这个ScheduledExecutorService接口中有一个方法scheduleAtFixedRate()会创建一个可定时的周期性运行的操作。如下所示:
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit)
/*
其中,参数解释如下:
command - 要执行的任务(注意类型)
initialDelay - 首次执行的延迟时间
period - 连续执行之间的周期
unit - initialDelay 和 period 参数的时间单位
*/
那么接下来就需要创建一个需要执行的任务,即如下所示:
public class DeadLockCheck implements Runnable {
private ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
@Override
public void run() {
long[] threadIds = mbean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] threadInfos = mbean.getThreadInfo(threadIds);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo);
}
}
}
}
在main方法的开头添加以下代码并运行:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 三秒后每十秒进行一次死锁扫描任务
scheduler.scheduleAtFixedRate(new DeadLockCheck(), 3L, 10L, TimeUnit.SECONDS);
结果如下所示:
线程1 get lockA
线程2 get lockB
"线程2" Id=10 BLOCKED on java.lang.String@52fe85 owned by "线程1" Id=9
"线程1" Id=9 BLOCKED on java.lang.String@c40c80 owned by "线程2" Id=10
从结果上我们就可以很明显地得到发生死锁的线程并且得知产生死锁现象的原因。
从上面举的死锁例子,可以知道产生死锁的原因是出现了锁被一个线程持有,并且该线程自己不会释放,不能被其他线程抢占亦或者是出现了嵌套的synchronized或者lock。那么怎样才能尽可能的避免产生死锁呢?
- 第一,尽量在仅需要锁的情况才让线程持有锁,并且尽量避免出现使用多个锁的情况;
- 第二,如果实在必须要使用多个锁,那么就需要注意锁的持有顺序(比如说银行家算法);
- 第三,可以使用带等待时间的方法,比如说类似于ReentrantLock中有一个tryLock(long timeout, TimeUnit unit)方法,这样就可以有更多的可控性。
死锁的内容就到此结束了,对于死锁其实需要更多到实战中获取经验,但前提是需要了解死锁的原因以及定位,否则也不好维护。
扩充的内容
这一部分的内容相比于上面的内容会更加深一些,也有可能比较难理解。
synchronized底层实现
在讲synchronized底层之前,先讲一下对象头。
对象头
所谓的“对象头”是指对象在内存的存储的其中一块内存空间的别名,其余两块分别是实例数据以及对齐填充。而对象头里又主要分为以下三分部分:
Mark Word:用于存储对象自身的运行时数据。比如hashCode、锁状态标志、线程持有的锁、偏向锁等。
类型指针:对象指向其类元数据的指针。JVM可以通过这个指针来确定这个对象所属哪个类。
记录数组长度数据:这一部分是记录数组长度,但是这一部分数据只有当对象是数组的时候才有。
最需要关注的是Mark Word,其是非固定的数据结构,会根据对象的状态复用自己的存储区域。从OpenJDK中找到markOopDesc类(/src/share/vm/oops/markOop.hpp),通过注释了解到Mark Word的结构如下:
age:GC分代年龄
lock:锁状态标志
biase_lock:偏向锁标志
hash:对象哈希值
epoch:偏向时间戳
JavaThread*:当前线程
这里以64位来看无锁状态和有锁状态时“Mark Word”的默认存储结构:
锁状态 | 54 bit | 2 bit | 1 bit | 4 bit | 1 bit 偏向锁标志 | 2 bit 锁标志 |
---|---|---|---|---|---|---|
无锁状态 | unused | hashCode | unused | 分代年龄 | 0 | 01 |
锁状态 | 54 bit | 2 bit | 1 bit | 4 bit | 1bit 偏向锁标志 | 2 bit 锁标志 |
---|---|---|---|---|---|---|
偏向锁 | 线程ID | epoch | unused | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||
重量级锁 | 指向重量级锁的指针 | 10 | ||||
GC标记 | 空 | 11 |
了解完对象头的“Mark Word”后,我们还需要再稍微了解一个东西:CAS操作(Compare and Swap)。
CAS操作中包含了三个操作数:内存位置、预期原值、新值,如果内存位置的值与预期原值相同,那么处理器就会自动将该位置的值更新为新值;否则,处理器不做任何处理。
先暂时了解到这里,后面会再开一章讲解CAS操作,只需要知道其是一个原子性操作即可。
synchronized本质
在了解完上面的知识后,就可以开始理解原理了。接下来就以这段代码为例:
public class Demo07 implements Runnable {
private boolean flag = false;
@Override
public void run() {
synchronized (this) {
if (flag) {
System.out.println("true");
} else {
System.out.println("false");
}
}
}
public static void main(String[] args) {
}
}
用javap -c命令反汇编其生成的字节码文件,这里抽取run方法部分,如下所示:
public void run();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: getfield #14 // Field flag:Z
8: ifeq 22
11: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
14: ldc #27 // String true
16: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: goto 30
22: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
25: ldc #35 // String false
27: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: aload_1
31: monitorexit
32: goto 38
35: aload_1
36: monitorexit
37: athrow
38: return
Exception table:
from to target type
4 32 35 any
35 37 35 any
从上面反汇编的结果我们可以知道,synchronized关键字其实是由一对monitorenter/monitorexit指令实现的,分别表示了同步块的进入和退出,其实也就是表示了同步块的作用域。但是在这个结果的第31行和第36行却发现了两个monitorexit指令,这又是为什么呢?
在解答这个疑问之前,先来看最后面的一个Exception Table,这个表格的意思就是当from->to的执行路径中出现了type类型异常时,就直接跳转到target+1再往下执行。
既然知道了表达意思,那么就可以看出这其实就是一个隐式的异常捕捉机制(try-finally)。这就是说,第二个monitorexit指令其实是在保证了抛异常时可将锁释放。
说到monitorenter和monitorexit就得说到monitor,也就是所谓的“管程”。那么什么是管程呢?其实就是一个让多个线程在同一时刻有且仅有一个线程可以访问共享资源的机制。而在Java中则是用监视锁Monitor来实现这个机制。
在JDK6以前,Monitor的实现单纯靠操作系统内部的互斥锁(即mutex),这样就会导致从用户态(目态)转换到内核态(管态),而这个操作称为重量级操作。
而在JDK6以后,JVM提供了三种不同的Monitor实现,即偏向锁、轻量级锁以及重量级锁。
需要知道的是:synchronized是JVM内部的Intrinsic Lock(内在锁)。因此我们需要到JVM的代码中找到其底层实现。Java代码运行可能是解释模式亦或者编译模式,因此synchronized实现也分布在了不同的模块下。
偏向锁
找到解释器类:interpreterRuntime.cpp。其中有一段代码体现了synchronized的主要逻辑,如下所示:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
Handle h_obj(thread, elem->obj());
// 判断是否使用偏向锁
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
IRT_END
其中,UseBiasedLocking是用于判断有没有开启偏向锁。在JVM启动的时候可以设置是否开启偏向锁。当开启偏向锁的时候,就会进入fast_enter操作,也就是获取锁的操作;而当没有开启偏向锁时,就会进入到slow_enter操作,也就是绕过了获取偏向锁而直接进入了获取轻量级锁的操作。
那么接下来搜索代码库中的fast_enter,可以找到一个synchronizer.cpp的文件,如下所示:
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
// 判断是否使用偏向锁,如果是则获取偏向锁,如果不是则向下执行|额外保证性检查
if (UseBiasedLocking) {
// 如果不在安全点,则获取当前偏向锁状态或者进行撤销与重偏向操作
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
// revoke_and_rebias方法的返回值确定了是撤销与重偏向操作就结束该方法
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
// 如果在安全点,则进行在安全点的逻辑操作
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
// 如果不使用偏向锁则进行轻量级锁的获取
slow_enter (obj, lock, THREAD) ;
}
其中,对于上面所提及的“安全点”,暂时性先认为其代表一个位置,在这个位置上可以获取锁的状态。接下来找到revoke_and_rebias方法,如下所示:
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");
// 1.获取Mark World,即对象的运行时数据
markOop mark = obj->mark();
/*
2.
判断偏向锁的状态是否为匿名偏向。
其实就是判断该对象有没有被其他线程占用,
即线程有没有获取偏向锁。
*/
if (mark->is_biased_anonymously() && !attempt_rebias) {
// 如果没有被占用,并且attempt_rebias不为真 则执行以下代码
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
// 这里进行的是CAS操作来设置偏向锁的状态,即将锁绑定至当前线程
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
/*
以下判断是检查CAS操作成功与否。
如果操作失败则说明存在竞争关系,
那么就让锁的状态为撤销状态。
所谓的撤销是指将锁对象改为非偏向锁状态
*/
if (res_mark == biased_value) {
return BIAS_REVOKED;
}
// 3.判断对象Mark Word的状态是偏向模式
} else if (mark->has_bias_pattern()) {
// Klass类中包含了元数据和方法信息,其是用于描述Java类
Klass* k = obj->klass();
// prototype_header其实就是对象头,存储类的epoch以及偏向锁定信息
markOop prototype_header = k->prototype_header();
// 4.如果对象相对应的class没有开启偏向模式
if (!prototype_header->has_bias_pattern()) {
// 则取消偏向操作,即让偏向锁状态为撤销状态
markOop biased_value = mark;
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");
return BIAS_REVOKED;
/*
5.判断mark的epoch和类本身的epoch是否一致
判断epoch其实就是判断当前锁的偏向线程是否处于活动状态
*/
} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
/*
如果不一致,则说明其偏向已经过期
即说明偏向锁的偏向线程已不再存活,偏向锁可以再偏向
*/
// 6.判断偏向锁是否开启
if (attempt_rebias) {
// 利用CAS操作更新线程ID、epoch以及分代年龄
assert(THREAD->is_Java_thread(), "");
markOop biased_value = mark;
markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
/*
如果操作失败,则说明该对象的锁状态已经从撤销偏向到另一线程
那么就让锁的状态为撤销后重偏向
*/
return BIAS_REVOKED_AND_REBIASED;
}
} else {
// 利用CAS操作更新分代年龄
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
/*
以下判断是检查CAS操作成功与否。
如果操作失败则说明存在竞争关系,
那么就让锁的状态为撤销状态。
*/
return BIAS_REVOKED;
}
}
}
}
/*
以下代码省略...通过注释了解即可
没有执行偏向则通过启发式策略决定是撤销操作还是重偏向操作
*/
}
那么这里总结一下偏向锁获取的步骤:
1.获取synchronized当前偏向锁的Mark Word,判断其内部的线程ID是否为空(即判断有无占用)并且attempt_rebias是否为false。如果判断满足条件,则通过CAS操作将当前对象设置为无锁。如果CAS操作失败则将偏向锁设为撤销状态。
2.当第一步判断不满足条件时,再判断当前偏向锁是否已经锁定即判断当前对象的Mark Word是否开启了偏向锁模式,满足条件则继续下一步。
3.判断对象相应的class有没有开启偏向模式,如果没有开启偏向模式则撤销偏向操作。如果条件不满足则进行下一步。
4.判断偏向锁是否过期,即判断当前锁的偏向线程是否处于活动状态。如果偏向锁的偏向线程已不再存活,则进行第五、六步判断。
5.在偏向锁的偏向线程已不再存活的条件下,如果偏向锁开启,就通过CAS操作更新线程ID、epoch以及分代年龄。如果CAS操作失败则说明产生了竞争,也就是说该对象的锁状态已经从撤销偏向操作到另一个线程。那么则当前偏向锁的状态为撤销后重偏向。
6.在偏向锁的偏向线程已不再存活的条件下,如果偏向锁关闭,那么就只通过CAS操作更新分代年龄。如果CAS操作失败了,说明存在线程竞争,将偏向锁的状态设为撤销状态。
这样也就说明了:当一个线程获取到偏向锁后,在下一次该线程进入或者退出同步块时都不需要CAS操作来加锁和解锁,只需要判断指向当前线程的偏向锁是否存在对象头中即可。
接下来我们再看如果是在安全点调用的revoke_at_safepoint方法的实现,代码如下所示:
void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
oop obj = h_obj();
// 更新并获取偏向锁偏向次数与撤销次数
HeuristicsResult heuristics = update_heuristics(obj, false);
// 判断是否为批量撤销
if (heuristics == HR_SINGLE_REVOKE) {
// 如果是单次撤销则执行这一部分
revoke_bias(obj, false, false, NULL);
} else if ((heuristics == HR_BULK_REBIAS) ||
(heuristics == HR_BULK_REVOKE)) {
// 如果是多次撤销或者多次偏向则执行这一部分
bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
}
clean_up_cached_monitor_info();
}
那么我们再来看其两个方法,首先先看revoke_bias,如下所示:
static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread) {
markOop mark = obj->mark();
// 如果对象Mark Word的状态不是偏向模式,则返回非偏向模式的状态
if (!mark->has_bias_pattern()) {
// TraceBiasedLocking默认为false
if (TraceBiasedLocking) {
ResourceMark rm;
tty->print_cr(" (Skipping revocation of object of type %s because it's no longer biased)",
obj->klass()->external_name());
}
return BiasedLocking::NOT_BIASED;
}
// 获取对象Mark Word的分代年龄
uint age = mark->age();
// 偏向锁的头(匿名偏向模式)
markOop biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
// 非偏向锁的头(无锁模式)
markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
// 获取偏向线程
JavaThread* biased_thread = mark->biased_locker();
// 判断当前偏向锁的偏向线程ID是否为空
if (biased_thread == NULL) {
// 当调用锁对象的hashCode方法可能会进入这个逻辑
// 如果不允许重偏向则设置对象的Mark Word为无锁模式
if (!allow_rebias) {
obj->set_mark(unbiased_prototype);
}
// 撤销完毕
return BiasedLocking::BIAS_REVOKED;
}
// 判断当前偏向锁偏向的线程是否存活
bool thread_is_alive = false;
// 判断当前线程是否为偏向线程
if (requesting_thread == biased_thread) {
//如果参数传递的当前线程就是偏向线程,就表示当前偏向锁的线程存活
thread_is_alive = true;
} else {
// 遍历jvm当前所有线程,遍历过程中如果找到了偏向线程则说明其存活
for (JavaThread* cur_thread = Threads::first(); cur_thread != NULL; cur_thread = cur_thread->next()) {
if (cur_thread == biased_thread) {
thread_is_alive = true;
break;
}
}
}
// 如果偏向线程非存活状态,则执行以下代码
if (!thread_is_alive) {
/*
如果允许重偏向的话,就将对象的Mark Word设置为匿名偏向状态
否则就将Mark Word设置为无锁状态
*/
if (allow_rebias) {
obj->set_mark(biased_prototype);
} else {
obj->set_mark(unbiased_prototype);
}
return BiasedLocking::BIAS_REVOKED;
}
// --当偏向线程还存活的时候,执行以下代码--
/*
通过最年轻到最老的顺序遍历线程栈中所有的锁记录
这里其实是为了判断偏向线程是否还在同步块中
*/
// cached_monitor_info是该线程拥有的锁对象的记录,按照加锁顺序的逆序排列
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread);
BasicLock* highest_lock = NULL;
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
/*
寻找线程对应的锁记录,如果找到了锁记录说明偏向线程仍然在执行同步代码块中的任务
*/
if (mon_info->owner() == obj) {
// 这里是将栈中线程的mark设为空
markOop mark = markOopDesc::encode((BasicLock*) NULL);
highest_lock = mon_info->lock();
highest_lock->set_displaced_header(mark);
} else {
if (TraceBiasedLocking && Verbose) {
tty->print_cr(" mon_info->owner (" PTR_FORMAT ") != obj (" PTR_FORMAT ")",
p2i((void *) mon_info->owner()),
p2i((void *) obj));
}
}
}
// 如果线程拥有锁
if (highest_lock != NULL) {
// 则将线程的锁标志设置为无锁状态
highest_lock->set_displaced_header(unbiased_prototype);
// 然后将displaced Mark Word复制到线程栈中
obj->release_set_mark(markOopDesc::encode(highest_lock));
} else {
// 如果线程不再拥有锁,如果允许重偏向,则将状态设为匿名偏向状态(即线程ID置0)
if (allow_rebias) {
obj->set_mark(biased_prototype);
} else {
// 如果不允许重偏向,则设置为无锁状态
obj->set_mark(unbiased_prototype);
}
}
return BiasedLocking::BIAS_REVOKED;
}
从上面的代码我们就可以总结偏向锁的撤销过程步骤:
1.在满足是偏向锁的条件下,判断偏向锁偏向的线程ID是否不为null,如果不为null则进行下一步。
2.当线程不为null的时候,判断偏向锁的线程是否存活。当线程不是存活时执行第三步,反之,执行第四步。
3.当线程不是存活状态时,判断是否允许重偏向。如果允许重偏向,则将对象的Mark Word设置为匿名偏向模式(即Thread ID为0的情况。反之,则设置为无锁状态(锁标志位为0)。
4.当线程是存活状态时,判断偏向锁偏向的线程是否仍然拥有锁。如果其仍然拥有锁,则将其升级为轻量级锁。如果其已经没有拥有锁,那么如果允许重偏向,则将对象的Mark Word设置为匿名偏向模式。反之,则偏向撤销,设置为无锁状态。
接着我们再来看bulk_revoke_or_rebias_at_safepoint方法,代码如下所示:
static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o,
bool bulk_rebias,
bool attempt_rebias_of_object,
JavaThread* requesting_thread) {
assert(SafepointSynchronize::is_at_safepoint(), "must be done at safepoint");
// 设置时间戳
jlong cur_time = os::javaTimeMillis();
o->klass()->set_last_biased_lock_bulk_revocation_time(cur_time);
Klass* k_o = o->klass();
Klass* klass = k_o;
// 判断是否为批量重偏向
if (bulk_rebias) {
// 如果是则执行这一部分
if (klass->prototype_header()->has_bias_pattern()) {
// 获取增加前类中的epoch
int prev_epoch = klass->prototype_header()->bias_epoch();
// 这里是将epoch自增
klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
int cur_epoch = klass->prototype_header()->bias_epoch();
// 遍历所有线程栈
for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
/*
需要注意的是,这里只会更新正在使用的偏向锁对象的epoch值
也就是说,这样就保证了线程安全性
*/
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
oop owner = mon_info->owner();
markOop mark = owner->mark();
// 判断类型是否为klass并且是偏向锁
if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
// 更新满足类型为klass的偏向锁对象的epoch
owner->set_mark(mark->set_bias_epoch(cur_epoch));
}
}
}
}
// 对当前的偏向锁对象进行重偏向
revoke_bias(o, attempt_rebias_of_object && klass->prototype_header()->has_bias_pattern(), true, requesting_thread);
} else {
// 批量撤销部分
/*
通过查看源码可知,prototype()方法返回的是一个关闭了偏向模式的prototyp
*/
klass->set_prototype_header(markOopDesc::prototype());
// 遍历所有线程栈
for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo* mon_info = cached_monitor_info->at(i);
oop owner = mon_info->owner();
markOop mark = owner->mark();
// 依旧寻找满足类型为klass的偏向锁,并且将锁的偏向撤销(即变为无锁模式)
if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
revoke_bias(owner, false, true, requesting_thread);
}
}
}
/*
这里将会完成撤销当前锁对象的偏向模式
也就是说,以后当该类的实例获得锁时,其会将锁升级为轻量级锁。
而实例的Mark Word的状态则是无锁模式
*/
revoke_bias(o, false, true, requesting_thread);
}
BiasedLocking::Condition status_code = BiasedLocking::BIAS_REVOKED;
if (attempt_rebias_of_object &&
o->mark()->has_bias_pattern() &&
klass->prototype_header()->has_bias_pattern()) {
// 创建一个偏向当前请求线程的Mark Word
markOop new_mark = markOopDesc::encode(requesting_thread, o->mark()->age(),
klass->prototype_header()->bias_epoch());
// 设置当前锁对象的Mark Word为上面新建的Mark Word
o->set_mark(new_mark);
status_code = BiasedLocking::BIAS_REVOKED_AND_REBIASED;
}
return status_code;
}
其实这个方法在revoke_and_rebias中也出现过,最后的注释部分说明了是以启发式来决定是撤销还是重偏向操作,而后面等到虚拟机运行到safepoint时,就会执行VM_BulkRevokeBias中的bulk_revoke_or_rebias_at_safepoint方法。
接下来用一张流程图来表示偏向锁的过程,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/e35ce465295089cb4ad46513db320479.jpeg)
最后总结一下:实际上,大多数情况锁就不存在多线程竞争,而且总是由同一个线程多次获得。在早期的重量级操作未免过于代价过重。因此,JDK6引入的偏向锁就解决了这个问题。偏向锁保证了同一时间段同一时刻有且仅有一个线程请求同一把锁。
但其实在偏向锁中有一个问题,在前面获取过程中如果两个线程之间通过CAS操作竞争偏向锁失败了,到达safepoint时候撤销偏向锁的时候会导致stop the world(即暂停所有当前运行的线程),进一步导致性能的下降。因此如果存在有锁竞争的情况下,应该禁用偏向锁。
那么如何禁用呢?通过在Eclipse中的Run Configurations的Arguments标签配置以下启动参数:
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
对于偏向锁再补充一个知识:虚拟机在启动的时候偏向锁会延迟4秒,因为JVM在启动的时候需要加载资源,而这些对象加上偏向锁是没有任何意义的,这样就减少了偏向锁撤销的成本。其实在JDK1.6以后,在没有禁止偏向锁延迟的情况下,使用的其实是轻量级锁。如果禁止偏向锁延迟,使用的则是偏向锁。
轻量级锁
在前面研究源码的时候说过,如果当偏向锁没有开启就会进入slow_enter方法,亦或者当存在多个线程竞争竞争偏向锁就会使偏向锁升级为轻量级锁。
这里需要补充一个小知识点,即Displaced Mark Word。
线程在执行同步代码块之前(即锁标志为01状态时),JVM会在当前线程的帧栈中划分一个名为锁记录(Lock Record)的内存空间。这个内存空间用于存储对象头的Mark Word的拷贝,而这个拷贝就被命名为Displaced Mark Word。在这个名为Lock Record的内存空间中,还有一个指向对象的指针。这里用一幅图来表示Lock Record,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/aa65552cfba972488b9b96a09962a84d.png)
在OpenJDK中Lock Record的实现是通过BasicLock以及BasicObjectLock来完成的,其数据结构如下所示:
class BasicLock VALUE_OBJ_CLASS_SPEC {
private:
volatile markOop _displaced_header;
public:
markOop displaced_header() const {
return _displaced_header;
}
void set_displaced_header(markOop header) {
_displaced_header = header;
}
};
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
private:
BasicLock _lock; // 锁对象
oop _obj; // 持有该锁的Java对象指针
};
那么接下来我们就可以查看具体实现轻量级锁的slow_enter方法,代码如下所示:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
// 获取对象的Mark Word
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");
/*
bool is_neutral() const {
return (mask_bits(value(), biased_lock_mask_in_place) == unlocked_value);
}
其实就是判断当前Mark是否为无锁状态
*/
if (mark->is_neutral()) {
// 无锁状态则先存储其Mark Word至lock的_displaced_header
lock->set_displaced_header(mark);
/*
这里通过CAS操作将对象的Mark Word更新为指向锁记录的指针
oop operator () () const {
return obj();
}
oop obj() const {
return _handle == NULL ? (oop)NULL : *_handle;
}
markOop* mark_addr() const {
// 锁对象的Mark Word地址
return (markOop*) &_mark;
}
*/
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
// 更新成功则表示线程成功获得锁,就继续向下执行。
TEVENT (slow_enter: release stacklock) ;
return ;
}
/*
当前Mark是加锁状态,那么就进一步判断对象头是否指向了当前线程的栈帧(即Lock Record)
*/
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
/*
满足条件则说明是锁重入(即当前线程持有该轻量级锁的情况)
就设置Displaced Mark Word为null
这是因为每次获取轻量级锁时都会创建一个Lock Record,那么除去第一个以外都需要设置为null
*/
lock->set_displaced_header(NULL);
return;
}
/*
当上面所有条件都不满足时就会发生轻量级锁升级为重量级锁的行为
实际上存在多个线程竞争轻量级锁,而导致了轻量级锁膨胀升级为重量级锁
*/
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
接下来我们再来看轻量级撤销的过程,代码如下所示:
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");
// 获取Lock Record里的Displaced Mark Word
markOop dhw = lock->displaced_header();
markOop mark ;
if (dhw == NULL) {
/*
如果Lock Record中的Displaced Mark Word为空
则说明锁为重量级锁,则直接返回
*/
mark = object->mark() ;
assert (!mark->is_neutral(), "invariant") ;
if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
}
if (mark->has_monitor()) {
ObjectMonitor * m = mark->monitor() ;
assert(((oop)(m->object()))->mark() == mark, "invariant") ;
assert(m->is_entered(THREAD), "invariant") ;
}
return ;
}
mark = object->mark() ;
/*
判断对象的Mark Word是否指向了Lock Record
即判断当前线程是否拥有轻量级锁
*/
if (mark == (markOop) lock) {
assert (dhw->is_neutral(), "invariant") ;
/*
如果当前线程拥有轻量级锁
就通过CAS操作将锁对象的Mark Word更新为指向Lock Record的指针
CAS操作成功则说明成功释放锁
*/
if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
TEVENT (fast_exit: release stacklock) ;
return;
}
}
/*
CAS操作失败则表明有竞争情况出现
那么就将轻量级锁升级为重量级锁
*/
ObjectSynchronizer::inflate(THREAD, object)->exit (THREAD) ;
}
这里用流程图来表示轻量级锁的获取和解锁过程,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/afc43bce4c341fa72bf9ed593eefabcd.jpeg)
在这个流程图中有个需要注意的地方——自旋。所谓的自旋就是指当存在一个线程来竞争锁时,这个线程会在原地循环等待获取锁,而不是把这个线程给阻塞。
在前面的时候说过,mutex互斥同步机制是操作系统级别管态和目态之间的切换。因此如果频繁地出现阻塞和唤醒线程对CPU来说其实是一件非常负重的工作,这样就导致了并发性能的降低。
其原理实际上就是CPU空转等待获取锁的控制权,等同于一个while(1)。但其实如果同步代码块耗费的时间过多,其他线程原地自旋就会消耗CPU。实际上自旋锁在JDK4就已经出现了,只是并没有使用。在JDK6以后又出现了一个叫自适应自旋锁。
所谓的自适应自旋锁就是线程空循环等待的自旋次数不是固定的,而是会动态地根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定自旋次数。比如说,如果有一个线程自旋成功了,那么下次自旋的次数就会增加。反之,则会减少自旋次数或者甚至不自旋直接升级为重量级锁。
接下来我们来看一下重量级锁的相关代码。
重量级锁
延续上面研究的源码,继续看inflate方法,代码如下所示:
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
assert (Universe::verify_in_progress() ||
!SafepointSynchronize::is_at_safepoint(), "invariant") ;
for (;;) {
const markOop mark = object->mark() ;
assert (!mark->has_bias_pattern(), "invariant") ;
/*
mark有如下几个状态:
Inflated(重量级锁状态) - 直接返回
Stack-locked(轻量级锁状态) - 膨胀
INFLATING(膨胀中) - 等待直到膨胀完成
Neutral(无锁状态) - 膨胀
BIASED(偏向锁) - 在这里不会存在的状态
*/
// CASE: 重量级锁状态
if (mark->has_monitor()) {
// 如果是重量级锁状态则直接返回
ObjectMonitor * inf = mark->monitor() ;
assert (inf->header()->is_neutral(), "invariant");
assert (inf->object() == object, "invariant") ;
assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
return inf ;
}
// CASE:膨胀中
if (mark == markOopDesc::INFLATING()) {
/*
当mark处于膨胀中状态时,说明某线程正在位于锁膨胀时期
则调用ReadStableMark方法来自旋,执行完毕后再通过continue继续检查
*/
TEVENT (Inflate: spin while INFLATING) ;
ReadStableMark(object) ;
continue ;
}
// CASE: 轻量级锁状态
if (mark->has_locker()) {
// 为线程分配一个ObjectMonitor监视器对象
ObjectMonitor * m = omAlloc (Self) ;
// 初始化值
m->Recycle();
m->_Responsible = NULL ;
m->OwnerIsThread = 0 ;
m->_recursions = 0 ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;
// 通过CAS操作将锁的状态设置成INFLATING状态
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
if (cmp != mark) {
// 如果CAS操作失败,则继续检查
omRelease (Self, m, true) ;
continue ;
}
/*
获取栈中的Displaced Mark Word
然后做相应的设置
设置monitor的header为Displaced Mark Word
owner为Lock Reord
object为锁对象
*/
markOop dmw = mark->displaced_mark_helper() ;
m->set_header(dmw) ;
m->set_owner(mark->locker());
m->set_object(object);
// 将锁对象头的Mark Word设置为重量级锁状态
guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
object->release_set_mark(markOopDesc::encode(m));
if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
TEVENT(Inflate: overwrite stacklock) ;
if (TraceMonitorInflation) {
if (object->is_instance()) {
ResourceMark rm;
tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
(void *) object, (intptr_t) object->mark(),
object->klass()->external_name());
}
}
return m ;
}
// CASE: 无锁状态
assert (mark->is_neutral(), "invariant");
/*
设置monitor的header为Mark Word
owner为null,object为锁对象
*/
ObjectMonitor * m = omAlloc (Self) ;
m->Recycle();
m->set_header(mark);
m->set_owner(NULL);
m->set_object(object);
m->OwnerIsThread = 1 ;
m->_recursions = 0 ;
m->_Responsible = NULL ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class
// 通过CAS操作替换对象头的mark word为重量级锁状态
if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
/*
如果CAS操作不成功
则说明存在另外一个线程在进行膨胀并释放其monitor
*/
m->set_object (NULL) ;
m->set_owner (NULL) ;
m->OwnerIsThread = 0 ;
m->Recycle() ;
omRelease (Self, m, true) ;
m = NULL ;
continue ;
}
if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
TEVENT(Inflate: overwrite neutral) ;
if (TraceMonitorInflation) {
if (object->is_instance()) {
ResourceMark rm;
tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
(void *) object, (intptr_t) object->mark(),
object->klass()->external_name());
}
}
return m ;
}
}
在完成inflate方法后将返回一个ObjectMonitor对象。那么在讲下面的内容之前,先补充一下ObjectMonitor类的知识。
所谓的ObjectMonitor就是一个监视器对象,其结构如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/5f294af7a0f8d7177155a164af2edf5e.png)
而在ObjectMonitor类中,其分别有以下属性代表图中的几个字段:_cxq代表ContentionList、_EntryList代表EntryList以及_WaitSet代表WaitSet,这三个属性都是由ObjectWriter组成,其内部数据结构是链表结构。
当膨胀完毕后就会调用ObjectMonitor的enter方法(源码),如下所示:
void ATTR ObjectMonitor::enter(TRAPS) {
/*
锁膨胀完了之后不代表线程已经竞争到锁
而这个方法需要进行的就是锁竞争
*/
Thread * const Self = THREAD ;
void * cur ;
// 通过CAS操作将owner指向Self
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
// 如果CAS操作成功则当前线程直接获得锁
assert (_recursions == 0, "invariant") ;
assert (_owner == Self, "invariant") ;
return ;
}
//如果CAS操作后的值是Self那说明是锁重入情况
if (cur == Self) {
_recursions ++ ;
return ;
}
// 如果当前线程之前拥有轻量级锁,此时cur不为空,并且为指向Lock Record的指针
if (Self->is_lock_owned ((address)cur)) {
// 重入次数设为1
_recursions = 1 ;
// 将owner从Lock Record改为当前线程
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
Self->_Stalled = intptr_t(this) ;
// 这里是自旋操作
if (Knob_SpinEarly && TrySpin (Self) > 0) {
assert (_owner == Self, "invariant") ;
assert (_recursions == 0, "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
Self->_Stalled = 0 ;
return ;
}
// 分配系统同步操作的参数
JavaThread * jt = (JavaThread *) Self ;
Atomic::inc_ptr(&_count);
EventJavaMonitorEnter event;
{
// 更改Java的线程状态
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
// 设置当前Monitor对象为当前线程等待的monitorObject
Self->set_current_pending_monitor(this);
DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_enter()) {
/*
如果当前线程没有持有该监视器并且没有出现在任何线程队列上
说明JVMTI_EVENT_MONITOR_CONTENDED_ENTER事件的相关处理程序
不能使用与此Monitor关联的事件的unpark()方法
JVMTI_EVENT_MONITOR_CONTENDED_ENTER:
线程将要进入某个指定 Monitor 资源所引发的事件
*/
JvmtiExport::post_monitor_contended_enter(jt, this);
}
OSThreadContendState osts(Self->osthread());
ThreadBlockInVM tbivm(jt);
for (;;) {
jt->set_suspend_equivalent();
// 调用系统同步操作
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
}
从代码上来看就可以知道当膨胀完以后进入enter方法还会进行相关的获取锁操作。当锁是无锁状态、锁重入亦或者先前持有轻量级所的线程为当前线程那么就进行相应的操作即可返回。当竞争锁竞争不成功时,亦或者说当锁获取不成功时,就会进入无限循环,反复调用enterI方法获得锁。
接下来再看enterI代码,如下所示:
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
/*
_Responsible:
表示的是当产生竞争时,会选择一个线程作为_Responsible
然后让其调用带时间参数的park方法
_succ:
表示的是当线程释放锁时,在EntryList中被唤醒的一个线程
*/
// 尝试获得锁
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
DeferredInitialize () ;
// 自旋操作
if (TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
// 这里是将线程封装到node结点中
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
/*
通过CAS操作将node结点插入到cxq链表的头部
如果CAS操作失败,则再尝试获得锁
操作失败的原因是因为其他线程进入到了该方法
*/
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
}
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
// 当没有其他线程竞争时,就将_Responsible指向自己
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
TEVENT (Inflated enter - Contention) ;
int nWakeups = 0 ;
int RecheckInterval = 1 ;
for (;;) {
// 进入循环先尝试获得锁
if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
// 如果竞争锁失败,则进行以下部分
if (_Responsible == Self || (SyncFlags & 1)) {
// 根据传入的时间参数挂起当前线程,一段时间后自动唤醒
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
// 直接挂起当前线程,等待唤醒
TEVENT (Inflated enter - park UNTIMED) ;
Self->_ParkEvent->park() ;
}
if (TryLock(Self) > 0) break ;
TEVENT (Inflated enter - Futile wakeup) ;
if (ObjectMonitor::_sync_FutileWakeups != NULL) {
ObjectMonitor::_sync_FutileWakeups->inc() ;
}
++ nWakeups ;
if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;
if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {
Self->_ParkEvent->reset() ;
OrderAccess::fence() ;
}
// 释放锁的时候将_succ设为EntryList中的一个线程
if (_succ == Self) _succ = NULL ;
OrderAccess::fence() ;
}
// 当执行到这一部分时就说明线程已经获取到锁
// 即可将表示当前线程的结点从EntryList中移除
UnlinkAfterAcquire (Self, &node) ;
if (_succ == Self) _succ = NULL ;
assert (_succ != Self, "invariant") ;
if (_Responsible == Self) {
_Responsible = NULL ;
OrderAccess::fence();
}
if (SyncFlags & 8) {
OrderAccess::fence() ;
}
return ;
}
从上面整个代码来看,可以得到一个重量级锁的获取过程如下:
1.当一个线程尝试获取锁时,如果该锁已经被其他线程占用,就会调用inflate方法进行膨胀操作。在膨胀操作过程中,判断锁的状态。当锁的状态处于重量级锁时则直接结束膨胀。当锁的状态处于膨胀中时则调用ReadStableMark方法进行等待检查。而当锁的状态处于轻量级锁时,就做膨胀操作。
2.在轻量级锁进行膨胀的过程中,通过CAS操作将锁的状态改为INFLATING状态。如果CAS操作成功则设置相应的锁对象信息并且返回该锁对象;如果CAS操作失败则继续循环检查。
3.当成功返回一个ObjectMonitor对象后,调用enter方法,在这个方法中进行对锁的获取(即对锁的竞争)。
4.进入enter方法后,先通过CAS操作将owner设为当前线程。通过CAS操作返回的owner原先的值来判断锁是否竞争成功。当锁竞争不成功时,会先尝试通过自旋操作来获得锁。如果自旋也无法获得锁,就会进入无限循环中反复调用enterI方法。
5.在enterI方法中,将当前线程封装成一个ObjectWaiter。然后通过CAS操作将这个对象插入到cxq链表的头部。如果CAS操作失败,则说明存在其他线程进入到该方法并且将自己的ObjectWaiter插入到cxq链表的头部,则继续向下执行。
6.当第五步的CAS操作失败后,进入一个无限循环块。在最开始先调用TryLock方法尝试获取锁,如果成功则退出循环。如果不成功则判断锁是否是自旋操作。如果是自旋操作则调用带时间参数的挂起方法等待时间结束后自动唤醒。如果不是自旋操作则直接挂起等待唤醒。
7.等待中的线程如果被唤醒则继续调用TryLock方法竞争锁,如果没有成功竞争到锁则继续进行第六步。
接下来我们再来看重量级锁的释放,代码如下所示:
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * Self = THREAD ;
// 当ObjectMonitor中的owner属性不是当前线程
if (THREAD != _owner) {
//判断当前线程是否为原先持有轻量级锁的线程。
if (THREAD->is_lock_owned((address) _owner)) {
// 如果是则更改_owner的值
assert (_recursions == 0, "invariant") ;
_owner = THREAD ;
_recursions = 0 ;
OwnerIsThread = 1 ;
} else {
// 异常:当前线程不是持有锁的线程
TEVENT (Exit - Throw IMSX) ;
assert(false, "Non-balanced monitor enter/exit!");
if (false) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
return;
}
}
// 当重入次数不为0,则让次数-1返回
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
// 必要操作:设置_Responsible为null
if ((SyncFlags & 4) == 0) {
_Responsible = NULL ;
}
for (;;) {
assert (THREAD == _owner, "invariant") ;
if (Knob_ExitPolicy == 0) {
/*
进行锁释放操作(即设置owner为null)
同一时刻如果有其他线程进入同步代码块则获得锁
*/
OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock
OrderAccess::storeload() ; // See if we need to wake a successor
/*
如果没有等待的线程或当前已经有被唤醒的线程
则当前线程不需要唤醒任何线程,直接结束方法
*/
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT (Inflated exit - simple egress) ;
return ;
}
TEVENT (Inflated exit - complex egress) ;
/*
当上述条件不满足时,即存在等待线程时
就通过CAS操作来设置_owner为当前线程
*/
if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
return ;
}
TEVENT (Exit - Reacquired) ;
}
guarantee (_owner == THREAD, "invariant") ;
// 当存在等待线程时,则根据QMode参数来决定唤醒策略
ObjectWaiter * w = NULL ;
int QMode = Knob_QMode ; // 默认为0
if (QMode == 2 && _cxq != NULL) {
// 当QMode为2时且cxq非空,则取出cxq队列的队首ObjectMonitor对象
w = _cxq ;
assert (w != NULL, "invariant") ;
assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
// 唤醒ObjectWaiter对象的线程,直接返回
ExitEpilog (Self, w) ;
return ;
}
if (QMode == 3 && _cxq != NULL) {
/*
当QMode为3时且cxq不为空
则将cxq队列插入到EntryList的尾部
*/
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
assert (w != NULL, "invariant") ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
ObjectWaiter * Tail ;
for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ;
if (Tail == NULL) {
_EntryList = w ;
} else {
Tail->_next = w ;
w->_prev = Tail ;
}
}
if (QMode == 4 && _cxq != NULL) {
/*
当QMode为3时且cxq非空
则将cxq队列插入到EntryList的头部
*/
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
assert (w != NULL, "invariant") ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
if (_EntryList != NULL) {
q->_next = _EntryList ;
_EntryList->_prev = q ;
}
_EntryList = w ;
}
w = _EntryList ;
if (w != NULL) {
// 唤醒EntryList的队首元素ObjectWaiter对象的线程
assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}
// 如果EntryList为空的话则处理cxq的元素
w = _cxq ;
if (w == NULL) continue ;
// 当cxq不为空则将cxq设为null
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
TEVENT (Inflated exit - drain cxq into EntryList) ;
assert (w != NULL, "invariant") ;
assert (_EntryList == NULL, "invariant") ;
if (QMode == 1) {
/*
当QMode为1时,将cxq中的元素以逆序的形式移至EntryList中
*/
ObjectWaiter * s = NULL ;
ObjectWaiter * t = w ;
ObjectWaiter * u = NULL ;
while (t != NULL) {
guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ;
t->TState = ObjectWaiter::TS_ENTER ;
u = t->_next ;
t->_prev = u ;
t->_next = s ;
s = t;
t = u ;
}
_EntryList = s ;
assert (s != NULL, "invariant") ;
} else {
/*
当QMode为0或者2时,将cxq的元素以正序的形式移至EntryList中
其实也就是说,当EntryList为空时,后到的线程会先获取到锁
*/
_EntryList = w ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
}
/*
在做完上面的所有操作后,还需要判断_succ是否不为空
如果succ不为空则说明有线程已经被唤醒了,不需要再让当前线程去唤醒
*/
if (_succ != NULL) continue;
w = _EntryList ;
// 获取EntryList中的首元素,然后唤醒该元素,即唤醒线程
if (w != NULL) {
guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}
}
}
到此,整个重量级锁的释放就结束了。
从上面的分析大致地可以了解到synchronized的底层实现到底是怎么的一个实现顺序,在分析的过程中其实也涉及到了锁的升级和降级的内容。
最后这里总结一下:synchronized代码块是由一对monitorenter/monitorexit指令实现。在JDK6以后,JVM提供了三种不同的Monitor实现:偏向锁、轻量级锁以及重量级锁。当JVM检测到不同的竞争状况时,就会自行切换到适合的锁实现。
ThreadLocal机制
ThreadLocal是Java提供的一种保存线程私有信息的机制,其在整个线程生命周期内有效,因此可以方便地在一个线程中不同业务层次之间传递信息。在讲ThreadLocal的原理之前,先来学习一下它的用法。
ThreadLocal初接触
从API文档可以看到,ThreadLocal类有以下方法:
// 返回此线程局部变量的当前线程副本中的值
T get()
// 返回此线程局部变量的当前线程的“初始值”
protected T initialValue()
// 移除此线程局部变量当前线程的值
void remove()
// 将此线程局部变量的当前线程副本中的值设置为指定值
void set(T value)
// JDK8 创建一个线程局部变量
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier);
在通过API文档了解到ThreadLocal的相关方法后,我们就可以试着去编写代码来简单使用一下ThreadLocal,代码如下所示:
public class MyThread04 implements Runnable {
ThreadLocal<Integer> tl = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 2;
}
};
@Override
public void run() {
while (tl.get() > 0) {
System.out.println(Thread.currentThread().getName() + "保存的值:"
+ tl.get());
tl.set(tl.get() - 1);
}
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
MyThread04 t = new MyThread04();
new Thread(t, "线程1").start();
new Thread(t, "线程2").start();
}
}
结果如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/784b2c7d1c8f489181bb508c25ec43c0.png)
从结果上可以看到,线程2和线程1保存的值互不干扰,而这也就体现了ThreadLocal的特点。
ThreadLocal适合需要资源共享但不需要维护状态的情况,换句话而言就是一个线程对共享资源的修改,不影响另一个线程的运行任务。
ThreadLocal底层实现
在学习完其使用和了解其特点后,就产生了一个问题:为什么ThreadLocal能够具备这样的能力?
接下来我们就找到其源码,这里以JDK8的源码为例。从源码大致来看可以发现,其内部有一个ThreadLocalMap内部类,代码如下所示:
public class ThreadLocal<T> {
static class ThreadLocalMap {
/**
* 在ThreadLocalMap中又有一个内部类Entry来充当存储结点
* 而这个内部类Entry继承了WeakReference类
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
// value为线程往ThreadLocal里存储的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量,容量值必须为2的幂
private static final int INITIAL_CAPACITY = 16;
// Entry表,大小必须为2的幂
private Entry[] table;
// 表中Entry的个数
private int size = 0;
// 重分配表大小的阈值,默认为0
private int threshold;
// ThreadLocalMap的构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table数组
table = new Entry[INITIAL_CAPACITY];
// 通过哈希运算确定位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 初始化结点
table[i] = new Entry(firstKey, firstValue);
// 设置大小为1以及扩容阈值
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
从源码中我们又发现了在ThreadLocalMap中又定义了一个Entry内部类,那么可以得到以下信息:Entry通过继承WeakReference类以及自身属性value来实现Key-Value键值对。很容易的就知道value是存放存入到ThreadLocal的值,而key的存放则是通过WeakReference来完成。而ThreadLocal则是通过ThreadLocalMap维护Entry数组来完成,从源码上来看底层数据结构相当于一个环形数组,如图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/a5b4c3ea6a342060b18dc58dd43a35d4.jpeg)
所谓的WeakReference(弱引用)就是不能确保其引用的对象不会被垃圾回收器回收的引用。因此这为ThreadLocal的垃圾清理提供了一定的便利性。
这里以一个UML类图来解释一下这几个类与Thread类之间的关系,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/fc6f7306e94fc7ef6afcf032d67e43ee.jpeg)
接下来再来看ThreadLocal的get方法,如下所示:
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 通过getMap方法获得一个ThreadLocalMap集合
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取Key-value键值对
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
/**
* 当map为空时则调用setInitialValue方法
* 实际上就是返回初始值
*/
return setInitialValue();
}
这里需要关注的几个方法:getMap、getEntry以及setInitialValue方法。首先先来看getMap方法,如下所示:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从源码上可以得知,在Thread类中维护了一个成员属性threadlocals,该属性是用来保存线程本地变量。而这样的设计,就让每个线程都有自己的数据,也就成功地实现了不同线程间数据隔离。
接下来我们再来看getEntry方法,如下所示:
/**
* 根据键来获取table数组中存储的相应的结点
* 当找不到相应的结点时则进行getEntryAfterMiss方法
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
在getEntry方法中看到了一个熟悉的公式,即hashCode&(length - 1),在先前HashMap的源码分析时已经讲解过,这里不再概述,只需要知道用其确定位置即可。当找不到相应结点则进行getEntryAfterMiss方法,如下所示:
/**
* 当找不到相应的结点时就会执行这个方法
* 通过循环遍历表中数据比对寻找结点
* 实际上就是基于线性探测法不断寻找
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
/**
* 当目标为空时,就说明相对应的ThreadLocal已经回收
* 则调用expungeStaleEntry来清理无效的结点
* 当目标不为空且不等于键时则进行向下遍历
*/
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
那么接下来我们再来看一下expungeStaleEntry方法,源码如下所示:
// 清理无效结点的方法
private int expungeStaleEntry(int staleSlot) {
// staleSlot表示的是无效结点的位置
Entry[] tab = table;
int len = tab.length;
/**
* 通过将value值设为null以及设置无效entry为空来清除当前无效结点
*/
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 从脏entry的位置开始遍历所有结点
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
/**
* 如果遍历到结点相应的ThreadLocal已经被回收
* 就会对其进行相应的清除操作
*/
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
/**
* 如果遍历到结点相应的ThreadLocal并没有被回收
* 就判断当前ThreadLocal的索引位置是否不为i
* 如果不为i则从h向后寻找空的entry,将当前的entry移动到该空entry的位置
* 实际上就是一个rehash操作
*/
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // 不在同一位置
tab[i] = null; //将旧位置清理
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 返回无效结点后的第一个空的位置索引
return i;
}
最后再来看setInitialValue方法,如下所示:
/**
* 返回当前线程的本地线程变量的初始值
* 如果没有调用set()方法,这个方法会在初次调用get()方法访问变量时被调用
* 如果想要设置初始值则需要重写该方法
*/
protected T initialValue() {
return null;
}
/**
* 设置本地线程变量的初始化值
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
/**
* 当map不为空时则调用map自带的set方法
* 需要注意的是这里的第一个参数是this并非当前线程
* 当map为空时则调用createMap方法创建Map集合
*/
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
setInitialValue方法中的代码实际上和ThreadLocal的set方法中的代码并没有过多差别,只是无参和有参的区别。
那么这里总结一下get方法的一个过程:
1.先从当前线程中获取ThreadLocalMap,查看当前ThreadLocal对应的Entry是否存在。
2.如果存在则返回其值value,如果不存在则说明未初始化,进行初始化操作并且返回初始化的值。
在前面的时候已经说过,ThreadLocal的set方法和setInitialValue方法没有多大差异,因此直接找到ThreadLocalMap的set方法,如下所示:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 通过线性探测法去寻找该位置上是否存在Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果entry中的key和参数key相等,就覆盖value值
if (k == key) {
e.value = value;
return;
}
/**
* 如果entry中的key为空,则替换现有位置的Entry中的存储数据
* 并且还会清理无效的Entry
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
/**
* 当ThreadLocal存储的位置中并没有其他ThreadLocal占用
* 即tab[i]==null时,就新建一个Entry存储到该位置
*/
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots方法用于清除脏entry(即key为null的entry)
* 如果没有清除无效entry,并且当大小超过阈值时,则进行rehash操作
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从set源码就可以看到相应的处理逻辑,通过线性探测法去查找相对应的Entry是否存在,如果存在则再判断Entry的key值是否和当前线程的ThreadLocal相同。如果相同的话则直接替换原值,如果不相同并且当前位置的Entry的key值为空,则进行替换操作并且清理数组中无效Entry;如果不存在,则直接在当前位置创建一个新的Entry。插入完成后尝试清理无效Entry,如果没有清理并且大小超过阈值,那么则进行rehash操作。
从源码以及逻辑来看,我们还需要关注replaceStaleEntry、cleanSomeSlots以及rehash方法。接下来一一展开其源码,如下所示:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge表示清理无效Entry的起点
int slotToExpunge = staleSlot;
/**
* 根据staleSlot向前遍历,直到找到无效Entry
* 也就是说,查找最前的一个无效Entry
*/
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 根据staleSlot向后遍历
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
/**
* 如果在遍历过程中找到了key,则将其与table[staleSlot]互换
*/
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
/**
* 如果在向前遍历时没有找到无效Entry
* 则将slotToExpunge的值设为当前i
* 反之,则slotToExpunge的值为当初向前遍历找到的无效Entry的位置索引
*/
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 以slotToExpunge为起点,做两次清理操作
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
/**
* 如果在向后遍历发现了无效Entry并且向前扫描并没有找到无效Entry
* 则将slotToExpunge的值设为当前位置i
*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
/**
* 如果在上面的遍历中都没有找到相关的key
* 则在当前位置直接新建一个Entry即可
*/
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
/**
* 当slotToExpunge和staleSlot不相等时
* 则说明存在无效的entry需要清理
*/
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
接下来再来查看cleanSomeSlots方法,如下所示:
/**
* 根据传入参数来清理无效entry
* i表示清理无效entry的起点
* n表示遍历次数
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
/**
* n >>>= 1 无符号右移
* 将遍历次数控制在log2(n)
*/
do {
i = nextIndex(i, len);
Entry e = tab[i];
/**
* 当向后遍历存在无效entry时
* 将n设为len,进行一片连续段的清理
*/
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
对于clearSomeSlots方法,其根据传入的参数来决定清理次数。这个方法在set方法以及replaceStaleEntry方法中都有涉及,而传入的n参数代表的意义不同。set方法中传入的是表的元素个数,而replaceStaleEntry方法传入的是表的大小。
最后再来看rehash方法,如下所示:
private void rehash() {
// 进行一次完全清理
expungeStaleEntries();
/**
* 进行完成清理后,size会变小
* threshold - threshold / 4相当于len/2
* 当满足这个条件的时候就进行扩容
*/
if (size >= threshold - threshold / 4)
resize();
}
在rehash方法中可以看到,仍然会进行一次完全清理。清理的时候会让size变小,同时阈值判断也要做相应的减小处理,以便扩容处理。接下来就分别来看这两个方法,如下所示:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 扩大为原长的两倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
// 记录元素个数
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 在扩容的时候如果碰到存在有无效entry的时候就会进行小清理
if (k == null) {
e.value = null; // Help the GC
} else {
// 通过线性探测法来将原有的有效Entry存入新表中
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
// 完全清理所有无效的Entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
最后来总结一下set方法的大致过程:
1.通过线性探测法先遍历table。遍历过程中如果发现有效且是当前ThreadLocal对应的Entry(即key相同)则直接替换其value值。在遍历过程中如果发现无效的Entry,则进行下一步,否则进行第三步。
2.遍历过程中发现存在无效的Entry,那么就进行replaceStaleEntry。在进行replaceStaleEntry的过程中,如果找到了当前ThreadLocal对应的Entry(找到key)则将其与该无效的Entry进行交换,并且将value设为新值。如果没有找到对应的Entry则直接在无效的Entry的位置上新建一个新的Entry。
3.遍历过程中如果发现并没有存在无效的Entry和当前ThreadLocal对应的Entry,那么会直接在遍历完后的第一个空位置放入新的Entry。
最后我们再来看一下remove方法,代码如下所示:
/**
* ThreadLocal的remove方法实际上
* 是调用ThreadLocalMap的remove方法
* 尽量在不再使用的情况下显式调用remove方法
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
/**
* 通过线性探测法查找对应的Entry
* 调用WeakReference的clear方法清除
* 顺便再进行一次连续片段清理
*/
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
/**
* 实际上是父类Reference的clear方法
* 其实就是显式断开弱引用
*/
public void clear() {
// 这样GC就可以将其进行垃圾回收
this.referent = null;
}
从上面的代码演示,其实已经很好地解释了ThreadLocal到底是如何运作的。但是其中的细节却耐人寻味,比如说为什么结点中ThreadLocal是弱引用,为什么会有多次清理的操作以及为什么在replaceStaleEntry方法中会出现交换操作。
比如说ThreadLocalMap中持有的ThreadLocal是弱引用,而value却是一个强引用。这样对于GC来说,其发现value是具有可达性的,那么就会导致垃圾回收时value不会被回收,但value却不能被访问到。因此这样就产生了内存泄漏问题。那么为什么不用强引用呢?如果使用强引用,在gc进行可达性分析的时候,会认为threadLocal依然可达,那么就不会对其进行垃圾回收,就会出现逻辑错误。
但是使用弱引用就会产生内存泄漏问题,实际上代码的精髓就在ThreadLocal的生命周期方法中get、set等调用了cleanSomeSlots、replaceStaleEntry以及expungeStaleEntry这三个方法中清理掉key为null的无效entry,这样就避免了内存泄漏问题。
需要注意的是,在实际开发中,通常是用线程池维护线程,就会导致value具备可达性。因此每次使用完ThreadLocal最好调用其remove方法。
对于上面提及的相关GC内容在这里不予以展开,以后另开一篇GC的有关内容。ThreadLocal中其中还可以了解的地方是其源码中出现的一个魔数,以后找机会补充一下文章。
后话
'''
文章篇幅其实已经有点长了,对于ReentrantLock机制并没有展开
如果以后有时间的话再重开一章或者就在这章展开吧
多线程是重中之重的知识点,理解其本质以及理解其源码对学习框架很有帮助
'''