javaee-多线程进阶

①常⻅的锁策略

(1)乐观锁&悲观锁

顾名思义

乐观锁:加锁的时候,认为锁竞争不会很激烈,不需要做额外工作

悲观锁:加锁的时候,认为锁竞争会很激烈,额外做一些工作(比如:加锁失败就等待 / 阻塞,直到拿到锁)

(2)重量级锁&轻量级锁

重量级锁:实现成本 / 开销维度大的锁,即运行更低效

轻量级锁:实现成本 / 开销难度小的锁,运行更高效

(3)挂起等待锁&自旋锁

挂起等待锁:获取锁失败 → 线程被挂起(进入阻塞队列)→ 锁释放后被唤醒

自选锁:获取锁失败 → 线程不挂起,循环重试(CPU 空转)→ 直到拿到锁 / 放弃

⾃旋锁伪代码:

java 复制代码
while (抢锁(lock) == 失败) {}

按之前的⽅式(挂起等待锁),线程在抢锁失败后进⼊阻塞状态,放弃CPU,需要过很久才能再次被调度.

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

6种锁的关系:

一般是:

悲观锁→重量级锁→挂起等待锁

乐观锁→轻量级锁→挂起等待锁

设计策略 实现成本 等待方式 典型例子 核心特征
悲观锁 轻量级 自旋锁 synchronized 轻量级锁 先锁后做、自旋等待、开销小
悲观锁 重量级 挂起等待锁 synchronized 重量级锁、ReentrantLock 先锁后做、挂起等待、开销大
乐观锁 轻量级 自旋锁 CAS、AtomicInteger、数据库版本号 先做后校验、自旋重试、无阻塞

而我们之前学的synchronized锁是自适应的,可以自由切换锁的类型1

(4)互斥锁和读写锁:

互斥锁:所有人都互斥,读和写都要抢同一把锁。

读写锁:读和读不互斥,读和写、写和写互斥,提高读多写少场景的效率。

因为读和读不会出现线程安全问题,但是写就会,所以有关写的都要互斥,读不用(从而提高效率)

(5)可重入锁&不可重入锁

可重入锁:同一线程已持有锁,再次获取该锁时不会死锁,而是直接成功(synchronized锁)

不可重入锁:同一线程已持有锁,再次获取该锁时会阻塞 / 死锁

(6)公平锁&非公平锁

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

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

(7)锁消除:锁消除是 JVM 提供的一种自动优化机制,核心是:JVM 在即时编译(JIT)阶段,检测到某些锁对象不可能被多线程访问,就会自动移除这些锁,避免无意义的加锁 / 解锁开销。

锁加了,但 JVM 发现没必要,就偷偷给你删了

(8)锁粗化:

先说锁粒度:锁保护的范围越大,粒度越粗;保护的范围越小,粒度越细。它直接影响并发效率 ------ 粗粒度锁安全但并发低,细粒度锁并发高但实现复杂、可能有额外开销

所以一个代码中,如果反复多次对锁粒度低的进行加锁,就可能被优化 成更粗粒度的加锁

  • 未优化:拿 1 个水果→开门→关门,重复 10 次(对应多次加锁 / 解锁);
  • 锁粗化优化:开门→一次性拿 10 个水果→关门(对应一次加锁 / 解锁)。
  • 所以虽然写的是未优化,但是运行起来有可能按优化的运行

②CAS

(1)CAS概念:

全称 Compare and swap,字面意思:"比较并交换",一个 CAS 涉及到以下操作:我们假设内存中的原数据 V,旧的预期值 A,需要修改的新值 B。

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

所以CAS是一种原子指令(由 CPU 硬件层面保证原子性),核心逻辑是:通过对比内存值与预期值,仅当二者一致时,才将内存值更新为目标值,整个过程一步完成、不可中断。

CAS 是无锁编程的核心技术,也是乐观锁的典型实现方式,全程无阻塞、无内核态切换,是并发编程中高效保证原子性的关键手段。

CAS伪代码:

java 复制代码
boolean CAS(address, expectValue, swapValue) {
            if (&address == expectedValue) {
                &address = swapValue;
                return true;
               }
                return false;
        }

(2)CAS是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

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

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

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

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

(3)CAS的应用:

1)实现原子类:

原子类是在不使用同步锁(如 synchronizedReentrantLock)的前提下,保证单个变量的多线程原子操作

java 复制代码
public class demo6 {

    private static AtomicInteger count=new AtomicInteger(0);
    public static void main(String[] args) {
    Thread t1=new Thread(()->{

        for(int i=0;i<50000;i++){
            count.getAndIncrement();
        }
    });

    }

①private只是编程规范, static用于多线程共享使用

②初始定义的()里填这个初始值,然后想使用就使用api方法

③想对count进行操作,就是用已经有的方法即可

java 复制代码
   count.getAndIncrement();//count++
   count.incrementAndGet();//++count
   count.addAndGet(n)//count+=n

所以这就是原子类,不用加锁也没有线程安全问题。boolean int long 甚至数组等等都有原子类,也有对应写好的方法

2)实现⾃旋锁

自旋锁伪代码:

