【浅学Java】多线程进阶

多线程进阶

  • 1. 常见的锁策略
    • 1.0 锁的核心特性
    • 1.1 乐观锁和悲观锁
    • 1.2 读写锁
    • 1.3 重量级锁和轻量级锁
    • 1.4 自旋锁
    • 1.5 公平锁和非公平锁
    • 1.6 可重入锁和不可重入锁
  • 2. CAS
    • 2.0 什么是CAS
    • 2.1 CAS的应用
    • 2.2 CAS 中的ABA问题
  • 3. Synchronized原理
    • 3.0 重新认识Synchronized
    • 3.1 Synchronized——锁的升级
    • 3.2 Synchronized——锁的优化
  • 4. Callable接口
    • 4.1 Callable的作用
    • 4.2 Callable和Runnable的区别
    • 4.3 不应Callable接口来求和
    • 4.4 用Callable进行求和
      • 在这里为什么要构造一个FutureTask实例?
      • 这里的接口为什么进行了new?
  • 5. JUC(java.util.concurrent) 的常见类
    • 5.1 ReentrantLock
      • 【面试】ReentrantLock和Synchronized的区别
    • 5.2 原子类
    • 5.3 信号量(Semaphore)
    • 5.4 CountDownLatch
    • 【面试】有了 synchronized 还需要 juc 下的 lock?
    • 【面试】信号量听说过么?之前都用在过哪些场景下?
  • 6. 多线程安全的集合类
    • 6.1 多线程环境使用 ArrayList
    • 6.2 多线程环境使用队列
    • 6.3 多线程环境使用哈希表
      • 1. Hashtable
      • 2. ConcurrentHashMap
      • 3. 相关面试题
  • 7. 死锁
    • 7.1 不可重入锁的连续上锁
    • 7.2 两个线程两把锁,互不相让
    • 7.3 哲学家就餐问题
    • 7.4 产生死锁的原因
    • 7.5 解决死锁的策略

1. 常见的锁策略

1.0 锁的核心特性

【浅学Java】多线程进阶_第1张图片

1.1 乐观锁和悲观锁

  1. 乐观锁:在考虑冲突概率的时候,认为一般不会发生冲突。如果真的发生了冲突,再去解决冲突。
  2. 悲观锁:在考虑冲突概率的时候,认为发生冲突的概率很大,所以会提前加锁。

乐观锁发生冲突了怎么办?
我们可以引入一个版本号来处理。假设我们需要多线程修改 “用户账户余额”,设当前余额为 100.,引入一个版本号 version, 初始值为 1.,并且我们规定 “提交版本必须大于记录当前版本才能执行更新余额
(具体图示见课件)

1.2 读写锁

读写操作的线程安全分析:
当多个线程尝试读一个变量时,是不会有线程安全问题的; 但是当多个线程尝试修改一个变量时,或者一个线程写一个线程读时,就会有线程安全问题。

在有些场景中,写操作本来就少,主要以读操作为主。为了提高并发效率,我们就不能一味的加锁,应该根据不同的场景来给读和写分别加锁。

Synchronized锁是没有对读和写进行区分的,只要使用就一定互斥了。
Java中专门提供了一个类:
【浅学Java】多线程进阶_第2张图片
场景举例:
【浅学Java】多线程进阶_第3张图片
由上可知:在写少读多时,使用读写锁可以极大的减少冲突,冲突减少即阻塞等待的线程就减少了,这就极大的提高了程序执行的效率。

1.3 重量级锁和轻量级锁

区分重量级锁和轻量级锁的主要依据就是:看加锁解锁开销大不大。
加锁解锁开销大就是代表频繁的开锁加锁。

重量级锁:加锁解锁的开销很大,往往需要内核态来完成。
轻量级锁:加锁解锁的开销不大,只需要在用户态就能完成

1.4 自旋锁

应用场景:
当一个线程在竞争一个锁时,没有竞争成功,但是过不了多久,这个锁就会被释放,所以这个线程没必要进入阻塞状态并且放弃这个CPU。

自旋锁的伪代码:

while (抢锁(lock) == 失败) {}

自旋锁 vs 挂起等待锁:

想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~
挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意, 这个很长的时间间隔里, 女神可能已经换了好几个男票了).
自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能立刻抓住机会上位.

