Java EE 进阶---多线程(一)

目录

一、常见的锁策略

乐观锁 vs 悲观锁

        重量级锁 vs 轻量级锁

读写锁&普通互斥锁

 自旋锁&挂起等待锁

可重入锁&不可重入锁

公平锁&非公平锁

synchronized实现了哪些锁策略?

二、Compare And Swap  比较并交换

 基于CAS的应用

 CAS实现自旋锁

 CAS 的 ABA 问题

解决ABA问题

三、Synchronized 原理

四、其他的优化操作

锁消除

 锁粗化

五、JUC


一、常见的锁策略

乐观锁 vs 悲观锁

悲观锁 :
总是假设最坏的情况,对运行环境持悲观态,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
对运行环境持乐观态,刚开始不加锁,当有竞争的时候再去加锁。

重量级锁 vs 轻量级锁

在实现锁的过程中,消耗的资源多不多

轻量级锁:可以纯用户态的锁,消耗的资源比较小

重量级锁:可能会调用到系统的内核态,消耗的资源比较多

读写锁&普通互斥锁

在现实中并不是所有的锁都要互斥,互斥必然会消耗很多的资源,所以优化出读写锁
读锁:共享锁,读与读可以同时拿到锁资源
写锁:排他锁,不能与 写写,写读,读写

普通互斥锁:synchronized,只能一个线程拿到锁资源,其他的要参与锁竞争,没有竞争到锁的时候就要阻塞等待

 自旋锁&挂起等待锁

自旋锁:不停的询问资源是否被释放,如果释放了第一时间可以获取锁资源
挂起等待锁:等待通知之后再去竞争锁,并不会第一时问获取到锁资源


可重入锁&不可重入锁

可重入锁:对于同一个锁对象可以加多次锁
不可重入锁:不能对同一个锁对象加多次锁

公平锁&非公平锁

公平锁:先排队等待的线程先获取到锁资源
非公平锁:没有先来后到这么一说,谁抢到是谁的
所有有争抢的事情,绝大多数都是不公平的


synchronized实现了哪些锁策略?

  • 既是乐观锁与是悲观锁
  • 既是轻量级锁与重量级锁  轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待锁实现的
  • 是普通互斥锁
  •  既是自旋锁与是挂起等待锁
  •  是可重入锁
  • 是非公平锁


自旋锁是基于CAS实现的 

二、Compare And Swap  比较并交换

Java EE 进阶---多线程(一)_第1张图片

 

基于CAS的应用

Java EE 进阶---多线程(一)_第2张图片

 Java EE 进阶---多线程(一)_第3张图片

 CAS实现自旋锁

自旋锁伪代码:

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

Java EE 进阶---多线程(一)_第4张图片

 CAS ABA 问题

ABA 问题:

假设存在两个线程 t1 t2. 有一个共享变量 num, 初始值为 A.
接下来 , 线程 t1 想使用 CAS num 值改成 Z, 那么就需要先读取 num 的值 , 记录到 oldNum 变量中 .
使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是 , t1 执行这两个操作之间 , t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A
线程 t1 CAS 是期望 num 不变就修改 . 但是 num 的值已经被 t2 给改了 . 只不过又改成 A . 这个时候 t1 究竟是否要更新 num 的值为 Z ?
到这一步 , t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程 .
这就好比 , 我们买一个手机 , 无法判定这个手机是刚出厂的新手机 , 还是别人用旧了 , 又翻新过的手
.

解决ABA问题

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

  • CAS 操作在读取旧值的同时, 也要读取版本号. 真正修改的时候, 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
这就好比 , 判定这个手机是否是翻新机 , 那么就需要收集每个手机的数据 , 第一次挂在电商网站上的手机记为版本1, 以后每次这个手机出现在电商网站上 , 就把版本号进行递增 . 这样如果买家不在意这是翻新机, 就买 . 如果买家在意 , 就可以直接略过 .

 

三、Synchronized 原理

结合上面的锁策略 , 我们就可以总结出 , Synchronized 具有以下特性 ( 只考虑 JDK 1.8):
1. 开始时是乐观锁 , 如果锁冲突频繁 , 就转换为悲观锁 .
2. 开始是轻量级锁实现 , 如果锁被持有的时间较长 , 就转换成重量级锁 .
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁
Java EE 进阶---多线程(一)_第5张图片

 

1) 偏向锁
第一个尝试加锁的线程 , 优先进入偏向锁状态 .
偏向锁不是真的 " 加锁 ", 只是给对象头中做一个 " 偏向锁的标记 ", 记录这个锁属于哪个线程 .
如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 )
如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别
当前申请锁的线程是不是之前记录的线程 ), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 .
偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 .
但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 .
举个栗子理解偏向锁
假设男主是一个锁 , 女主是一个线程 . 如果只有这一个线程来使用这个锁 , 那么男主女主即使不领证结婚( 避免了高成本操作 ), 也可以一直幸福的生活下去 .
但是女配出现了 , 也尝试竞争男主 , 此时不管领证结婚这个操作成本多高 , 女主也势必要把这个动作完成了, 让女配死心 .
2) 轻量级锁
随着其他线程进入竞争 , 偏向锁状态被消除 , 进入轻量级锁状态 ( 自适应的自旋锁 ).
此处的轻量级锁就是通过 CAS 来实现 .
通过 CAS 检查并更新一块内存 ( 比如 null => 该线程引用 )
如果更新成功 , 则认为加锁成功
如果更新失败 , 则认为锁被占用 , 继续自旋式的等待 ( 并不放弃 CPU).
自旋操作是一直让 CPU 空转 , 比较浪费 CPU 资源 .
因此此处的自旋不会一直持续进行 , 而是达到一定的时间 / 重试次数 , 就不再自旋了 .
也就是所谓的 " 自适应 "
3) 重量级锁
如果竞争进一步激烈 , 自旋不能快速获取到锁状态 , 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex .
执行加锁操作 , 先进入内核态 .
在内核态判定当前锁是否已经被占用
如果该锁没有占用 , 则加锁成功 , 并切换回用户态 .
如果该锁被占用 , 则加锁失败 . 此时线程进入锁的等待队列 , 挂起 . 等待被操作系统唤醒 .
经历了一系列的沧海桑田 , 这个锁被其他线程释放了 , 操作系统也想起了这个挂起的线程 , 于是唤醒
这个线程 , 尝试重新获取锁 .

