多线程进阶

目录

常见的锁策略

乐观锁 vs 悲观锁

重量级锁 vs 轻量级锁

自旋锁 vs 挂起等待锁

读写锁

读加锁

写加锁

可重入锁 vs 不可重入锁

公平锁 vs 非公平锁

CAS

实现原子类

实现自旋锁

ABA 问题

synchronized 原理

 锁升级

锁消除

锁粗化

小结:


常见的锁策略

实际开发中涉及到的锁不仅仅是 synchronized 一种, 不仅仅局限于 Java 中

自己需要实现一把锁需要更深刻的理解

乐观锁 vs 悲观锁

 锁的一种特性, 是 " 一类锁 " ,  不是一把具体的锁

悲观乐观, 是对后续锁冲突是否激烈(频繁) 给出的预测

如果预测接下来所冲突的概率不大, 就可以少做一些工作, 成为 乐观锁, 反之称为悲观锁.

重量级锁 vs 轻量级锁

轻量级锁, 所的开销小

重量级锁, 锁的开销大

乐观锁  --   轻量级锁

悲观锁  --   重量级锁

前者是预测锁冲突的概率, 后者是实际的开销

自旋锁 vs 挂起等待锁

自旋锁 是 轻量级锁的典型实现

挂起等待锁 是 重量级锁的典型实现

挂起等待锁 借助 系统api 实现, 一旦出现锁竞争了, 就会在内核中出发一系列动作(比如进入阻塞状态, 阻塞开销很大)

自旋锁往往在 纯用户态 实现. 

读写锁

把加锁分成两种

读加锁

读的时候, 能读, 不能写

写加锁

写的时候, 不能读, 不能写

可重入锁 vs 不可重入锁

一个线程针对同一把锁, 连续加锁两次, 不会死锁, 就是可重入锁, 会死锁就是不可重入锁

公平锁 vs 非公平锁

很多线程尝试加一把锁时, 一个线程拿到锁后其他线程等待, 一旦第一个线程释放锁后, 接下来看线程怎么加锁?

公平锁: 所有线程按先来后到竞争锁

非公平锁: 剩下线程以 " 均等 " 的概率得到锁

系统默认提供的加锁是 非公平锁

这些锁策略描述了一把锁的基本特点:

synchronized 属于哪一种?

1> " 悲观乐观 ", 自适应

2> " 重量轻量 ", 自适应

3> " 自选 挂起等待 ", 自适应

4> 不是读写锁

5> 是可重入锁

6> 是非公平锁

CAS

功能: Compare and swap 比较和交换的是 内存 和 寄存器

CAS 是一个 CPU 指令, 一个 CPU 指令就能完成比较交换的逻辑, 单个 CPU 指令是原子的, 用来代替加锁. 基于 CAS 实现线程安全的方式被称为 " 无锁编程 "

优点: 保证线程安全, 避免阻塞(效率高)

缺点: 1> 代码更复杂不好理解

         2> 只能是和一些特定场景, 不如加锁方式更普遍

 CAS 本质上是 CPU 提供的指令 => 被操作系统封装成 api => 被JVM封装 => 程序员使用

实现原子类

实现原子类例子:

int 进行 ++, 不是原子的 (load, add, save), 会出现线程安全问题.

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        t1.start();
        t2.start();
        //如果没有这俩 join , 肯定不行, 线程还没有自增完毕, 就开始打印了,
        //打印出来的count 可能是 0;
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }
}

AtomicInteger 基于 CAS 的方式对 int 进行封装, 此时 ++ 就是原子的了 

结论: 原子类里面是基于 CAS 来实现的

 public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 5000; i++) {
                // count++
                count.getAndIncrement();
//                // ++count
//                count.incrementAndGet();
//                // count--
//                count.getAndDecrement();
//                // --count
//                count.decrementAndGet();
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 5000; i++) {
                // count++
                count.getAndIncrement();
//                // ++count
//                count.incrementAndGet();
//                // count--
//                count.getAndDecrement();
//                // --count
//                count.decrementAndGet();
            }
        });
        t1.start();
        t2.start();
        //如果没有这俩 join , 肯定不行, 线程还没有自增完毕, 就开始打印了,
        //打印出来的count 可能是 0;
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }

