线程安全
Java 内存模型
多线程风险
在 Java 程序中,存储数据的内存空间分为共享内存和本地内存。线程在读写主存的共享变量时,会先将该变量拷贝一份副本到自己的本地内存,然后在自己的本地内存中对该变量进行操作,完成操作之后再将结果同步至主内存。
| 类型 | 存储介质 | 数据 | 特征 |
|---|---|---|---|
| 共享内存 | 主内存 | 存放变量 | 多线程共享 |
| 本地内存 | CPU 高速缓存、缓冲区、寄存器以及其它硬件优化 | 临时存放线程使用的变量副本 | 使用期间其它线程无法访问 |
- 优势:由于 CPU 执行速度明先快于内存读写速度,将运算需要的数据拷贝到 CPU 高速缓存中运算,可以大大加快程序运行速度。
- 劣势:主内存数据和本地内存的不同步,导致多个线程同时操作主内存里的同一个变量时,变量数据可能会遭到破坏。
public class ThreadDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
class MyThread implements Runnable {
private int x = 0; // 对象中的数据由线程共享
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
x++;
}
System.out.println("final x: " + x); // 最后输出的数据不一定为 20000
}
}
行为规范
JMM 定义了共享内存系统中多线程程序读写操作行为的规范,用来保证共享内存的原子性、可见性、有序性。
原子性
原子性是指一个操作,要么全部执行并且执行过程不会被打断,要么就都不执行。
- Java 语言本身只保证了基本类型变量的读取和赋值是原子性操作。
- 简单操作的原子性可以通过 Atomic 原子类实现。
- 通过 synchronized 和 ReenTrantLock 等锁结构可以保证更大范围的原子性。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
-
Java 语言会尽可能保证主内存数据和本地内存同步,但仍可能出现不可见问题。

