标题线程安全的概念
并发编程中,经常会遇到多个线程访问同一个共享变量,当同时对共享变量进行读写操作时,就会产生数据不一致的风险,这是线程不安全。
线程安全:在多线程环境和单线程环境,都能保证正确性(复合预期,行为与其规范完全一致),就是线程安全的。
Java中的锁分类
1、线程是否需要对资源加锁:乐观锁和悲观锁。
2、资源已经被锁定,线程是否阻塞:自旋锁/适应性自旋锁。
3、在嵌套调用中可以重复获取锁:可重入锁(递归锁)和不可重入锁(自旋锁)。
4、在线程阻塞时等待超时后中断:可中断锁。
5、从多个线程并发访问资源,Synchronized 四种状态:无锁、偏向锁、 轻量级锁 和 重量级锁。
6、多个线程获取锁的顺序是否根据排队顺序:公平锁和非公平锁。
7、多个线程能否获取同一把锁:排他锁和共享锁。
锁升级的过程
1、偏向锁:初始状态下,锁是偏向于第一个获取它的线程的。
2、轻量级锁:当另一个线程尝试获取锁时,偏向锁会升级为轻量级锁。这是因为存在多个线程竞争同一个锁,无法满足偏向锁的场景。
3、自旋锁:如果轻量级锁尝试失败,即无法成功获取锁,线程将进入自旋状态,不断尝试获取锁。自旋锁可以避免线程的阻塞和唤醒,减少性能损失。
4、重量级锁:当自旋锁尝试一定次数后仍然无法获取锁,锁将升级为重量级锁。重量级锁会将请求锁的线程阻塞,并且使用操作系统提供的底层互斥量来实现锁的同步。
锁升级过程中的策略选择
Java锁升级策略的选择是根据锁的竞争情况和并发性能的权衡。对于多个线程并发访问的场景,采用偏向锁可能无法满足需求,因为有多个线程竞争同一个锁。此时,采用轻量级锁和自旋锁的组合可以提高并发性能,减少线程的阻塞和唤醒开销。而在高度竞争的场景下,可能会直接升级为重量级锁,以保证线程的安全性。
锁升级过程的优化
在Java的锁升级过程中,有一些优化措施可以提高性能和并发能力。例如,自适应自旋锁和锁消除等技术可以根据运行时的实际情况进行锁升级策略的调整,减少不必要的自旋和锁操作。此外,锁粗化和锁膨胀等技术可以根据代码块的范围进行锁的调整,避免频繁的锁升级和降级。
Java实现多线程安全的方式
1、使用同步代码块保证线程安全
2、使用 Lock 接口保证线程安全
3、使用原子变量保证线程安全
4、使用线程安全的容器类保证线程安全
5、避免共享资源保证线程安全
具体步骤
1、使用同步代码块保证线程安全
可以使用 synchronized关键字来控制并发访问,保证同一时间只有一个线程可以访问同步代码块或方法。
public class Counter {
private int count;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
2、使用 Lock 接口保证线程安全
与 synchronized类似,Lock 接口也可以用来控制并发访问,比如:可以使用ReentrantLock 。
public class Counter {
private int count;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
count--;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
Lock 接口提供了更多的灵活性和更细粒度的控制,可以支持多个条件变量。
!!!! 在使用 synchronized 和 Lock 接口时,要注意避免死锁问题。
3、使用原子变量保证线程安全
在多线程访问时,原子变量可以保证变量的原子性操作,即使有多个线程同时访问该变量,也可以保证操作的正确性。
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public void decrement() {
count.decrementAndGet();
}
public int getCount() {
return count.get();
}
}
4、使用线程安全的容器类保证线程安全
除此之外,Java 中提供了一些线程安全的容器类,如比如:ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList 等。
public class ConcurrentHashMapThreadSafeExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 创建两个线程来操作ConcurrentHashMap
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
map.put("A", i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
map.put("B", i);
}
});
// 启动线程并等待执行完成
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(map);
使用这些容器类可以避免多线程访问容器时出现的并发问题。
5、避免共享资源保证线程安全
尽量避免多个线程访问同一个共享资源,可以使用线程本地变量(ThreadLocal)来保证线程的独立性。
public class ThreadLocalExample {
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 2; i++) {
executorService.submit(() -> {
String date = new ThreadLocalExample().formatDate(new Date());
System.out.println("Thread: " + Thread.currentThread().getName() + " formatted date: " + date);
});
}
executorService.shutdown();
}
private String formatDate(Date date) {
SimpleDateFormat dateFormat = dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
在这个示例中,我们创建了一个ThreadLocal对象,用于存储SimpleDateFormat对象,并为每个线程提供独立的SimpleDateFormat对象实例。