自旋锁是一种轻量级锁的实现方式:

  1. 优点:没有放弃CPU进行阻塞等待,而是不断地进行尝试获取,一旦锁被释放就能第一时间获取到锁。
  2. 缺点:如果锁被其他其他线程持有地时间比较久,那么就会持续地消耗CPU资源(而挂起等待的时候是不消耗CPU的)

【重点】synchronized锁中的轻量级锁,大概率就是通过自旋锁的方式进行实现的。

1.5 公平锁和非公平锁

【浅学Java】多线程进阶_第4张图片
注意:一般情况下的锁都是非公平锁,要想实现公平锁,就要加上一定的数据结构进行限制来达到约定的公平。

1.6 可重入锁和不可重入锁

【浅学Java】多线程进阶_第5张图片

2. CAS

2.0 什么是CAS

  1. CAS 全称为 compare and swap,即“比较并交换”的意思,它通过一次CPU的占用就可以同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤,达到“比较并交换”效果。
  2. 它的底层是一个原子的硬件指令。

2.1 CAS的应用

在Java中,CAS应用很多。

  1. 实现原子类
    典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作。
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

伪代码:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

原理分析图:
【浅学Java】多线程进阶_第6张图片

  1. 实现自旋锁
    基于 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;
   }
}

2.2 CAS 中的ABA问题

  1. 什么是ABA问题?
    【浅学Java】多线程进阶_第7张图片
  2. 如何解决ABA问题
    ABA问题是一种小概率事件,但是我们也应该重视它并解决它,解决方法很简单,和乐观锁一样,就是加上版本号
    【浅学Java】多线程进阶_第8张图片

3. Synchronized原理

3.0 重新认识Synchronized

Synchronized是一种自适应锁,经过上面对各种锁的认识,我们对Synchronized也有了更近一步的认识:

  1. Synchronized在初始状态为乐观锁。随着冲突的增多,就变为悲观锁
  2. Synchronized不是读写锁
  3. Synchronized初始状态为轻量级锁,如果锁被某些线程持有的时间过长/锁的冲突概率比较高时,就会变成重量级锁。
  4. Synchronized是非公平锁
  5. Synchronized是可重入锁
  6. Synchronized为轻量级锁时,大概率时一个自旋锁;为重量级锁的时候大概率为一个挂起等待锁。

3.1 Synchronized——锁的升级

【浅学Java】多线程进阶_第9张图片

3.2 Synchronized——锁的优化

【浅学Java】多线程进阶_第10张图片

4. Callable接口

4.1 Callable的作用

Callable是一个接口,它相当于把线程封装了一个返回值。

4.2 Callable和Runnable的区别

Runnable:只是描述一个过程,不关注结果,没有返回值。
Callable:也是描述一个过程,但是有返回值。

Callable中包含一个call方法,相当于Runnable中的run方法,都是用来描述一个具体的任务,不同的是:call方法是带有返回值的

4.3 不应Callable接口来求和

思路:

  1. 对于主线程来说,其必须得等t线程执行完之后在进行打印,才符合要求得思路
  2. 在主线程中进行等待(wait),等到 t 执行完循环之后,再通知(notify)主线程
public class ThreadDemo28 {
    static class Result{//设置静态内部类,就可以在实例化时,不需要创建外部类
        public int sum=0;
        public Object locker = new Object();
    }
    public static void main(String[] args) throws InterruptedException {
        Result result = new Result();
        Thread t = new Thread(){
            @Override
            public void run() {
                for(int i=1;i<=1000;i++){//这里存在优化,sum的值不能及时同步到内存当中
                    result.sum+=i;
                }
                synchronized (result.locker){
                    result.locker.notify();
                }
            }

        };
        t.start();
        synchronized (result.locker){
            while(result.sum==0){
                //当在t线程中的求和运算结果没有出来时,内存中sum的值一直就是0,直到运算结束才会同步到内存
                result.locker.wait();
            }
        }
        System.out.println(result.sum);
    }
}

4.4 用Callable进行求和

public class ThreadDemo29 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个匿名内部类, 实现 Callable 接口
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            //重写 Callable 的 call 方法, 完成累加的过程
            public Integer call() throws Exception {
                int sum=0;
                for(int i=0;i<=1000;i++){
                    sum+=i;
                }
                return sum;
            }
        };
        //把 callable 实例使用 FutureTask 包装一下
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的
        //call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中
        Thread t = new Thread(futureTask);
        t.start();
        
        //调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
        int result = futureTask.get();
        System.out.println(result);
    }
}