-
通常用 volatile 关键字来保证可见性。
-
通过 synchronized 和 ReenTrantLock 等锁结构在释放锁之前会将对变量的修改刷新到主存当中,也能够保证可见性。
有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。
- Java 内存模型具备先天的有序性。但 Java 允许编译器和处理器对指令进行重排序,可能影响多线程并发执行时的有序性。
- 通过 synchronized 和 ReenTrantLock 等锁结构可以保证有序性。
- volatile 关键字可以禁止 JVM 的指令重排,也可以保证有序性。
底层实现
synchronized 关键字
修饰方法或代码块。被线程访问时由线程抢占锁,直到执行完毕后自动释放锁。其他线程没有获得锁将无法访问上锁内容。保证了指定内容在同一时刻只有一个线程能访问。
- 修饰 static 方法实质是给当前类上锁:这个类的所有 synchronized static 方法共享一个锁。
- 修饰实例方法实质是给对象上锁:这个对象内所有的 synchronized 实例方法共享一个锁。
每一个对象都有且仅有一个与之对应的 monitor 对象。synchronized 关键字修饰方法时会对方法添加标志位,当线程执行到某个方法时,JVM会去检查该方法的访问标志是否被设置,如果设置了线程会先获取这个对象所对应的 monitor 对象,再执行方法体,方法执行完后释放 monitor 。
同步代码块则是在同步代码块前插入 monitorenter ,在同步代码块结束后插入 monitorexit 。
public class ThreadDemo {
public static void main(String[] args) {
ThreadDemo test = new ThreadDemo();
new Thread(test::m1).start();
new Thread(test::m2).start();
}
public synchronized void m1() {
System.out.println("1");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
System.out.println("2");
}
public synchronized void m2() {
System.out.println("3");
try {
Thread.sleep(500);
} catch(InterruptedException e) {}
System.out.println("4");
}
}
同步对象
创建两个不同的对象就拥有两把不同的锁,不同对象的 synchronized 实例方法互不影响。
public class ThreadDemo {
public static void main(String[] args) {
ThreadDemo test1 = new ThreadDemo();
ThreadDemo test2 = new ThreadDemo();
new Thread(test1::m1).start();
new Thread(test2::m2).start();
}
public synchronized void m1() {
System.out.println("1");
try { Thread.sleep(1500); } catch(InterruptedException e) {}
System.out.println("4");
}
public synchronized void m2() {
try { Thread.sleep(500); } catch(InterruptedException e) {}
System.out.println("2");
try { Thread.sleep(500); } catch(InterruptedException e) {}
System.out.println("3");
}
}
同步方法
其他线程无法获取该对象锁,就不能访问该对象的所有 synchronized 实例方法,但仍可以访问其他方法。 synchronized 实例方法中调取的数据仍可能被其他方法修改。
在实际开发过程中,我们常常对写操作加锁,但对读操作不加锁,提升系统的并发性能。但可能会导致脏读问题。
public class ThreadDemo {
public static void main(String[] args) {
ThreadDemo test = new ThreadDemo();
new Thread(test::m1).start();
new Thread(test::m2).start();
}
boolean data = false;
public synchronized void m1() {
System.out.println(data); // false
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
System.out.println(data); // true
}
public void m2() throws {
try {
Thread.sleep(500);
} catch(InterruptedException e) {}
this.data = true;
}
}
同步代码块
如果我们需要同步的代码只有一小部分,就没有必要对整个方法进行同步操作,我们只需要同步的代码块进行包裹。
修饰代码块,需要指定被上锁的对象或者类。每次线程进入 synchronized 代码块时就会要求当前线程持有该对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行操作。我们通常使用 this 对象或者当前类的 class 对象作为锁。
不要以字符串对象作为锁的对象。字符串常量在常量池里被锁定,可能会导致意想不到的阻塞。
public class ThreadDemo {
public static void main(String[] args) {
ThreadDemo test = new ThreadDemo();
new Thread(test::m1).start();
new Thread(test::m2).start();
}
public void m1() {
synchronized(this) {
System.out.println("1");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
System.out.println("2");
}
}
public synchronized void m2() {
synchronized(this) {
System.out.println("3");
try {
Thread.sleep(500);
} catch(InterruptedException e) {}
System.out.println("4");
}
}
}
线程执行代码出现异常时也会自动释放所有锁,因此在 synchronized 内部处理异常一定要非常小心。如果不想释放锁,使用 try-catch 语句捕获异常。
ReentrantLock 锁
实现 Lock 接口,使用时需导入 import java.util.concurrent.locks.*;。
实现功能和 synchronized 关键字类似。但 synchronized 关键字是在 JVM 层面实现的,而 ReenTrantLock 是在 JDK 层面实现的。需要手动调用 lock 和 unlock 方法配合 try/finally 语句块来完成。
public class ReentrantLockTest {
// 创建锁对象, final修饰后:锁对象是唯一和不可替换的,非常专业
private final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 5; i++){
new Thread(new MyThread()).start();
}
}
static class MyThread implements Runnable {
@Override
public void run() {
try {
// 加锁,通常在 try 语句里完成
lock.lock();
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "excute");
} catch (InterruptedException e) {}
finally{
// 解锁,必须在 finally 语句里完成
lock.unlock();
}
}
}
}
}
ReenTrantLock 比 synchronized 增加了一些高级功能,主要有以下三点:
实现等待中断
调用 lockInterruptibly 方法上锁,线程中断标志置为 true 时会抛出 InterruptedException 异常并释放锁。防止线程因为无法获得锁而一直等待,常用来从外部破坏线程死锁。
public class ThreadDemo {
// 创建锁对象
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread(),"thread1");
Thread t2 = new Thread(new MyThread(),"thread2");
t1.start();
t2.start();
Thread.sleep(500);
// 提前中断线程
t2.interrupt();
}
static class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "begin");
try {
// 加可中断锁
lock.lockInterruptibly();
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "out");
} finally{
try{
lock.unlock();
} catch(IllegalMonitorStateException e) {}
System.out.println(Thread.currentThread().getName() + "end");
}
}
}
}
调用 tryLock 方法上锁,可以从线程内部破坏死锁,可以更好地解决死锁问题。
- 传入时间参数设定等待锁的时间,超时没有获得锁则中止。
- 无参则返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。
public class ThreadDemo {
// 创建锁对象
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread(),"thread1");
Thread t2 = new Thread(new MyThread(),"thread2");
t1.start();
t2.start();
}
static class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "begin");
try {
// 加锁失败直接退出
if(!lock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "out");
return;
}
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
try{
lock.unlock();
} catch(IllegalMonitorStateException e) {}
System.out.println(Thread.currentThread().getName() + "end");
}
}
}
}
实现公平锁
允许先等待的线程先获取锁,防止线程因无法获得锁而一直等待。但由于性能优势,默认情况下仍使用非公平锁。在构造锁对象时添加参数 true 即可实现。
import java.util.concurrent.locks.*;
public class ReentrantLockTest {
// 创建锁对象,且声明为公平锁
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 5; i++){
new Thread(new MyThread()).start();
}
}
static class MyThread implements Runnable {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "excute");
} catch (InterruptedException e) {}
lock.unlock();
}
}
}
}
选择性通知
ReentrantLock 对象可以创建一个或多个 Condition 对象,实现线程间的等待通知机制。比 synchronized 关键字 使用 wait/notify 方法更为简便和易用。
线程获得 Lock 锁之后便可调用 Condition 接口的 await 方法释放锁并等待,直到有其他线程调用 Condition 的 signal 方法唤醒线程。通过设置多个 condition 对象,多个线程等待不同的 condition 对象,可以实现选择性地叫醒线程。
public class ThreadDemo {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
lock.lock();
new Thread(new MyThread()).start();
System.out.println("主线程等待通知");
try {
condition.await();
} finally {
lock.unlock();
}
System.out.println("主线程恢复运行");
}
static class MyThread implements Runnable {
@Override
public void run() {
lock.lock();
try {
condition.signal();
System.out.println("子线程通知");
} finally {
lock.unlock();
}
}
}
}

浙公网安备 33010602011771号