一、背景
1.1 问题代码
有 1 个变量,有 100 个线程,每个线程对其加 100 次,代码如下:
public class MyRunnable implements Runnable{
// 记录浏览人次
private int count;
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("count====="+(++count));
}
}
}
public class Test1 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
for(int i=0;i<100;i++){
new Thread(myRunnable).start();
}
}
}
测试结果如下,可以发现并没有预期的得出 10000 的结果,因为出现了线程安全问题。为什么会少加了 2 次呢?我们分析下原因。
现在有 100 个线程针对一个 count 变量,假设此时 count 的值为 10,此时 100 个线程要同时对 count 进行 +1 操作,每个线程都需要先把 count 的值拿出来,然后进行 +1,最后再把 count 的值放进去,这样就造成了其他线程浪费了执行次数。
这就是多个线程访问同一共享资源出现的线程安全问题,接下来我们分别用悲观锁和乐观锁来解决这个问题。
二、悲观锁
2.1 背景
java 在 jdk1.5 之前都是靠 synchronized 关键字保证同步的,这样可以确保无论哪一个线程持有共享变量的锁,都采用独占的方式来访问这些变量,其实就是一种悲观锁,故 synchronized 是悲观锁。
2.2 存在的问题
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
一个线程持有锁会导致其他所有需要此锁的线程挂起。若一个高优先级的线程等待一个低优先级的线程会导致优先倒置,引起性能问题。
2.3 概念
一上来就加锁,每次只能有一个线程访问,访问完毕后再解锁。即使有 100 个线程竞争资源,也要有 99 个线程排队。
2.4 特点
线程安全,性能较差。
2.5 常用的实现方式
传统的关系型数据库里面用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前先上锁。再比如 java 里面的同步原语 synchronized 、Lock 等关键字的实现也是悲观锁的具体实现。
2.6 代码展示
使用 synchronized 关键字来解决上面出现的线程安全的问题,代码如下所示:
public class MyRunnable implements Runnable{
// 记录浏览人次
private int count;
String a = "abd";
@Override
public void run() {
synchronized (a){
for(int i=0;i<100;i++){
System.out.println("count====="+(++count));
}
}
}
}
三、乐观锁
3.1 概念
一开始不加锁,认为是没有问题的,线程一起运行,等要出现线程安全问题的时候才开始控制。
每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
3.2 特点
线程安全,性能较好。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
3.3 实现方式
乐观锁是一种思想,CAS 是这种思想的一种实现方式。
乐观锁的具体实现主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 CAS(Compare And Swap 比较并交换)。
JAVA 对 CAS 的支持:在 jdk1.5 中新增的 java.util.concurrent 就是建立在 CAS 之上的,相对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现,在性能上有很大的提高。
乐观锁适用于多读少写的场景 ( SVN 就是很好的例子),一般案例就是版本号控制,即在数据库表中增加一个 version 字段,这个数据标识当前数据的版本,每次更新操作都会 version+=1
3.4 原理
CAS:乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程不会被挂起,而是被告知这次竞争失败,并可以再次尝试。
CAS 操作中包含三个操作数 ---- 需要读写的内存位置(V),进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置的值更新为新值 B。否则处理器不做任何操作。
无论哪种情况,他都会在 CAS 指令之前返回该位置的值(在 CAS 的一些特殊情况下将返回 CAS 是否成功,而不提取当前值) CAS 有效的说明了 “我认为位置 V 应该包含 A 值;如果包含 A 值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现有的值即可”。这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。
3.5 代码展示
使用乐观锁解决线程安全的问题。
public class MyRunnable implements Runnable{
// 记录浏览人次
// 整数修改的乐观锁,是原子类实现的
private AtomicInteger count = new AtomicInteger();
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"--count====="+(count.incrementAndGet()));
}
}
}
public class Test1 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
for(int i=0;i<100;i++){
new Thread(myRunnable).start();
}
}
}