线程安全问题是我们每个Java后端开发必知必会的知识点,今天我们就一文搞定如何实现线程同步,细讲目前常见的实现线程同步的方式。
当多个线程访问公共资源的时候,才可能出现数据安全问题,那么如果我们的整个类的代码没有使用公共资源,那么这个类就是线程安全的。比如:
public class StatelessService {
public void add(String status) {
System.out.println("add status:" + status);
}
public void update(String status) {
System.out.println("update status:" + status);
}
}
在 Java 中,不可变对象(Immutable Object)是指其内部状态在创建后无法被修改的对象。这种特性带来了线程安全、易于维护和共享等优势。
以下是 Java 中常见的不可变对象及其分类:
String
类Integer
、Long
、Float
、Double
、Short
、Byte
、Character
、Boolean
BigInteger
和 BigDecimal
LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
Collections.unmodifiableXXX()
方法创建,返回的集合视图不可修改,尝试修改会抛出 UnsupportedOperationException
List.of(T... elements)
、Set.of(T... elements)
、Map.of(K, V, K, V...)
创建final
,防止继承。private final
。setter
方法。List
),需在构造器中进行防御性复制。synchronized
关键字synchronized
是 Java 中用于实现线程同步的关键字,其核心目标是确保多线程环境下共享资源的安全访问,避免因并发操作导致的数据不一致或竞态条件问题。
synchronized
可以通过以下三种方式实现线程同步:
同步实例方法
this
)。public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 线程安全的操作
}
}
同步静态方法
Class
对象(如 Counter.class
)。public class Counter {
private static int staticCount = 0;
public static synchronized void incrementStatic() {
staticCount++; // 线程安全的操作
}
}
同步代码块
lock
)。public class Counter {
private int count = 0;
private final Object lock = new Object(); // 专用锁对象
public void increment() {
synchronized (lock) {
count++; // 仅对 count++ 操作加锁
}
}
}
Lock 接口是 Java 并发包(java.util.concurrent.locks
)中提供的一个线程同步机制,用于替代传统的 synchronized
关键字。它提供了比 synchronized
更灵活、更强大的同步控制能力,尤其适用于复杂并发场景的需求。
ReentrantLock 是 Lock
接口最常用的实现类,支持可重入性(同一线程可多次获取同一把锁)和公平锁/非公平锁的配置。以下是ReentrantLock的代码示例:
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁(必须在finally中确保释放)
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount()); // 输出 2000
}
}
如果是在单机的情况下,使用synchronized和Lock保证线程安全是没有问题的。但如果在分布式集群环境中,即某个应用如果部署了多个集群下的多个节点,每一个节点使用可以synchronized和Lock保证线程安全,但不同的节点之间,没法保证线程安全。
此时就需要考虑使用分布式锁了。分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。
这里我们以比较常用的redis分布式锁举例,使用 Jedis 实现 Redis 分布式锁,详细说明如下:
核心原理
SET key value NX EX
命令,确保原子性设置锁和过期时间。代码示例
import redis.clients.jedis.Jedis;
import java.util.Collections;
public class RedisDistributedLock {
// Redis 连接实例
private Jedis jedis;
// 锁的键名
private String lockKey;
// 客户端唯一标识(用于释放锁)
private String clientId;
// 锁的过期时间(秒)
private int expireTime;
public RedisDistributedLock(Jedis jedis, String lockKey, String clientId, int expireTime) {
this.jedis = jedis;
this.lockKey = lockKey;
this.clientId = clientId;
this.expireTime = expireTime;
}
/**
* 尝试获取锁
* @return 是否成功获取锁
*/
public boolean tryLock() {
String result = jedis.set(lockKey, clientId, "NX", "EX", expireTime);
return "OK".equals(result);
}
/**
* 释放锁
*/
public void unlock() {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));
}
// 示例:使用锁
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "resource_lock";
String clientId = java.util.UUID.randomUUID().toString();
RedisDistributedLock lock = new RedisDistributedLock(jedis, lockKey, clientId, 30); // 锁过期时间为30秒
if (lock.tryLock()) {
try {
// 执行业务逻辑
System.out.println("Lock acquired, executing critical section...");
Thread.sleep(10000); // 模拟业务操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
System.out.println("Lock released.");
}
} else {
System.out.println("Failed to acquire lock.");
}
jedis.close();
}
}
关键点说明
clientId
使用 UUID 或线程 ID 作为唯一标识,确保只有持有锁的客户端才能释放。GET
和 DEL
操作的原子性。finally
块中释放锁,确保资源释放。volatile
关键字当同一时间内有且只有一个线程对数据修改,且要保证其他线程全部读取到最新的修改后的数据状态,在这种场景下我们就可以使用volatile
关键字来保证数据的可见性。
public class VolatileExample {
private volatile boolean flag = true; // 确保可见性
public void run() {
while (flag) {
// 循环直到 flag 被修改为 false
}
System.out.println("Thread stopped.");
}
public void stop() {
flag = false; // 修改后,其他线程立即感知
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread thread = new Thread(() -> example.run());
thread.start();
Thread.sleep(1000); // 主线程等待1秒
example.stop(); // 修改 flag
thread.join();
}
}
ThreadLocal
对象ThreadLocal
(线程局部变量)是 Java 提供的一种线程安全机制,通过为每个线程维护独立的变量副本,确保每个线程只能访问自己的副本,避免线程间的数据竞争。
ThreadLocal
提供类get()和set()等方法,为每个使用该变量的线程都保存一份独立的副本。Thread类中有一个成员变量ThreadLocal.ThreadLocalMap,ThreadLocalMap则定义在ThreadLocal类中的静态内部类,它是一个Map,他的key是线程对象,value是线程的变量副本。
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
threadLocal.set(value + 1);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
}
在 Java 多线程编程中,线程安全的集合类(java.util.concurrent
包)是解决并发问题的核心工具。与传统的 Vector
、Hashtable
或 Collections.synchronizedXXX
不同,JUC 提供的集合类在性能和并发能力上进行了深度优化,适用于高并发场景。以下是对 JUC 线程安全集合类的详细介绍:
synchronized
或 ReentrantLock
锁控制并发访问。Vector
Hashtable
Stack
Collections.synchronizedList(new ArrayList<>())
Collections.synchronizedMap(new HashMap<>())
CAS
(Compare and Swap)和分段锁(Segment)减少锁粒度。ConcurrentHashMap
ConcurrentSkipListMap
ConcurrentSkipListSet
CopyOnWriteArrayList
CopyOnWriteArraySet
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
SynchronousQueue
DelayQueue
JUC 提供了基于 CAS(Compare and Swap)的原子类,用于实现无锁的线程安全操作。常见原子类如下:
AtomicInteger
、AtomicLong
:原子整数操作。AtomicReference
:原子引用类型操作。AtomicBoolean
:原子布尔操作。AtomicArray
:原子数组操作。import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子自增
}
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount()); // 输出 2000
}
}
有时候,我们在操作集合数据时,可以通过数据隔离设计 ,来保证线程安全。
比如每个用户只被线程池中的一个线程处理,不存在多个线程同时处理一个用户的情况。所以这种人为的数据隔离机制,也能保证线程安全。
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(8,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue(500),
new ThreadPoolExecutor.CallerRunsPolicy());
List<User> userList = Lists.newArrayList(
new User(1L, "汤姆", 11, "北京"),
new User(2L, "李雷", 13, "天津"),
new User(3L, "韩梅梅", 18, "云南"));
for (User user : userList) {
threadPool.submit(new Work(user));
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(userList);
}
static class Work implements Runnable {
private User user;
public Work(User user) {
this.user = user;
}
@Override
public void run() {
user.setName(user.getName() + "测试");
}
}
}
数据隔离设计还有另外一种场景:kafka生产者把同一个订单的消息,发送到同一个partion中。每一个partion都部署一个消费者,在kafka消费者中,使用单线程接收消息,并且做业务处理。
这种场景下,从整体上看,不同的partion是用多线程处理数据的,但同一个partion则是用单线程处理的,所以也能解决线程安全问题。