全称是Compare and swap,字面意思:“比较并交换”,是乐观锁的一种实现方式,不会真正阻塞线程,而是不断尝试更新
假设当前主内存值为V,工作内存中的值为A,当前修改要修改的值为B(每个线程都有V,A,B)
CAS的流程如下:
比较内存中的值V和当前工作内存的值A是否相等
若相等,可以认为当前主内存的值没有被修改,当前线程就将值B写回主内存
若不相等,说明当前线程的值A已经过时了(主内存已经发生了变化),此时无法将B写回主内存,于是将当前主内存的最新值V保存到当前工作内存中,重新执行任务,再尝试写回B,不断循环,直到写回
具体CAS是如何实现的,则要看不同的操作系统,JVM 用到了不同的 CAS 实现原理:
- java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
- Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性
所以CAS是因为硬件予以了支持,软件层面才能做到
CAS的应用- 使用CAS实现了原子类
java.util.concurrent.atomic包中的所有类都是线程安全的原子类(并且是通过无锁实现的线程安全)
i++是个线程不安全的操作,而我们以AtomicInteger这个基于整形的原子类为例,有以下方法:
public static void main(String[] args) {
// 默认为0
AtomicInteger count = new AtomicInteger();
// 初始化为10
AtomicInteger count1 = new AtomicInteger(10);
// 等同于++i
System.out.println(count.incrementAndGet());// 1
// i++
System.out.println(count.getAndIncrement());// 1
// --i
System.out.println(count.decrementAndGet());// 1
// i--
System.out.println(count.getAndDecrement());// 1
// 获取值
System.out.println(count.getAndDecrement());// 0
}
要实现i++或者i–之类的原子操作,又不想上锁,就可以使用atomic包下的原子类来实现
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTest {
static class Counter {
// 基于整形的原子类
AtomicInteger count = new AtomicInteger();
void increase() {
// i++
count.incrementAndGet();// 使用CAS机制保证了变量的原子性
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count.get());// 10000
}
}
线程一和线程二中均为:V == 0, A == 0, B == 1
然后线程一想写回,此时旧的值 A == 主内存中的值 V ,因此把 B 写回,然后 V == 1
现在线程二想要写回,此时旧的值A == 0, 而 V == 1,两者不等,说明主内存中的值已经更新了,因此线程二重新读取更新后的 V 值,此时线程 2 的 A == 1,B自增之后 B == 2,然后再次尝试写回,如果 V 还是等于 2 ,就写回主内存,如果有被别的线程抢先修改了,重新读取主内存的值
这样循环往复,最终一定能保证线程的安全
- 使用CAS来实现自旋锁(自旋锁也是乐观锁的一种实现)
自旋锁是指获取锁失败的线程不仅如此阻塞态,而是在CPU上空转(线程不让出CPU,而是跑一些无用指令),不断查询当前锁的状态,就好像等红绿灯,脚踩刹车等待而不是熄火等待,这样的好处就是,启动快,开销小
自旋锁伪代码
public class SpinLock {
private Thread owner = null;// 当前获取锁的线程
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
this.owern表示当前获取锁的线程
null表示期望获取锁的线程为null,当前自旋锁没有被任何线程持有
只有当this.owner == null,当前自旋锁没有被任何线程持有,才尝试将this.owern == Thread.currentThread() 将持有锁的线程设置为当前线程
这三个参数就类似于CAS的(V,A,B)
CAS引发的ABA问题有两个线程t1和t2,同时修改共享变量num,初始的num == A
正常情况下,t1修改了num = B,t2再去修改的时候发现 num != A ,也就是t2的工作内存中的值已经过期了,因此不会进行修改,于是重新获取新的 num,继续尝试修改
但是会有这么种情况:
t1想要修改num的值为B,因此把数据读入主内存,但是此时t2开始执行任务了,t2先把A改为B,然后又把B改回为A,当t1线程准备写回的时候,发现num还是等于A,就会误以为这个值没有改动过,于是将把num更改为B
从结果来看好像并没有什么不妥,但实际上是存在问题的,不妨假设一个场景:
你在网上买了个东西,现在有两个线程t1和t2并行扣钱50
t1先执行,扣了50元,然后t2开始执行,按理说,由于初始值已经被修改,t2不会再执行了,但此时其中有个线程给t1又转了50,现在值又变成100了,t2线程发现值是100,就想着不是没扣呢么,然后又一次扣了50
本来只需要扣50的,结果由于ABA问题的存在,扣了100
为了解决ABA问题,就引入了一个版本号的概念,那么上述场景中,当t2执行num = B之后,版本号加1为2,当t2执行num = A之后,版本号有加1为3,此时t1线程中保存的版本号还是1,读取主内存的版本号发现都已经到第3版了,那肯定是不能乱修改了
具体的流程图参考乐观锁部分,这里只做简单回顾了
注意:
synchronized的升级策略只有当CAS的 V == A 的时候,才会用到版本号,来判断当前主内存的 V 是否被其他线程反复修改过了
在线程 1 看到 V == A 准备修改值之前,判断其他线程是否对主内存有过操作
如果线程发现V != A,那么不需要判断版本号就能直到被其他线程修改了
我们平时看起来都用的synchronized关键字,背后到底是什么锁,是由JVM来处理的,会根据竞争激烈的程度动态选择锁的实现(面试爱考)
随着竞争的逐渐激烈,会按下列顺序依次升级
无锁:
此时没有任何线程获取锁,因此对象就是无锁状态
偏向锁:
当有第一个线程t1尝试获取锁,JVM分配偏向锁给该线程t1,当t1线程再次获取锁的时候(重入或者再次执行),没有加锁和解锁的过程,只是验证一下锁的持有线程是否为t1,若是的话就直接放行
比如一个小区的停车场,如果是检测到车辆为小区内的车就直接放行,如果是外来车辆,就需要登记进入
轻量级锁:
当有第二个线程t2,在t1执行之后尝试获取锁,JVM就会取消偏向锁的状态,将锁升级为轻量级锁
使用CAS(自旋锁)来进行轻量级锁的获取,有加锁和解锁的过程,就是通过自旋来进行的
t2线程尝试通过自旋的方式来获取轻量级锁,如果还有其他线程在t2还没有结束的时候尝试获取锁,那其他线程都会自旋等待t2执行结束
重量级锁(悲观锁的实现,线程进入阻塞态):
当竞争非常激烈,多个线程同时竞争轻量级锁(一般来说就是当前线程数为CPU核数的一半),就会将轻量级锁膨胀为重量级锁
或者当自旋次数10次(默认值)以上,也会将轻量级锁膨胀为重量级锁(避免CPU空跑,就将线程阻塞)
或者当程序中调用了Object的wait方法,直接会将锁膨胀为重量级锁,无论当前竞争激烈与否,因为wait方法实际上需要对象monitor实现
轻量级锁只是在语言层面进行循环,重量锁之所以叫重量锁,是因为他要依赖于操作系统,提供mutex来实现重量级锁,实现用户态到内核态的切换
当线程获取锁时会进入阻塞态就是悲观锁,其他的不进入阻塞状态的是乐观锁
其余两种创建线程的方式前面我们已经讲了两种:一种是继承Thread类,另一种是实现Runnable接口
Runnable接口是不带返回值的接口,覆写run方法(线程的核心工作任务方法)
而我们接下去要讲的一种实现Callable接口,是带返回值的接口,覆写call方法(线程的核心工作任务方法,有返回值)
这几种创建线程的方式,最终都是启动线程,都需要传递给Thread类,然后通过start方法启动线程
实现Callable接口过去我们使用的Runnable接口都是没有返回值的,想要得到返回值,就需要一个对象或成员属性来接收
public class RunnableCount {
static class Counter {
int sum;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Object lock = new Object();
Thread t1 = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum++;
}
synchronized (lock) {
counter.sum = sum;
lock.notifyAll();
}
});
t1.start();
// 主线程阻塞,当子线程执行完run方法之后,获取计算结果
synchronized (lock) {
if(counter.sum == 0) {
lock.wait();
}
}
System.out.println(counter.sum);// 500500
}
}
而Callable就是带返回值的工作方法接口,线程核心方法是call方法,用FutureTask类接收Callable的返回值,然后启动线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable = new Callable() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
// 接收call方法的返回值使用FutureTask对象, FutureTask是Future接口的子类
// 表示此时FutureTask接收的是指定Callable对象的返回值
FutureTask futureTask = new FutureTask<>(callable);
// Thread类接收Callable接口必须通过FutureTas类传入
Thread t = new Thread(futureTask);
t.start();
// get方法会阻塞当前线程,直到call方法执行完毕,才恢复执行,因此不需要使用join方法
int ret = futureTask.get();
System.out.println(ret);// 500500
}
注意点:
线程池启动线程
- Callable接口的返回值使用FutureTask子类来接收
- Callable接口的对象最终也是通过Thread类的start方法启动线程,向Thread类传入FutureTask对象(包括call方法)
- 调用FutureTask的get获取call方法的返回值,调用get方法的线程会一致阻塞直到call方法执行结束,有返回值再继续执行
给线程池提交任务我们使用submit方法,因为这个方法既能接收run方法,也能接收call方法
submit(Runnable | Callable)
代码示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable = new Callable() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
ExecutorService pool = Executors.newFixedThreadPool(3);
// Future接口就是搭配Callable接口使用的,用于接收Callable的返回值
Future ret = pool.submit(callable);
System.out.println(ret.get());// 500500
}
juc下的常用子类
对象锁 juc.lock
在Java中除了synchronized关键字可以实现对象锁之外,java.util.concurrent中的Lock接口也可以实现对象锁
public interface Lock
juc.lock这个对象锁在JDK1.0就有,需要JVM借助操作系统提供的mutex系统原语实现
但是在JDK1.5之后,Java语言自己实现的互斥锁,不需要接住操作系统的monitor机制,因此在很多场景下Lock的效率会更高一些
// 使用Lock接口需要显示地进行加锁和解锁操作 // 加锁方法,获取锁失败的线程进入阻塞状态,直到其他线程释放锁,再次竞争,死等 void lock(); // 加锁方法,获取锁失败的线程进入阻塞态,等待一段时间时间到了,如果还没有获取到锁,就恢复执行其他代码 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 解锁方法 void unlock();
lock()和unlock()的使用:
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
lock.lock();// 代码从这里开始加锁,直到碰到unlock解锁
// 互斥代码块
try {
System.out.println("我是" + Thread.currentThread().getName() + "线程");
Thread.sleep(8000);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "释放锁");
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println("我是" + Thread.currentThread().getName() + "线程");
}finally {
lock.unlock();
}
}, "t2");
t2.start();
}
// 执行结果
我是t1线程
t1释放锁
我是t2线程
tryLock()的使用:
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
lock.lock();// 代码从这里开始加锁,直到碰到unlock解锁
// 互斥代码块
try {
System.out.println("我是" + Thread.currentThread().getName() + "线程");
Thread.sleep(8000);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "释放锁");
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
boolean isLocked = false;
try {
isLocked = lock.tryLock(3000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
if(isLocked) {
// 只有获取锁成功的线程才需要执行unlock方法
lock.unlock();
}
}
System.out.println("t1, 我等不到你了, 有缘再见");
}, "t2");
t2.start();
}
// 执行结果
我是t1线程
t1, 我等不到你了, 有缘再见
t1释放锁
synchronized和lock的区别:
synchronized是Java的关键字,由JVM实现,需要依赖操作系统提供的线程互斥原语(mutex); Lock标准库的类和接口,其中一个最常用的子类(ReentrantLock, 可重入锁)由Java本身实现,不需要依赖操作系统
synchronized隐式地加锁和解锁,lock需要显示进行加锁和解锁
synchronized在获取锁失败的线程时,会死等; lock可以使用tryLock等待一段时间之后自动放弃加锁,线程恢复执行
synchronized是非公平锁,ReentrantLock默认是非公平锁,可以在构造方法中传入true开启公平锁
ReentrantLock lock = new ReentrantLock(true); 传入true之后就是一个公平锁
synchronized不支持读写锁,Lock子类ReentrantReadWriteLock支持读写锁,读读并发,读写、写写互斥
public class ReentrantReadWriteLock
一般场景synchronized就足够了,如果需要用超市等待锁,公平锁,读写锁再考虑使用juc.lock
暑期编程PK赛 得CSDN机械键盘等精美礼品!


