解决并发问题的方法(有锁、无锁)
1 并发问题解决的方式
- 无锁
- 局部变量
- 不可变对象
- ThreadLocal
- 有锁
- synchronized
- ReetrantLock
1.1 无锁的解决方式
1.1.1 局部变量
- 善用局部变量可以避免出现线程安全问题。
- 当每一个线程都运行同一行代码时,如果只是操作局部变量,则不可能会造成并发问题。因为每个线程操作的变量是局部的,并不存在交际。
public void test(){
int i = 0;
i++;
System.out.println(i);
}
1.1.2 不可变对象
- 这个对象本身是不可以被改变的。自然就不会害怕多个线程去访问它。
String s = "Hello"
// “Hello”就是一个不可改变的对象,他就是一个固定的字符串。
1.1.3 ThreadLocal
- 当多个线程去访问THreadLocal对象是,都会单独的为访问的线程提供一个该对象的副本。即每个对象只会被一个线程操作,自然无并发问题。
1.1.4 CAS原子类
- CAS = compare and swap 比较并交换
- CAS中有三个基本操作数
- 内存地址V
- 旧的预期值A
- 要修改的新数值B
- CAS思想:只有V里面的值和A相等,才会把V里面的值改成B
- Java中,采用CAS思想的类,都以Atomic开头,基于乐观锁,保证不会出现并发问题。
// simple using of AtomicInteger
private AtomicInteger counter = new AtomicInteger(0);
public void atomicAdd(){
counter.incrementAndGet();
}
// source code of Atomic
public class AtomicInteger extends Number implements java.io.Serializable{
private static final long serialVsersionUID = ...;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
// unsafe 提供硬件级别的原子操作,由于Java无法直接操控底层代码,为此Java需要使用native方法来拓展这部分功能,unsafe就是其中的一个操作入口。
// unsafe提供了分配、释放内存,挂起、恢复程序,定位对象字段内存地址,修改对象字段值,CAS操作。
private static final long valueOffset;
static {
try{
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField(value));
} catch(Exception ex){
throw new Error(ex)
}
}
}
// source code of getAndAddInt
// getAndAddInt method is a cas operate in unsafe class
public final int getAndAddInt(Object var1, long var2, int var4){
int var5;
do{
// use var5 to get the old value
var5 = this.getIntVolatile(var1, var2);
// compareAndSwapInt = cas
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// a counter of interface which do not have concurrency problem.
public static class AccessCounter{
// count the times of the acccess to this interface
AtomicInteger accessCount = new AtomicInteger(0);
public void access(){
accessCount.incrementAndGet();
sout("reslut is: " + accountCount.get());
}
}
CAS操作实际模拟
- 两个线程同时对一个变量K(初始值0)加一。
- 线程A,线程B,获得K的旧值0.
- 线程A,把K加1以后,再次访问主存中的K,发现现在K的值(0)与自己记录的旧值(0)相等,所以生效,把自己操作的结果写入主存(k=0 --> k=1)
- 线程B,把K加1以后,再次访问主存中的K,发现现在的K值(1)与自己记录的旧值(0)不等,所以失效。再次记录主存中K的值,作为一个“新”的旧值。
- 线程B,把K加1以后,再次访问主存中的K,发现现在的K值(1)与自己记录的旧值(1)相等,所以生效,把自己的操作的结果写入主存(k=1 --> K=2)
1.2 有锁的解决方式
1.2.1 synchronized & reentrandLock
- 采用悲观锁的策略。
- synchronized是通过语言层面来实现,reentrandLock是通过编程层面来实现。
public class Counter{
private int i = 0;
private ReentrantLock lock = new ReentrantLock();
// lock by reentrantLock
public void lockByReentrantLock(){
lock.lock();
try{
add();
} finally{
lock.unlock();
}
}
// lock by synchronized
public synchronized void lockBySynchronized(){
add();
}
private void add(){
i++;
}
}
- 加锁的原理:
- 线程A和线程B同时更新一个资源
- A抢到了锁,开始更新。
- B发现锁被人抢走了,进入等待队列。
- A更新完成,释放锁,传递消息给等待队列。
- B因为是等待队列里第一名,所以抢到锁,开始更新。