悲观锁与乐观锁
悲观锁和乐观锁,是保证数据并发安全防止更新丢失的两种方法。
1、多线程并发,数据更新丢失
静态变量sCount线程不安全:
public class NoLock {
private static int sCount = 0;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//线程A让sCount自增100次
for (int i = 0; i < 100; i++) {
sCount++;
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//线程B让sCount自增100次
for (int i = 0; i < 100; i++) {
sCount++;
}
}
}, "B").start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Main" + sCount);
}
}
运行结果可能:
Main103
或可能:
Main200
2、悲观锁 Pessimistic Lock
认为程序并发严重,在读取数据的时候,认为其他线程会修改此数据,故加锁防止其他线程修改。 例子:使用synchronized加锁,保证数据线程安全。
public class PessimisticLock {
private static int sCount = 0;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//线程A让sCount自增100次
for (int i = 0; i < 100; i++) {
synchronized (PessimisticLock.class){
sCount++;
}
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//线程B让sCount自增100次
for (int i = 0; i < 100; i++) {
synchronized (PessimisticLock.class){
sCount++;
}
}
}
}, "B").start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Main" + sCount);
}
}
运行结果:
Main200
2.1、synchronized缺点
synchronized加锁确保了线程安全,但却是以消耗性能为代价的,synchronized会让没有得到锁资源的线程进入BLOCKED状态,当争夺到锁资源后恢复为RUNNABLE状态,此过程涉及操作系统用户模式和内核模式的转换,代价较高。
3、乐观锁 Optimistic Lock
认为程序并发不严重,在读取数据的时候,其他线程不会修改,故不加锁;但在更新数据的时候,会判断其他线程是否更新此数据,如果失败会让线程不断去尝试更新。 例子:使用原子操作类AtomicInteger的底层实现CAS机制。
public class OptimisticLock {
private static AtomicInteger sCount = new AtomicInteger(0);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//线程A让sCount自增100次
for (int i = 0; i < 100; i++) {
sCount.incrementAndGet();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//线程B让sCount自增100次
for (int i = 0; i < 100; i++) {
sCount.incrementAndGet();
}
}
}, "B").start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Main" + sCount);
}
}
运行结果:
Main200
3.1、CAS机制
CAS机制,Compare And Swap,即比较并替换,使用基本操作数:内存地址,旧的预期值,将修改的新值。更新一个变量的时候,只有当变量的预期值和内存地址当中的实际值相同时,才会将内存地址对应的值修改为新值。
3.2、CAS自旋
当变量的预期值与内存地址当中的实际值不相同时,线程会重新获取内存地址的当前值,并重新计算将要修改的新值,这种尝试的过程被称为自旋。
3.3、CAS缺点
| CAS的缺点 | 解释 |
|---|---|
| CPU开销较大 | 在并发量比较高的情况下,如果多线程反复尝试更新某变量,会给CPU带来很大压力。 |
| 无法保证代码块的原子性 | 只能保证一个变量的原子性操作,而不能像synchronized一样保证整个代码块的原子性 |
欢迎关注Android技术堆栈,专注于Android技术学习的公众号,致力于提高Android开发者们的专业技能!