这里使用CAS比较owner和null,如果相等说明锁没有被占用,那么把现在运行的线程赋值给owner,此时owner的值不再为null,其他的线程无法使用,从而反复执行while循环里的CAS代码进行比较,从而形成自旋锁

java 复制代码
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 和 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, 还是经历了一个变化过程。

解决方案:

• CAS?操作在读取旧值的同时,也要读取版本号.

• 真正修改的时候,

◦ 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.

◦ 如果当前版本号⾼于读到的版本号.就操作失败(认为数据已经被修改过了).

也就是说不能选有可能发生误判的变量来作为参考

③JUC(java.util.concurrent)的常⻅类

JUC 是 Java 提供的并发编程工具包 ,专门解决多线程开发中的线程同步、任务调度、并发容器、原子操作等问题,替代了传统的 synchronized + Thread 的低效写法,是高并发开发的核心工具。

(1)Callable接⼝

Callable和Runnable是并列关系,但是Callable有返回值(泛型),Runnable没有(void)

使用方法就是先单独初始化,<>内写类型,然后在{} 里重写call()方法,方法名要指定返回类型,方法里写执行的指令,然后需要返回(当然用lambda也可以)

而且还得引入一个相同储存类型的类FutureTask<>,把Callable传入()中,然后把实例化的FutureTask传入建立的线程里,启动后,结果仍然需要futuretask的方法提取

java 复制代码
public class demo7 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int res=0;
                for(int i=0;i<100;i++){
                    res+=i;
                }
                return res;
            }
        };//有;号!!

/*lambda表达式
        Callable<Integer> callable= () -> {
            int res=0;
            for(int i=0;i<100;i++){
                res+=i;
            }
            return res;
        };//有;号!!
        
*/
        FutureTask<Integer> futureTask=new FutureTask<>(callable);//中继?

        Thread t=new Thread(futureTask);
        t.start();

        System.out.println(futureTask.get());  //要声明抛出异常
    }
}

(2)ReentrantLock锁

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

但是根synchronized的代码块不一样,这里需要手动上锁解锁

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

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

・unlock (): 解锁.

・ReentrantLock 和 synchronized 的区别:

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

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

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

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

・更强大的唤醒机制. synchronized 是通过 Object 的 wait /notify 实现等待 - 唤醒。每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待 - 唤醒,可以更精确控制唤醒某个指定的线程.如何选择使用哪个锁?

・锁竞争不激烈的时候,使用 synchronized, 效率更高,自动释放更方便.

・锁竞争激烈的时候,使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为,而不是死等.

・如果需要使用公平锁,使用 ReentrantLock.

(3)Semaphore信号量

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

使用:定义里的括号就是初始值

acquire()就是信号量-1,release就是信号量+1,

信号量最小值是0,如果是0了还使用acquire那就阻塞,以下的代码无法运行,release()就是信号值+1,无上限

java 复制代码
Semaphore semaphore=new Semaphore(4);
        semaphore.acquire();
        System.out.println("执行");
        semaphore.acquire();
        System.out.println("执行");
        semaphore.acquire();
        System.out.println("执行");
        semaphore.release();
        System.out.println("执行");
        semaphore.release();
        System.out.println("执行");
        semaphore.release();
        System.out.println("执行");

(4)CountDownLatch:

让一个或多个线程等待,直到其他线程完成一组操作后,再继续执行

定义方法需要再括号内写数字,代表想运行的次数,相当于计数器

每执行完一个线程在尾部写上 方法名.countDown(); 就是初始值-1

CountDownLatch.await() 会在计数器减到 0 时自动解除阻塞,然后执行main线程里的代码

java 复制代码
 CountDownLatch count=new CountDownLatch(10);

        ExecutorService pool= Executors.newFixedThreadPool(4);
        for(int i=0;i<10;i++){
            int id=i;
            pool.submit(()->{
                System.out.println("执行"+id);
                try {
                    Thread.sleep(500);
                }catch (InterruptedException e){
                    throw new RuntimeException(e);
                }
                count.countDown();
            });
        }
        count.await();
        System.out.println("执行完10个了");

④多线程环境使⽤哈希表

(1)HashMap : 线程不安全

Hashtable:线程安全,但是效率低(对所有方法全部一律加锁)

(2)ConcurrentHashMap

是上面的代替品,只对写操作进⾏加锁。

每一个头结点都单独有自己的锁,节点用链表向下延伸

相关推荐
我真会写代码2 小时前
线程池高频面试题(整理版)
java·线程池
无敌秋2 小时前
C++ public, private, protected类的继承
开发语言·c++
m0_579393662 小时前
C++代码混淆与保护
开发语言·c++·算法
qq_148115372 小时前
C++中的享元模式实战
开发语言·c++·算法
__Yvan2 小时前
Kotlin 的 ?.let{} ?: run{} 真的等价于 if-else 吗?
android·开发语言·前端·kotlin
左左右右左右摇晃2 小时前
Java并发——线程间的通信
java·开发语言
小小小米粒2 小时前
[特殊字符] 正常部署 AI + 流式输出(Stream)[特殊字符] 为什么会 CPU 炸了?
开发语言·python
烟花巷子2 小时前
C++中的解释器模式
开发语言·c++·算法
用户298698530142 小时前
Java: 从 Word 文档中提取文本和图像
java·后端