实现的基本思路(如下图):

有一个内存 A, 两个寄存器 A, B

CAS (M, A, B) 

如果 M 和 A 值相同, M 和 B 的值交换, 整个操作返回 true

如果 M 和 A 值不相同, 无事发生, 整个操作返回 false

我们只关心 是否交换之后 M 的情况, B里面是啥不关心

\

线程不安全 本质上是进行自增的过程穿插进行了

CAS 这里的自增不要穿插执行, 核心思路是一样的, 加锁是通过阻塞的方式避免穿插, CAS 是通过重试的方式避免穿插.

这里面是把 A 和 value 比较过程本质是检查在是否有其他线程穿插过来.

CAS 实现的不止 AromicInteger. 还有很多其他的.

实现自旋锁

ABA 问题

CAS 进行关键操作, 使用过值没有发生变化, 来作为其他线程穿插判定依据

这种判定不够严谨, 因为可能出现 A -> B -> A 的情况, 中间变了, 又变回来了, 已经穿插执行了

虽然被穿插执行了, 但是由于值改回来了, 一般不会出现什么bug, 但在极端情况下就不好说了

举个例子:

去 ATM 取钱, 本身的账户 1000, 想要取 500, 但在过程中出现 bug, 按两下取钱按钮, 此时产生了两个线程进行扣款操作!!

 

但是如果此时在来一个 t3 线程,给我的线程余额充值 500, 这是我的 t1 线程中 value 就会再次增加500 变为 1000, t1 就会在次扣一次钱

解决方案:

只要让判定的数值 按照一个方向增长即可. 有增有减, 会出现 ABA 情况, 只是增加(减少) 就不会了, 针对账户余额(必须要能加能减)这样的概念可以引入一个额外的变量----版本号, 每次修改余额让版本号自增即可, 在使用 CAS 判定的时候不是判定余额, 而是版本号了.

在实际开发中, 一般不会直接使用 CAS, 都是封装好的.

synchronized 原理

synchronized 几个重要的机制:

1> 锁升级

2> 锁消除

3> 锁粗化

 锁升级

偏向锁:举个例子(线程池优化了 " 找下一任 " 的效率, 偏向锁优化了 " 分手 " 的效率)

我是个妹子

使用偏向锁之前, 看上一个小哥哥, 想和她交往, 培养感情后在一起了, 交往一段时间后就想分手, 就需要想办法, 过程比较低效

使用偏向锁之后, 我看上一个小哥哥, 只是搞暧昧, 把情侣能做的事全做了, 但是不捅破窗户纸, 到最后想分就分.如果搞暧昧的过程中如果有个妹子想接近小哥哥, 那么我直接和小哥哥表白确定情侣关系.

偏向锁只是一个标记, 不是真的加锁, 当锁冲突的时候, 偏向锁就会升级为 轻量级加锁, 就是真的加锁了, 偏向锁的核心思想就是" 懒汉模式 " 的另一种体现, 锁升级的过程中就是在 性能 和 线程安全 之间进行权衡

锁消除

是一种编译器优化的手段

编译器自动针对你当前写的 枷锁的代码 做出判定, 如果觉得这个场景不需要加锁, 就会把 synchronized 给优化掉

StringBuilder 不带 synchronized 

StringBuffer 带有 synchronized

如果在单个线程中使用 StringBuffer 就会自动优化掉 synchronized (触发概率不高)

锁粗化

锁的粒度

synchronized 里面代码越多认为锁的粒度越粗, 代码越少, 锁的粒度越细

粒度细的时候, 能够并发执行逻辑更多, 更有利于充分调用多核 CPU 资源, 但是如果粒度细的锁, 被反复进行加锁解锁, 实际效果可能不如粒度粗的锁

小结:

理解每个锁的含义, 转化为自己的话

  • 34
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值