【浅学Java】多线程进阶

1. 常见的锁策略

1.0 锁的核心特性

在这里插入图片描述

1.1 乐观锁和悲观锁

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

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

1.2 读写锁

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

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

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

1.3 重量级锁和轻量级锁

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

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

1.4 自旋锁

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

自旋锁的伪代码:

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

自旋锁 vs 挂起等待锁:

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

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

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

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

1.5 公平锁和非公平锁

在这里插入图片描述
注意:一般情况下的锁都是非公平锁,要想实现公平锁,就要加上一定的数据结构进行限制来达到约定的公平。

1.6 可重入锁和不可重入锁

在这里插入图片描述

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

原理分析图:
在这里插入图片描述

  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问题?
    在这里插入图片描述
  2. 如何解决ABA问题
    ABA问题是一种小概率事件,但是我们也应该重视它并解决它,解决方法很简单,和乐观锁一样,就是加上版本号
    在这里插入图片描述

3. Synchronized原理

3.0 重新认识Synchronized

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

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

3.1 Synchronized——锁的升级

在这里插入图片描述

3.2 Synchronized——锁的优化

在这里插入图片描述

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的区别

在这里插入图片描述

5.2 原子类

在这里插入图片描述

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

运行结果:
在这里插入图片描述

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("比赛结束");
    }

运行结果:
在这里插入图片描述

【面试】有了 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

在这里插入图片描述

6.2 多线程环境使用队列

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

6.3 多线程环境使用哈希表

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

  1. Hashtable
  2. ConcurrentHashMap

1. Hashtable

在这里插入图片描述

2. ConcurrentHashMap

在这里插入图片描述

3. 相关面试题

在这里插入图片描述

7. 死锁

7.1 不可重入锁的连续上锁

在这里插入图片描述

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

在这里插入图片描述

7.3 哲学家就餐问题

在这里插入图片描述

7.4 产生死锁的原因

在这里插入图片描述

7.5 解决死锁的策略

在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值