目录
一、Java内存模型
JMM的核心概念
二、什么是线程安全?
1、原子性
2、有序性
3、可见性
三、如何确保线程安全?
1、sychronized关键字
2、Lock接口和其实现
3、volatile关键字
4、Atomic原子类
5、ThreadLocal
6、不可变对象
7、并发集合类
8、并发工具类
9、Future和Callable
一、Java内存模型
在谈及线程安全前,需要了解内存模型一下
Java内存模型
(Java Memory Model,JMM)是一种抽象的概念,JMM并不真实存在,它是一种规范
,规定了
在Java并发编程中如何处理多线程之间的内存交互,程序中变量在内存中的访问方式
JMM的核心概念
1、主内存与工作内存:
主内存:
是所有线程共享的内存区域,
所有变量都存储在主内存中,主内存是共享的
工作内存:
每个线程都有自己的工作内存(局部变量存储区),线程对共享变量的所有操作都发生在工作内存中,然后同步回主内存
这种模型允许线程在本地缓存共享变量的副本,提高性能,但也带来了同步的复杂性
2、内存屏障
内存屏障(Memory Barrier)是JMM中用于控制内存访问顺序的指令。它确保指令序列中的内存读写操作按照特定的顺序执行,从而保证线程间的内存可见性和有序性
3、Happens-Before规则
Happens-Before是JMM中最核心的概念之一,它定义了一组偏序关系,用于判断两个操作之间的内存可见性和有序性,用于描述多线程程序中操作的执行顺序,确保了线程之间的正确通信和数据一致性
简而言之:如果在同一个线程中,操作A在操作B之前执行,那么我们说一个操作A happens-before 另一个操作B,那么A的执行结果对B是可见的,且A的执行顺序排在B之前,也就是先执行的操作的结果必须对后执行的操作可见
用一个例子来深入了解内存模型
🌰:线程A和线程B从主内存读取和修改x=1的过程
线程A和线程B要进行通信的话,必须要进行以下2个步骤:
-
初始化:x = 1,存储在主内存。
-
线程A读取:A从主内存读取x,复制值1到A的工作内存。
-
线程B读取:B从主内存读取x,复制值1到B的工作内存。
-
线程A修改:A在工作内存中修改x至2
-
线程A写回:A将工作内存中的x值2写回主内存。
-
线程B重新读取:B从主内存读取最新的x值2,保证了数据的可见性。
这个过程展示了JMM如何确保多线程环境下的数据一致性
二、什么是线程安全?
线程安全:在多线程环境下,多个线程可以安全地访问和操作共享数据,而不会引发数据不一致或程序错误的问题
JMM 解决并发程序中最关键的两个问题:线程间的可见性和指令重排序。
- 线程间的可见性:确保当一个线程修改了共享变量的值时,其他线程可以立即看到这一改变。没有良好的可见性保证,一个线程对共享变量的修改可能对其他线程不可见,导致数据不一致。
- 指令重排序:为了提高性能,编译器和处理器常常会改变指令的执行顺序(只要这种改变不影响单线程内的程序逻辑)。然而,在多线程环境下,这种重排序可能导致严重问题
实际上,线程安全问题的具体表现体现在三个方面,原子性、有序性、可见性,JMM 通过以下几种方式来控制和协调上述现象:
1、原子性
原子性:一个操作要么全部完成,要么全部不完成,不会被中断
JMM保证了基本类型的读取和写入操作是原子的,即不可分割的。但是,复合操作(如i++)需要额外的同步机制(Automic类)才能保证原子性
例如:两个线程同时尝试修改同一个变量,原子性可以确保这个修改操作作为一个整体被执行,而不是被另一个线程业务
场景:银行取钱,你想要从账户中取出100元。在理想情况下,这个操作应该是“原子”的,也就是说,要么整个取款操作成功完成(你拿到了100元,账户余额减少了100元),要么操作完全不发生(你没拿到钱,账户余额不变)。中间不能出现任何状态,比如你只拿到了50元而账户扣除了100元,这会导致数据不一致
2、有序性
有序性:保证了指令的执行顺序不会导致数据依赖关系的破坏
JMM限制了编译器和处理器对指令的重排序,以避免破坏程序的语义。volatile、synchronized和final等关键字提供了内存屏障,强制执行特定的内存操作顺序,从而保证了线程之间的正确交互
例如:如果一个线程先读取变量A,然后读取变量B,有序性确保在另一个线程中,如果先修改了B,然后再修改A,那么第一个线程读取B时,不会看到A的更新值,因为它应该先读取A的旧
场景:排队买票,如果你前面的人买完票后,后面的人突然插队到你前面买了票,这就打破了正常的顺序
3、可见性
可见性:当一个线程修改了一个共享变量后,其他线程是否能够看到这个修改
JMM规定,当一个线程修改了共享变量的值,新值对其他线程来说是可见的。通常通过volatile关键字、synchronized代码块或方法、以及final字段来实现
场景:五子棋游戏,你在游戏中放置了黑棋,然后告诉朋友“我已经在某个位置下了黑棋”。为了游戏能够正常进行,你的朋友必须能看到你刚下的黑棋的位置,才能做出相应的策略
这三个概念对于多线程编程非常关键,它们确保了程序在并发执行时的正确性和一致性。通过使用锁、volatile关键字、原子变量等机制,程序员可以控制和管理多线程环境下的原子性、可见性和有序性,从而避免数据竞争、死锁等问题,使程序能够正确地运行
三、如何确保线程安全?
这里大概叙述一下,后续文章进行详细补充,具体可以采取以下几种策略:
1、sychronized
关键字
确保同一时间只有一个线程可以访问资源。
public synchronized void method() {
// 访问和修改共享资源
}
2、
L
ock接口和其实现
使用java.util.concurrent.locks
包中的ReentrantLock
等类允许更细粒度的锁控制,如公平锁、非公平锁、读写锁等
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 访问和修改共享资源
} finally {
lock.unlock();
}
}
3、volatile关键字
用于变量,确保变量的读写操作具有可见性,即一个线程对变量的修改对其他线程立即可见。volatile不保证原子性,但对于简单的读写操作(如对基本类型的读写)
4、Atomic
原子类
使用java.util.concurrent.atomic
包中的原子变量类,如AtomicInteger
、AtomicReference
等,这些类通过底层的硬件支持实现高效的原子操作。
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
5、ThreadLocal
ThreadLocal变量为每个线程提供独立的副本,避免了线程间的共享和同步问题
6、不可变对象
设计不可变对象(immutable objects),即对象在创建后其状态不可更改,这样可以避免多线程访问时的同步问题。例如:String、Integer等包装类都是不可变的
public final class ImmutableClass {
private final int value;
public ImmutableClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
7、并发集合类
使用java.util.concurrent
包中的线程安全集合类,如ConcurrentHashMap
、CopyOnWriteArrayList
等,这些类已经内建了线程安全机制。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
8、并发工具类
如CountDownLatch、CyclicBarrier、Semaphore等,提供了更复杂的线程同步手段
9、Future和Callable
可以用于异步执行任务,内部使用线程池和同步机制来保证线程安全
通过这些机制,Java可以在多线程环境中有效地管理资源访问,保持数据的一致性和正确性
其中
- Volatile 实现可见性、有序性
当一个线程修改了一个volatile变量的值,这个修改会立即被其他线程可见,无需等待主内存和工作内存之间的数据同步
- CAS 实现原子性
Java中的CAS(Compare and Swap,比较并交换)是一种无锁技术,当多个线程同时尝试对同一个共享变量执行CAS操作时,只有一个线程会比较成功并更新变量的值,其他线程则会因为比较失败而需要重新尝试,相对同步机制,性能会有一定的优化
- Lock 实现原子性、可见性、有序性
Java中接口定义了锁的操作接口,它的实现类,如ReentrantLock,内部持有一个AQS对象,AQS通过内部维护一个同步状态,以及一个FIFO队列来管理获取锁的线程。而这个管理线程的过程就会涉及到CAS和volatile的的操作
- Synchronized 实现原子性、可见性、有序性
同一时间只有一个线程可以执行synchronized的代码,意味着这个线程在执行时不会被干扰,其他线程也会按照预定的顺序执行同步代码,这样就实现了原子性和有序性。
而在该线程执行期间,它会直接在主内存中读取操作共享变量,当下个线程执行代码块时,也就会看到最新的数据,从而实现了可见性