目录
[64位JVM对象头中Mark Word结构](#64位JVM对象头中Mark Word结构)
线程安全问题
多个线程同时操作共享变量,如有写操作可能会出现线程安全问题,这里通过一个出票的场景来演示线程安全问题
需求
一共有200张票,现通过3个出票机同时出票,出完为止
具体实现
java
package concurrency;
public class TicketMachineTest {
public static void main(String[] args) {
// 出票任务
TicketTask task = new TicketTask();
// 3个出票机
new Thread(task, "一号出票机").start();
new Thread(task, "二号出票机").start();
new Thread(task, "三号出票机").start();
}
}
// 出票任务
class TicketTask implements Runnable {
// 当前票号(从1开始)
private int currentTicketNo = 1;
// 一共200张票
private static final int TOTAL_TICKET_COUNT = 200;
@Override
public void run() {
while (true) {
// 票已经全部出完
if (currentTicketNo > TOTAL_TICKET_COUNT) {
break;
}
// 还有剩余的票
// 假设出票耗时5ms
issueTicketTime(5);
// 出票结果
String result = String.format("%s-出了[%s]号票", getMachineName(), currentTicketNo++);
System.out.println(result);
}
}
/**
* 获取出票机名字
*/
public String getMachineName() {
return Thread.currentThread().getName();
}
/**
* 出票耗时
*/
public void issueTicketTime(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
测试结果
通过测试,发现存在线程安全问题,具体体现在如下两个方面
- 多个出票机出了同一张票

- 实际出票数量超过了规定的总票数

解决线程安全问题
解决方式
单个JVM情况下,可以通过多种方式来解决如上面出现的线程安全问题
- 使用synchronized
- 使用ReentrantLock
- 使用原子类
使用synchronized解决线程安全问题
这里主要介绍通过synchronized来解决线程安全问题
java
// 出票任务
class TicketTask implements Runnable {
// 当前票号(从1开始)
private int currentTicketNo = 1;
// 一共200张票
private static final int TOTAL_TICKET_COUNT = 200;
@Override
public void run() {
// 票已经全部出完
if (currentTicketNo > TOTAL_TICKET_COUNT) {
return;
}
while (true) {
// currentTicketNo作为共享变量
// 多个线程同时对currentTicketNo有写的操作
// 这里使用synchronized来解决数据一致性问题
synchronized (TicketTask.class) {
// DCL
if (currentTicketNo > TOTAL_TICKET_COUNT) {
break;
}
// 还有剩余的票
// 假设出票耗时5ms
issueTicketTime(5);
// 出票结果
String result = String.format("%s-出了[%s]号票", getMachineName(), currentTicketNo++);
System.out.println(result);
}
}
}
/**
* 获取出票机名字
*/
public String getMachineName() {
return Thread.currentThread().getName();
}
/**
* 出票耗时
*/
public void issueTicketTime(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
synchronized几种常见使用方式
代码块
java
package concurrency;
public class SynchronizedTest {
private static final Object MONITOR = new Object();
public static void main(String[] args) {
// MONITOR对象锁
synchronized (MONITOR) {
}
}
}
java
package concurrency;
public class SynchronizedTest {
public static void main(String[] args) {
// SynchronizedTest类的字节码对象锁
synchronized (SynchronizedTest.class) {
}
}
}
实例方法
java
package concurrency;
public class SynchronizedTest {
// this锁
public synchronized void instanceMethod() {
}
}
静态方法
java
package concurrency;
public class SynchronizedTest {
// 当前类的Class对象锁
public static synchronized void staticMethod() {
}
}
synchronized底层实现原理
概述
在JVM中,每个对象都关联一个监视器(Monitor),synchronized就是通过Monitor来实现线程同步的,对象头中的MarkWord会指向Monitor,当线程进入同步块时,会尝试获取Monitor,获取成功继续执行,否则阻塞等待
Monitor监视器
每一个对象都和一个监视器Monitor关联,Monitor包含如下几个部分
- Owner:持有锁的线程
- EntryList:等待锁的线程队列
- WaitSet:调用wait方法的线程队列
查看synchronized同步块的汇编码
- 使用【javap -c】命令查看汇编码

- 通过下图可以看到,synchronized同步块是monitorenter和monitorexit的过程

使用jconsole命令查看synchronized程序
- 使用了synchronized的程序如下
java
package concurrency;
public class SynchronizedTest {
private static final Object MONITOR = new Object();
public static void main(String[] args) {
// t1线程
new Thread(() -> {
synchronized (MONITOR) {
try {
Thread.sleep(100_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t1").start();
// t2线程
new Thread(() -> {
synchronized (MONITOR) {
}
}, "t2").start();
// t3线程
new Thread(() -> {
synchronized (MONITOR) {
}
}, "t3").start();
}
}
- 使用jconsole命令查看t1、t2、t3线程的执行情况,可以看到t1线程获取到对象锁,t2和t3线程在等待锁而进入BLOCKED状态




锁升级的过程
为减少获取锁和释放锁带来的性能损耗,JDK6之后引入了偏向锁、轻量级锁、重量级锁的概率,并允许锁升级
64位JVM对象头中Mark Word结构
|------------------------------------------------------------------------------------|
| 锁状态 | 25bit | 31bit | 1bit | 4bit |
|--------------|------------------------|-----------------------|------|--------------|
| 无锁 | unused | hashCode | 0 | 01 |
| 偏向锁 | threadID+epoch | age | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录 | (00) | | |
| 重量级锁 | 指向Monitor | (10) | | |
| GC标记 | | (11) | | |
|------------------------------------------------------------------------------------|
无锁状态
初始状态
偏向锁
在无竞争情况下,当一个线程访问同步块时,会在对象头和栈帧中的锁记录里存储偏向线程的ID,之后该线程进入和退出同步块时不需要CAS操作来加锁和释放锁
轻量级锁
当有另一个线程竞争锁时,偏向锁会升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,然后将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程会尝试使用自旋来获取锁
重量级锁
当轻量级锁竞争失败,并且自旋超过一定次数(或者等待线程超过一定数量)时,轻量级锁会升级为重量级锁。重量级锁会使得竞争失败的线程阻塞,直到持有锁的线程释放锁,然后唤醒阻塞的线程