多线程锁策略

常见的锁策略

  • 乐观锁 vs 悲观锁

悲观锁:认为多个线程访问同⼀个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁

乐观锁:认为多个线程访问同⼀个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据

Synchronized 初始使用乐观锁策略, 当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略

  • 重量级锁 vs 轻量级锁

锁的核心特性 "原子性"

重量级锁:加锁机制重度依赖了 OS 提供了 mutex

轻量级锁: 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁

  • 自旋锁(Spin Lock)

大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU,这个时候就可以使用自旋锁来处理这样的问题

自旋锁是⼀种典型的 轻量级锁 的实现方式:

自旋锁伪代码:

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

优点:没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁

缺点:如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源 (而挂起等待的时候是不消耗 CPU 的)

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的

  • 公平锁 vs 非公平锁

公平锁:遵守 "先来后到",B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁

非公平锁:不遵守 "先来后到",B 和 C 都有可能获取到锁

注:公平锁和非公平锁没有好坏之分,关键还是看适用场景

  • 可重入锁 vs 不可重入锁

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类包括 synchronized关键字锁都是可重入的

注:Linux 系统提供的 mutex 是不可重入锁

  • 读写锁

读加锁和读加锁之间,不互斥

写加锁和写加锁之间,互斥

读加锁和写加锁之间,互斥

注:Synchronized 不是读写锁

  • CAS

全称:Compare and swap,字面意思:比较并交换

CAS 是怎么实现的:

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

unsafe 的 CAS 依赖的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg

Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性

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

CAS 有哪些应用:

(1)实现原子类:

public class test22 {
    // AtomicInteger可以将++或--操作封装成原子操作
    // 防止在多线程下针对同一个变量出现线程不安全问题,由于以下代码不涉及加锁操作,所以执行的效率更高
    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();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

AtomicInteger可以将++或--操作封装成原子操作,防止在多线程下针对同一个变量出现线程不安全问题,由于以下代码不涉及加锁操作,所以执行的效率更高

(2)实现自旋锁:

自旋锁伪代码

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;
    }
}
  • CAS 的 ABA 问题:

ABA 问题:就是t1线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程

注:ABA 问题引来 BUG

例如:银行取款问题

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

  • synchronized 原理

基本特点:

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.

2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.

3. 实现轻量级锁的时候大概率用到的自旋锁策略

4. 是⼀种不公平锁 ,是⼀种可重入锁 ,不是读写锁

加锁工作过程:

JVM 将 synchronized 锁分为 无锁→偏向锁→轻量级锁→重量级锁状态

注:不能降级

其他的优化操作:锁消除,锁粗化(将多个相同的操作放在一个锁内进行完成)

  • JUC的常见类

    1. Callable 接口:Callable 是⼀个 interface,相当于把线程封装了一个 "返回值"

    public class test24 {
        // JUC的常见类 Callable
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            Callable<Integer> callable = new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int result = 0;
                    for (int i = 1; i < 1000; i++) {
                        result += i;
                    }
                    return result;
                }
            };
            // 创建FutureTask对象来接受callable的返回值
            FutureTask<Integer> futureTask = new FutureTask<>(callable);
            Thread t = new Thread(futureTask);
            t.start();
            t.join();
            System.out.println(futureTask.get());
        }
    }

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

用法:lock():加锁, 如果获取不到锁就死等

**trylock(超时时间):**加锁,如果获取不到锁,等待一定的时间之后就放弃加锁

unlock():解锁

ReentrantLock 和 synchronized 的区别:

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

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

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

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

  • 信号量 Semaphore

    信号量:用来表示"可用资源的个数",本质上就是一个计数器

    代码示例:

    package thread;
    import java.util.concurrent.Semaphore;
    ​
    public class test25 {
    ​
        public static int count = 0;
    ​
        // 信号量Semaphore
        public static void main(String[] args) throws InterruptedException {
            Semaphore semaphore = new Semaphore(1);
            Thread t1 = new Thread(() -> {
                try {
                    for (int i = 0; i < 5000; i++) {
                        // 申请资源
                        semaphore.acquire();
                        count++;
                        // 释放资源
                        semaphore.release();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            Thread t2 = new Thread(() -> {
                try {
                    for (int i = 0; i < 5000; i++) {
                        semaphore.acquire();
                        count++;
                        semaphore.release();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(count);
        }
    }

  • CountDownLatch:同时等待 N 个任务执行结束

    代码示例:

    package thread;
    ​
    import java.util.concurrent.CountDownLatch;
    ​
    public class test26 {
        // 将一个任务分成多段去完成
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch latch = new CountDownLatch(10);
            for (int i = 0; i < 10; i++) {
                int id = i;
                Thread t = new Thread(() -> {
                    System.out.println("线程启动: " + id);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("线程结束: " + id);
                    latch.countDown();
                });
                t.start();
            }
            // 通过await等待所有线程执行完毕, 等countDown调用次数和构造方法一致时, await()才会返回
            latch.await();
        }
    }

  • 线程安全的集合类

    多线程环境使用 ArrayList:

    1. 自己使用同步机制 (synchronized 或者 ReentrantLock)

    2. Collections.synchronizedList(new ArrayList);
    3. 使用 CopyOnWriteArrayList:

      当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素

      添加完元素之后,再将原容器的引用指向新的容器

多线程环境使用队列:

LinkedBlockingQueue,ArrayBlockingQueue,PriorityBlockingQueue,TransferQueue

多线程环境使用哈希表:

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

Hashtable:只是简单的把关键方法加上了 synchronized 关键字

ConcurrentHashMap:

  • 相关面试题

    • ConcurrentHashMap的读是否要加锁,为什么?

      读操作没有加锁,目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的数据,搭配了 volatile 关键字

    • 介绍下 ConcurrentHashMap的锁分段技术?

      简单的说就是把若干个哈希桶分成一个 "段" (Segment), 针对每个段分别加锁,目的也是为了降低锁竞争的概率

      当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争

    • ConcurrentHashMap在jdk1.8做了哪些优化?

      取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象)

      将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式,当链表较长的时候(大于等于 8 个 元素)就转换成红黑树

    • Hashtable和HashMap、ConcurrentHashMap 之间的区别?

      HashMap:线程不安全,key 允许为 null

      Hashtable:线程安全,使用 synchronized 锁 Hashtable 对象,效率较低key 不允许为 null

      ConcurrentHashMap:线程安全使用 synchronized 锁每个链表头结点,锁冲突概率低, 充分利用 CAS 机制优化了扩容方式,key 不允许为 null

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值