目录
一、CAS
1、CAS的简单介绍
2、CAS的实现
3、CAS的应用
(1)CAS实现原子类
(2)实现自旋锁
4、CAS引发的ABA问题
(1)ABA问题的解释
(2)ABA问题引发的bug
(3)ABA问题的解决方法
二、synchronized锁原理
(1)无锁
(2)偏向锁
(3)轻量级锁
(4)重量级锁
三、Callable接口
1、java中创建线程的四种方式
2、Callable接口
(1)创建线程计算 1 + 2 + 3 + ... + 1000, 不使用 Callable 版本
(2) 创建线程计算 1 + 2 + 3 + ... + 1000, 使用 Callable 版本
四、JUC的常见类
1、对象锁 juc.lock
2、Lock接口的常用方法
3、ReentrantLock 和 synchronized 的区别
五、死锁
1、死锁的解释
2、死锁的形成示例
3、避免死锁
六、相关代码
全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
● 比较 A 与 V 是否相等。(比较)
● 如果比较相等,将 B 写入 V。(交换)若不相等,说明当前线程的值A已经过时(主内存发生了变化),就将主内存的最新值V保存到工作内存中,此时无法将B写回主内存
● 返回操作是否成功。
(V是当前主内存值,A是当前工作内存值,B是当前线程想要修改的值)
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg; Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。
简而言之,是因为硬件予以了支持,软件层面才能做到。
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
public class AtomicTest {
class Counter{
//基于整型的原子类
AtomicInteger count=new AtomicInteger();
}
public static void main(String[] args) {
AtomicInteger count=new AtomicInteger();
//等同于 ++i
System.out.println(count.incrementAndGet());
//等同于 i++
System.out.println(count.getAndIncrement());
//等同于 --i
System.out.println(count.decrementAndGet());
//等同于 i--
System.out.println(count.getAndDecrement());
}
可以看出来,下面的代码完全没有用到锁,但是依旧是线程安全的
public class Atomic2 {
static class Counter {
AtomicInteger count = new AtomicInteger();
void increase() {
//++i 使用CAS机制保证变量的原子性
count.incrementAndGet();
}
}
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());
}
}
工作流程:
线程1执行cas操作,要将最新值1写回主内存,此时V==0、A==0,B==1,由于cas(V==A),所以就将1写回主内存,线程2执行cas操作,将最新值1写回主内存,此时V==1,A==0,B==1,cas(C!=A),说明主内存在线程2读取时已经有过修改,本次写回操作失败,将主内存最新值1加载到当前工作内存中,再次尝试以上操作,此时V==1,B==1,B==2,cas(V==A),就可以将最新值2写回到主内存
自旋锁就是获取锁失败的线程不进入阻塞态,而是在CPU上空转(线程不让出CPU,而是跑一些无用的指令),不断查询当前锁的状态。
this.ower:表示当前获取锁的线程
null:期望获取锁的线程为null,表示当前自旋锁没有被任何线程持有
只有当this.ower==null时,就尝试将当前锁this.ower==Thread.cuurrentThread(),将持有锁的线程设置为当前线程
ABA 的问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
先读取 num 的值, 记录到 oldNum 变量中.
使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A
大部分的情况下, t2 线程这样的一个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除一 些特殊情况
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
CAS 操作在读取旧值的同时, 也要读取版本号. 真正修改的时候,
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)
在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进行包装, 在内部就提 供了上面描述的版本管理功能.
在JVM中,虽然看上去都用的是synchronized锁,但是到底是什么具体的锁是由JVM进行处理的,JVM会根据竞争的激烈程度动态的选择具体实现的锁。
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 比特就业课 状态。会根据情况,进行依次升级。越往下走说明竞争越强。
synchronized void increase(){
val++;
}
此时没有任何线程调用increase()方法,没有任何线程尝试获取该锁,这是就是处于无锁状态
当第一个线程(t1)尝试获取锁时,JVM就会分配偏向锁给该线程,当这个线程再次获取锁时,没有加锁和解锁的过程,就只需要验证是否还是刚才那个线程(t1),如果是的话就直接通过
当有第二个线程(t2)在t1线程执行之后尝试获取锁,JVM就会取消偏向锁的状态,将锁升级为轻量锁,轻量级锁就是自适应的自旋锁。
当有很多个线程同时竞争轻量锁时(一般来说就是当前线程数占据了CPU的一半),JVM就会将轻量锁升级为重量级锁,此时就需要依赖操作系统提供的mutex来实现重量级锁。
只要在程序中调用了Objec.wait()方法,就会直接膨胀微轻量级锁,无论当前竞争是否激烈,因为wait方法实际上就是需要对象monitor实现的。
继承Thread类
实现Runnable接口->不带返回值的接口,覆写run方法(线程的核心工作方法)
实现Callable接口->带返回值的接口,覆写call方法(线程的工作方法,有返回值)
线程池
/**
* 使用Runnable实现
*/
public class NoCallable {
private static class Count{
int sum=0;
Object lock=new Object();
}
public static void main(String[] args) throws Exception {
Count count=new Count();
Thread t=new Thread(()->{
int sum=0;
for (int i = 0; i <=1000 ; i++) {
sum+=i;
}
synchronized (count.lock) {
count.sum = sum;
//唤醒主线程
count.lock.notify();
}
});
t.start();
//主线程阻塞等待子线程执行完毕之后再打印结果
synchronized (count.lock){
if (count.sum==0){
count.lock.wait();
}
}
System.out.println("子线程执行结束,最终结果为:"+count.sum);
}
}
可以看到, 上述代码需要一个辅助类 Result, 还需要使用一系列的加锁和 wait notify 操作, 代码复杂, 容易出错.
/**
* 使用Callable接口实现
*/
public class Callabletest {
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方法的返回值使用FutuerTask类
FutureTask futureTask=new FutureTask<>(callable);
//Thread类接收Callable接口必须通过FutuerTask类
Thread t=new Thread(futureTask);
t.start();
//get方法会阻塞当前线程,直到call方法执行完毕,才会恢复当前线程
int result=futureTask.get();
System.out.println("子线程结束,result ="+result);
}
}
可以看到, 使用 Callable 和 FutureTask 之后, 代码简化了很多, 也不必手动写线程同步代码了
①Callable接口的返回值使用FuterTask子类接收
②Callable接口的对象最终也是通过Thread类的start方法启动线程,向Thread类传入FuterTask对象
③调用FuterTask的get方法获取call方法的返回值,调用get方法的线程就会一直阻塞,直到call方法执行结束后,有返回值线程才会继续执行。
在java中除了synchronized关键字可以实现对象锁之外,java.util.concurrent中的Lock接口也可以实现对象锁
使用Lock接口需要显示的进行加锁和解锁操作,加锁获取锁失败就会进入阻塞状态(死等)
加锁,获取锁失败的线程进入阻塞态,等待一段时间后,时间过了如果还没获取到锁,就会放弃加锁,执行其他的代码
解锁操作
public class LockTest {
public static void main(String[] args) {
// 传入ture,此时就是一个公平锁
ReentrantLock lock = new ReentrantLock(true);
Thread t1 = new Thread(() -> {
lock.lock(); // 代码从这里开始加锁,直到碰到unlock解锁
// 互斥代码块
try {
System.out.println(Thread.currentThread().getName() + ":获取到锁,其他线程等待");
Thread.sleep(8000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock(); // 代码从这里解锁,
}
System.out.println(Thread.currentThread().getName() + "释放锁");
},"男1");
t1.start();
Thread t2 = new Thread(() -> {
boolean isLocked = false;
try {
isLocked = lock.tryLock(3000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (isLocked) {
// 只有获取锁成功的线程才需要执行unlock方法
lock.unlock();
}
}
System.out.println(Thread.currentThread().getName()+":不爱我就拉倒,我去找别的女孩~");
},"男2");
t2.start();
}
}
●synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准 库的一个类, 在 JVM 外实现的(基于 Java 实现).
●synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
●synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就 放弃.
●synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启 公平锁模式.
●synchronized不支持读写锁,Lock子类的ReentrantReadWriteLock支持读写锁.
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线 程被无限期地阻塞,因此程序不可能正常终止。
public class DeadLockTest {
public static void main(String[] args) {
Object projectLock = new Object();
Object songLock = new Object();
Thread t1 = new Thread(() -> {
synchronized (projectLock) {
System.out.println("舒服了,我开始写项目");
synchronized (songLock) {
System.out.println("老师先唱个曲儿,我再去写项目");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"学生");
Thread t2 = new Thread(() -> {
synchronized (projectLock) {
System.out.println("大家先把项目写完,我再唱个曲儿~~");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (songLock) {
System.out.println("大家写完项目了,我给大家来一首***");
}
}
},"老师");
t1.start();
t2.start();
}
}
死锁产生的四个必要条件:
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。】
循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样 就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让 死锁消失。
其中最容易破坏的就是 "循环等待".
rocket_class_Grammer: java的语法相关知识的学习笔记 - Gitee.comhttps://gitee.com/ren-xiaoxiong/rocket_class_-grammer/tree/master/src/Thread