四、其他的优化操作

锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.

什么是 " 锁消除 "
有些应用程序的代码中 , 用到了 synchronized, 但其实没有在多线程环境下 . ( 例如 StringBuffer) 此时每个 append 的调用都会涉及加锁和解锁 . 但如果只是在单线程中执行这个代码 , 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销 .

Java EE 进阶---多线程(一)_第6张图片

 锁粗化

一段逻辑中如果出现多次加锁解锁 , 编译器 + JVM 会自动进行锁的粗化 .
锁的粒度 : 粗和细
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
实际开发过程中 , 使用细粒度锁 , 是期望释放锁的时候其他线程能使用锁 .
但是实际上可能并没有其他线程来抢占这个锁 . 这种情况 JVM 就会自动把锁粗化 , 避免频繁申请释
放锁 .
Java EE 进阶---多线程(一)_第7张图片

 


五、JUC

java.util.concurrent 包的简称,JDK1.5之后对多线程的一种实现,这个包下放的类都和多线程有关,提供了很多工具类

Java EE 进阶---多线程(一)_第8张图片

Callable 接口

Callable 是一个 interface . 相当于把线程封装了一个 " 返回值 ". 方便程序猿借助多线程的方式计算结果 .也是描述任务的接口.
Java EE 进阶---多线程(一)_第9张图片

 

public class Demo03_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 < 5; i++) {
                    sum += i;
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("等待1秒");
                }
                // 返回结果
                return sum;
            }
        };

        // 通过FutureTask类来创建一个对象,这个对象持有callable
        FutureTask futureTask = new FutureTask<>(callable);
        // 创建线程并指定任务
        Thread thread = new Thread(futureTask);
        // 让线程执行定义好的任务
        thread.start();
        // 获取线程执行的结果
        System.out.println("等待结果...");
        Integer result = futureTask.get();
        // 打印结果
        System.out.println(result);

    }
}

ReentrantLock

可重入互斥锁. synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

ReentrantLock synchronized 的区别:
  • synchronized 是一个关键字, JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准
       库的一个类 , JVM 外实现的 ( 基于 Java 实现 ).
  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活但是也容易遗漏 unlock.
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.

Java EE 进阶---多线程(一)_第10张图片

 

public class Demo05_ReentrantLock {
    /**
     * ReentrantLock可以根据不同的Condition去休眠或唤醒线程
     * 同一把锁可以分为不同的休眠或唤醒条件
     */
    private static ReentrantLock reentrantLock = new ReentrantLock();
    // 定义不同的条件
    private static Condition boyCondition = reentrantLock.newCondition();
    private static Condition girlCondition = reentrantLock.newCondition();

    public static void demo05_Condition () throws InterruptedException {
        Thread threadBoy = new Thread(() -> {
            // 让处理男生任务的线程去休眠
            try {
                boyCondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 唤醒处理女生任务的线程
            girlCondition.signalAll();
        });

        Thread threadGirl = new Thread(() -> {
            // 让处理女生任务的线程去休眠
            try {
                girlCondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 唤醒处理男生任务的线程
            boyCondition.signalAll();
        });



    }

    /**
     * 创建读写锁
     */
    public static void demo04_ReadWriteLock () {
        // 创建
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        // 获取读锁, 共享锁,读与读可以同时进行
        readWriteLock.readLock();
        // 获取写锁,排他锁(互斥锁),读写,写读,写写不能共存
        readWriteLock.writeLock();
    }

    /**
     * 演示创建一个公平锁
     */
    public static void demo03_fair () {
        // 通过构造方法,传入true时为公平锁,false为非公平锁,默认为false
        ReentrantLock reentrantLock = new ReentrantLock(true);
    }

    /**
     * 模拟业务中如果出现异常情况,如何释放锁
     */
    public static void demo02 () throws Exception {
        // 创建一个ReentrantLock对象
        ReentrantLock reentrantLock = new ReentrantLock();
        // 加锁
        reentrantLock.lock();
        try {
            // TODO : 业务逻辑
            throw new Exception("业务出现异常");
        } finally {
            // 保证出现异常的时候也可以释放锁
            reentrantLock.unlock();
        }
    }

    /**
     * 演示基本方法
     * @throws InterruptedException
     */
    public static void demo01_lock() throws InterruptedException {
        // 创建一个ReentrantLock对象
        ReentrantLock reentrantLock = new ReentrantLock();
        // 加锁
        reentrantLock.lock();
        // 尝试加锁, 死等
        reentrantLock.tryLock();
        // 尝试加锁,有超时时间
        reentrantLock.tryLock(1, TimeUnit.SECONDS);

        // 释放锁
        reentrantLock.unlock();
    }
}

你可能感兴趣的:(java-ee,java,jvm)