CAS的三大问题
上面我们在学习多线程的原子操作时提到了可以使用java里面的atomic来完成多线程的计算,但是cas也有一些问题:
1、循环+CAS,自旋的实现让所有线程处于高速运行,争抢cpu执行时间的状态。如果操作长时间不成功,会带来很大的cpu资源消耗。
2、仅针对单个变量的操作,不能用于多个变量来实现原子操作。
3、ABA问题。
ABA问题,线程1 从内存当中获取到变量V的值是A,线程2也从内存当中获取到V的值A,然后线程2将V的值修改成B, 然后线程2又将变量V的值改成了A,这时候线程1 判断变量V的值也是A,然后就进行修改成功了。
这个时候可能就会有个疑问即使我中间变化了,可我最终还是把值给改成我所期待的值,并不会有影响,这种想法只是在修改一个Integer类型时,可能中间修改过多少次并没有太大的影响,但是如果是链表的话,那么这个影响是很大的:
package com.study.cas.aba;
// 存储在栈里面元素 -- 对象
public class Node {
public final String value;
public Node next;
public Node(String value) {
this.value = value;
}
@Override
public String toString() {
return "value=" + value;
}
}
package com.study.cas.aba;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
// 实现一个 栈(后进先出)
public class Stack {
// top cas无锁修改
AtomicReference<Node> top = new AtomicReference<Node>();
public void push(Node node) { // 入栈
Node oldTop;
do {
oldTop = top.get();
node.next = oldTop;
}
while (!top.compareAndSet(oldTop, node)); // CAS 替换栈顶
}
// 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
public Node pop(int time) {
Node newTop;
Node oldTop;
do {
oldTop = top.get();
if (oldTop == null) { //如果没有值,就返回null
return null;
}
newTop = oldTop.next;
if (time != 0) { //模拟延时
LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
}
}
while (!top.compareAndSet(oldTop, newTop)); //将下一个节点设置为top
return oldTop; //将旧的Top作为值返回
}
}
package com.study.cas.aba;
import java.util.concurrent.locks.LockSupport;
public class Test {
public static void main(String[] args) throws InterruptedException {
Stack stack = new Stack();
stack.push(new Node("B")); //B入栈
stack.push(new Node("A")); //A入栈
Thread thread1 = new Thread(() -> {
Node node = stack.pop(800);
System.out.println(Thread.currentThread().getName() +" "+ node.toString());
System.out.println("done...");
});
thread1.start();
Thread thread2 = new Thread(() -> {
LockSupport.parkNanos(1000 * 1000 * 300L);
Node nodeA = stack.pop(0); //取出A
System.out.println(Thread.currentThread().getName() +" "+ nodeA.toString());
Node nodeB = stack.pop(0); //取出B,之后B处于游离状态
System.out.println(Thread.currentThread().getName() +" "+ nodeB.toString());
stack.push(new Node("D")); //D入栈
stack.push(new Node("C")); //C入栈
stack.push(nodeA); //A入栈
System.out.println("done...");
});
thread2.start();
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
System.out.println("开始遍历Stack:");
Node node = null;
while ((node = stack.pop(0))!=null){
System.out.println(node.value);
}
}
}
可以看到,线程1想要将栈顶的A替换成B,但是线程2比1快,线程2先行将A的next替换成C、D,然后线程1进行比较,发现栈顶都是A,然后将next元素替换成B,这样,C、D就出栈了,这样就造成了数据丢失的情况。
这是因为比较条件不够充分的原因(只比较旧值),使用AtomicStampedReference加版本号可以解决这个问题,代码如下:
package com.study.cas.aba;
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.locks.LockSupport;
public class ConcurrentStack {
// top cas无锁修改
//AtomicReference<Node> top = new AtomicReference<Node>();
AtomicStampedReference<Node> top =
new AtomicStampedReference<>(null, 0);
public void push(Node node) { // 入栈
Node oldTop;
int v;
do {
v = top.getStamp();
oldTop = top.getReference();
node.next = oldTop;
}
while (!top.compareAndSet(oldTop, node, v, v+1)); // CAS 替换栈顶
}
// 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
public Node pop(int time) {
Node newTop;
Node oldTop;
int v;
do {
v = top.getStamp();
oldTop = top.getReference();
if (oldTop == null) { //如果没有值,就返回null
return null;
}
newTop = oldTop.next;
if (time != 0) { //模拟延时
LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
}
}
while (!top.compareAndSet(oldTop, newTop, v, v+1)); //将下一个节点设置为top
return oldTop; //将旧的Top作为值返回
}
}
java中锁的概念
自旋锁:是指一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断是否能够被成功获取,直到获取到锁才会退出循环。
乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
悲观锁:假定发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
独享锁(写):给资源上锁,线程可以修改资源,其他线程不能再枷锁;(单写)。
共享锁(读):给资源上读锁后只能读不能写,其他线程也只能加读锁,不能加写锁(多读)
可重入锁、不可重入锁:绝大部分的锁都是可重入锁,同一个线程可以重复获得锁,可以自由进入同一把锁所同步的其他代码,不可重入相反
公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,视为公平。
同步关键字synchronized
1、用于实例方法、静态方法时,隐式指定锁对象
2、用于代码块时,显式指定锁对象
3、锁的作用域:对象锁、类锁、分布式锁
4、引申:如果是多个进程,怎么办?
特性:可重入、独享、悲观锁
锁优化:锁消除(开启锁消除的参数:-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks)
例如:stringbuffer用了synchronized,是线程安全的,在单线程下,重复多次append会触发jit运行时编译器的性能优化,会认为重复地加锁解锁是没有意义的,就会消除锁。
锁粗化:是一种使用锁的思想,有些情况下我们希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗,jvm也会帮我做优化(重复执行很多次)。
加锁的状态如何记录?