多线程CAS、synchronized锁原理 、JUC以及死锁

目录

一、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、避免死锁 

​ 六、相关代码


一、CAS

1、CAS的简单介绍

全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

     ●  比较 A 与 V 是否相等。(比较)

     ●  如果比较相等,将 B 写入 V。(交换)若不相等,说明当前线程的值A已经过时(主内存发生了变化),就将主内存的最新值V保存到工作内存中,此时无法将B写回主内存

     ●  返回操作是否成功。

(V是当前主内存值,A是当前工作内存值,B是当前线程想要修改的值)

多线程CAS、synchronized锁原理 、JUC以及死锁_第1张图片

 2、CAS的实现

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

     java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;

     unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;                     Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。

简而言之,是因为硬件予以了支持,软件层面才能做到。

3、CAS的应用

(1)CAS实现原子类

标准库中提供了 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写回到主内存

多线程CAS、synchronized锁原理 、JUC以及死锁_第2张图片

(2)实现自旋锁

自旋锁就是获取锁失败的线程不进入阻塞态,而是在CPU上空转(线程不让出CPU,而是跑一些无用的指令),不断查询当前锁的状态。

多线程CAS、synchronized锁原理 、JUC以及死锁_第3张图片

 this.ower:表示当前获取锁的线程

 null:期望获取锁的线程为null,表示当前自旋锁没有被任何线程持有

只有当this.ower==null时,就尝试将当前锁this.ower==Thread.cuurrentThread(),将持有锁的线程设置为当前线程

4、CAS引发的ABA问题 

(1)ABA问题的解释

ABA 的问题:

       假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.

       接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要

               先读取 num 的值, 记录到 oldNum 变量中.

               使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.

      但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

多线程CAS、synchronized锁原理 、JUC以及死锁_第4张图片 (2)ABA问题引发的bug

大部分的情况下, t2 线程这样的一个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除一 些特殊情况

多线程CAS、synchronized锁原理 、JUC以及死锁_第5张图片

 (3)ABA问题的解决方法

    给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

    CAS 操作在读取旧值的同时, 也要读取版本号. 真正修改的时候,

    如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.

    如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)

多线程CAS、synchronized锁原理 、JUC以及死锁_第6张图片

 在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进行包装, 在内部就提 供了上面描述的版本管理功能.

二、synchronized锁原理 

    在JVM中,虽然看上去都用的是synchronized锁,但是到底是什么具体的锁是由JVM进行处理的,JVM会根据竞争的激烈程度动态的选择具体实现的锁。

    JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 比特就业课 状态。会根据情况,进行依次升级。越往下走说明竞争越强。

多线程CAS、synchronized锁原理 、JUC以及死锁_第7张图片

synchronized void increase(){
     val++;
}

(1)无锁

此时没有任何线程调用increase()方法,没有任何线程尝试获取该锁,这是就是处于无锁状态

(2)偏向锁 

当第一个线程(t1)尝试获取锁时,JVM就会分配偏向锁给该线程,当这个线程再次获取锁时,没有加锁和解锁的过程,就只需要验证是否还是刚才那个线程(t1),如果是的话就直接通过

多线程CAS、synchronized锁原理 、JUC以及死锁_第8张图片 (3)轻量级锁

当有第二个线程(t2)在t1线程执行之后尝试获取锁,JVM就会取消偏向锁的状态,将锁升级为轻量锁,轻量级锁就是自适应的自旋锁。

多线程CAS、synchronized锁原理 、JUC以及死锁_第9张图片

 (4)重量级锁

当有很多个线程同时竞争轻量锁时(一般来说就是当前线程数占据了CPU的一半),JVM就会将轻量锁升级为重量级锁,此时就需要依赖操作系统提供的mutex来实现重量级锁。

多线程CAS、synchronized锁原理 、JUC以及死锁_第10张图片

 只要在程序中调用了Objec.wait()方法,就会直接膨胀微轻量级锁,无论当前竞争是否激烈,因为wait方法实际上就是需要对象monitor实现的。

三、Callable接口 

1、java中创建线程的四种方式

继承Thread类

实现Runnable接口->不带返回值的接口,覆写run方法(线程的核心工作方法)

实现Callable接口->带返回值的接口,覆写call方法(线程的工作方法,有返回值)

线程池

2、Callable接口

 (1)创建线程计算 1 + 2 + 3 + ... + 1000, 不使用 Callable 版本

/**
 * 使用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 操作, 代码复杂, 容易出错.

(2) 创建线程计算 1 + 2 + 3 + ... + 1000, 使用 Callable 版本

/**
 * 使用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方法执行结束后,有返回值线程才会继续执行。

多线程CAS、synchronized锁原理 、JUC以及死锁_第11张图片 四、JUC的常见类

1、对象锁 juc.lock

在java中除了synchronized关键字可以实现对象锁之外,java.util.concurrent中的Lock接口也可以实现对象锁

多线程CAS、synchronized锁原理 、JUC以及死锁_第12张图片

 2、Lock接口的常用方法

使用Lock接口需要显示的进行加锁和解锁操作,加锁获取锁失败就会进入阻塞状态(死等)

多线程CAS、synchronized锁原理 、JUC以及死锁_第13张图片

 加锁,获取锁失败的线程进入阻塞态,等待一段时间后,时间过了如果还没获取到锁,就会放弃加锁,执行其他的代码

 解锁操作

多线程CAS、synchronized锁原理 、JUC以及死锁_第14张图片

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();
    }
}

 3、ReentrantLock 和 synchronized 的区别

●synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准 库的一个类, 在 JVM 外实现的(基于 Java 实现).

●synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.

●synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就 放弃.

●synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启 公平锁模式.

●synchronized不支持读写锁,Lock子类的ReentrantReadWriteLock支持读写锁.

五、死锁 

1、死锁的解释

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线 程被无限期地阻塞,因此程序不可能正常终止。

多线程CAS、synchronized锁原理 、JUC以及死锁_第15张图片

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();
    }
}

2、死锁的形成示例

多线程CAS、synchronized锁原理 、JUC以及死锁_第16张图片

多线程CAS、synchronized锁原理 、JUC以及死锁_第17张图片

多线程CAS、synchronized锁原理 、JUC以及死锁_第18张图片

多线程CAS、synchronized锁原理 、JUC以及死锁_第19张图片

多线程CAS、synchronized锁原理 、JUC以及死锁_第20张图片

多线程CAS、synchronized锁原理 、JUC以及死锁_第21张图片

3、避免死锁 

死锁产生的四个必要条件:

     互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

     不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

     请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。】

     循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样 就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让 死锁消失。

其中最容易破坏的就是 "循环等待". 

多线程CAS、synchronized锁原理 、JUC以及死锁_第22张图片 六、相关代码

rocket_class_Grammer: java的语法相关知识的学习笔记 - Gitee.comhttps://gitee.com/ren-xiaoxiong/rocket_class_-grammer/tree/master/src/Thread

 

你可能感兴趣的:(多线程,java)