在这里为什么要构造一个FutureTask实例?

对于Thread类来说,其构造方法为要求放入的是一个Runnable,所以不能直接把Callable作为参数传到Thread中,所以就需要将Callable转换成符合要求的参数。

因此可以通过FutureTask的构造方法,传入Callable接口的实例,构造FutureTask对象,由于FutureTask间接实现了Runnable接口,将FutureTask作为Thread的参数即可满足需要。

这里的接口为什么进行了new?

Callable<Integer> callable = new Callable<Integer>() {
            @Override
            //重写 Callable 的 call 方法, 完成累加的过程
            public Integer call() throws Exception {
                int sum=0;
                for(int i=0;i<=1000;i++){
                    sum+=i;
                }
                return sum;
            }
        };

这里的 new 看似是对接口进行了实例化,其实并不是这样,接口并不能进行实例化,这只是匿名内部类的一种实现方式,所表达的意思就是一个匿名的类实现了Callable这个接口。

5. JUC(java.util.concurrent) 的常见类

5.1 ReentrantLock

可重入互斥锁,其定位于与synchronized类似,都是用来达到互斥效果,保证现成安全。

ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”

public class ThreadDemo30 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();//加锁,如果获取不到锁就死等
        reentrantLock.tryLock();//加锁,如果湖区不到锁,等待一段时间之后,就放弃加锁
        reentrantLock.unlock();//解锁
    }
}

【面试】ReentrantLock和Synchronized的区别

【浅学Java】多线程进阶_第11张图片

5.2 原子类

【浅学Java】多线程进阶_第12张图片

5.3 信号量(Semaphore)

信号量就是用来表示可用资源个数的一个计数器

理解信号量
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(4);//表示有四个可用资源

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("申请资源");
                    semaphore.acquire();
                    System.out.println("申请资源成功");
                    Thread.sleep(3000);
                    System.out.println("释放资源");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for(int i=0;i<10;i++){
            Thread t = new Thread(runnable);
            t.start();
        }
    }

运行结果:
【浅学Java】多线程进阶_第13张图片

5.4 CountDownLatch

同时等待 N 个任务执行结束.

好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.

public static void main(String[] args) throws InterruptedException {
        //表示有十个任务需要完成
        CountDownLatch latch =new CountDownLatch(10);

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    long time=(long) (Math.random() * 10000);
                    Thread.sleep(time);
                    System.out.println("比赛用时:"+time);
                    latch.countDown();//完成任务,任务数就减1
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        for(int i=0;i<10;i++){
            Thread t = new Thread(runnable);
            t.start();
        }
        latch.await();
        System.out.println("比赛结束");
    }

运行结果:
【浅学Java】多线程进阶_第14张图片

【面试】有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例,

  1. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
  2. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  3. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true 开启公平锁模式.
  4. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

【面试】信号量听说过么?之前都用在过哪些场景下?

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.

6. 多线程安全的集合类

原来的集合类, 大部分都不是线程安全的.

Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.

6.1 多线程环境使用 ArrayList

【浅学Java】多线程进阶_第15张图片

6.2 多线程环境使用队列

  1. ArrayBlockingQueue
    基于数组实现的阻塞队列
  2. LinkedBlockingQueue
    基于链表实现的阻塞队列
  3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列
  4. TransferQueue
    最多只包含一个元素的阻塞队列

6.3 多线程环境使用哈希表

HashMap 本身不是线程安全的.
在多线程环境下使用哈希表可以使用:

  1. Hashtable
  2. ConcurrentHashMap

1. Hashtable

【浅学Java】多线程进阶_第16张图片

2. ConcurrentHashMap

【浅学Java】多线程进阶_第17张图片

3. 相关面试题

【浅学Java】多线程进阶_第18张图片

7. 死锁

7.1 不可重入锁的连续上锁

【浅学Java】多线程进阶_第19张图片

7.2 两个线程两把锁,互不相让

【浅学Java】多线程进阶_第20张图片

7.3 哲学家就餐问题

【浅学Java】多线程进阶_第21张图片

7.4 产生死锁的原因

【浅学Java】多线程进阶_第22张图片

7.5 解决死锁的策略

【浅学Java】多线程进阶_第23张图片

你可能感兴趣的:(JavaEE,java,jvm,算法)