盘点下场景并发问题的解决方案:
图从上图我们看到,解决并发问题的方法分为两大类: 无锁和有锁。无锁可分为:局部变量、不可变对象、ThreadLocal、CAS原子类;而有锁的方式又分为synchronized关键字和ReentrantLock可重入锁。
一、局部变量
局部变量是在方法、构造函数或代码块内部声明的变量,其作用域仅限于声明它的方法、构造函数或代码块内部。局部变量在每个线程中都有自己的副本,每个线程在访问局部变量时都操作自己的副本,不会对其他线程产生影响。
public class LocalVariableExample {
public static void main(String[] args) {
int i = 10; // i是方法内的局部变量
System.out.println(i); // 输出:10
// 这里不能直接访问i,因为i的作用域仅限于main方法
}
}
二、不可变对象
不可变对象是指在创建后其状态不能被修改或改变的对象。一旦创建了不可变对象,它的内部状态将保持不变,任何修改操作都不会改变该对象的状态。不可变对象是线程安全的,因为它们的状态不会被多个线程同时修改,从而避免了线程安全问题。
下面是一个示例演示了如何创建一个简单的不可变对象:
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
在上述示例中,ImmutablePerson
类的字段name
和age
都是私有的和final的,并且没有提供任何修改其状态的方法,因此该类是不可变的。创建一个不可变对象后,其状态将保持不变,其他线程可以安全地访问这个对象。
三、ThreadLocal
ThreadLocal
,每个线程都可以独立地存储和获取自己的数据副本,而不会影响其他线程。这样可以避免多线程之间共享数据时可能导致的并发问题,并提供一种简单的线程封闭(Thread Confinement)方法。
ThreadLocal
的主要特点:
-
线程本地存储:
ThreadLocal
允许每个线程都拥有自己的局部变量副本,线程之间的数据互不干扰。 -
数据隔离:通过
ThreadLocal
,可以实现将数据与线程关联,实现线程隔离。 -
简化多线程编程:使用
ThreadLocal
可以简化多线程编程,无需显式进行线程同步。 -
无需额外的锁:
ThreadLocal
避免了使用锁的开销,提高了并发性能。 -
对共享数据的访问:每个线程通过
ThreadLocal
直接访问自己的局部变量副本,无需竞争共享数据。
四、CAS原子类
AS(Compare-And-Swap)是一种并发编程中常用的无锁算法,用于实现原子操作。在多线程环境下,CAS可以解决共享变量的并发问题,保证操作的原子性,避免使用锁带来的性能开销。Java中提供了一系列CAS原子类,位于java.util.concurrent.atomic
包下,常用的CAS原子类包括:
-
AtomicBoolean:提供了原子的boolean值操作,例如
get()
、set()
、compareAndSet()
等。 -
AtomicInteger:提供了原子的整数操作,例如
get()
、set()
、getAndIncrement()
、compareAndSet()
等。 -
AtomicLong:提供了原子的长整数操作,功能类似于
AtomicInteger
。 -
AtomicReference:提供了对对象引用的原子性操作,例如
get()
、set()
、compareAndSet()
等。 -
AtomicIntegerArray:提供了原子性的整型数组操作,例如
get()
、set()
、getAndIncrement()
等。 -
AtomicLongArray:提供了原子性的长整型数组操作,功能类似于
AtomicIntegerArray
。 -
AtomicReferenceArray:提供了原子性的对象引用数组操作,例如
get()
、set()
、compareAndSet()
等。
使用CAS原子类,我们可以在不使用锁的情况下实现线程安全的操作,例如计数器的自增、自减等。CAS操作是基于底层硬件指令的原子性保证,它利用了CPU提供的CAS指令,实现了在单个CPU指令周期内对内存的读取、比较和写入操作,从而保证了操作的原子性。
以下是一个简单的示例演示了AtomicInteger
的使用:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private static AtomicInteger accessCount= new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
int value = accessCount.incrementAndGet();
System.out.println(Thread.currentThread().getName() + ": " + value);
});
thread.start();
}
}
}
实际运行情况:
在上述示例中,我们创建了一个AtomicInteger
对象作为计数器,每个线程通过incrementAndGet()
方法对计数器进行原子性的自增操作,无需使用锁,从而实现了线程安全的计数功能。输出结果会显示不同线程对计数器进行递增后的值,由于CAS操作的原子性,结果是正确的并且无竞争。
五、synchronized关键字和ReentrantLock可重入锁
Synchronized和ReentrantLock都是采用了悲观锁的策略。因为他们的实现非常类似,只不过一种是通过语言层面来实现(Svnchronized),另一种是通过编程方式实现(ReentrantLock),所以咱们把两种方式放在一起分析了.
先看看下面例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++; // 进行临界区操作
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
在上述示例中,我们创建了一个名为ReentrantLockExample
的类,它包含一个count
整数变量和一个ReentrantLock
对象。increment()
方法是一个临界区方法,它使用ReentrantLock
来获取锁,然后执行对count
变量的自增操作,最后释放锁。这样,我们确保在多线程环境下对count
的操作是线程安全的。
加锁原理图: