【JavaEE】锁策略 + synchronized原理 + CAS + JUC下常用类和接口 + 死锁

news2024/11/19 6:29:29

目录

锁策略

乐观锁VS悲观锁

轻量级锁VS重量级锁

自旋锁VS挂起等待锁

互斥锁VS读写锁

公平锁VS非公平锁

可重入锁VS不可重入锁

synchronized原理

synchronized特性

synchronized优化机制

加锁过程优化

锁消除

锁粗化

CAS

CAS概念

CAS原理

CAS应用

自旋锁的实现

原子类

CAS中的ABA问题

JUC常用类和接口

线程池常用类和接口

原子类

ReentrantLock类

构造方法

实例方法

Semaphore类

构造方法

实例方法

CountDownLatch类

构造方法

实例方法 

Callable接口

死锁

死锁是什么

死锁产生的原因

解决死锁


锁策略

锁策略不仅在Java的多进程中需要考虑,大多数情况下需要考虑锁的情况都有可能涉及到锁策略。


乐观锁VS悲观锁

这是两种类型的锁,都是预测锁冲突的大小角度而考虑的。

乐观锁:预测多个线程访问同一变量的概率比较小,基本不会发生锁冲突。所以每次访问共享变量的时候直接尝试获取变量同时识别当前的数据是否出现了访问冲突乐观锁的实现可以引入一个版本号,借助版本号来识别出数据是否发生冲突。

悲观锁:预测多个线程访问同一变量的概率比较大,很有可能发生锁冲突。所以每次访问共享变量的时候都要先加锁。悲观锁的实现就要先加锁,获取到锁再进行操作,没有获取到就等待。


轻量级锁VS重量级锁

轻量级锁:对于加锁的开销比较小。加锁机制不依赖操作系统提供的mutex锁。仅有少量的内核用户态切换,也不容易引发线程调度。

重量级锁:对于加锁的开销比较大。加锁机制很依赖操作系统提供的mutex锁。有大量的内核用户态切换,容易引发线程调度。

加锁本质:


自旋锁VS挂起等待锁

自旋锁:获取锁时,如果获取失败,并不会放弃CPU进行阻塞,而是立刻又获取锁。往复循环,知道获取到锁为止。

        优点:①如果锁释放比较快,那么这样就可以在第一时间获取到锁。

                   ②它这种方法由于没有放弃CPU,不涉及到线程阻塞和调度,所以这是轻量级锁。

         缺点:如果锁迟迟不释放,那么这样就会一直消耗CPU资源。

挂起等待锁:获取锁时,如果获取失败,直接放弃CPU,然后阻塞等待。如果被唤醒了,才会进行加锁。

        优点:不占用CPU资源。

        缺点:①无法第一时间获取到锁。

                   ②由于涉及到线程的阻塞和调度,所以这是重量级锁。


互斥锁VS读写锁

互斥锁:如果一个线程获取到锁了,如果还有想获取该锁,就只能进行阻塞等待。不管是这两个的操作是读还是写。

读写锁:读写锁提供三种操作:①对读操作加锁  ②对写操作加锁  ③解锁

                读锁和读锁之间没有互斥

                读锁和写锁之间存在互斥

                写锁和写锁之间存在互斥


公平锁VS非公平锁

设想一个场景,如果多个线程A,B,C获取锁。若A线程获取锁成功。随后B线程也想获取锁,失败然后阻塞等待;C线程最后也想获取,失败然后阻塞等待。过了一段时间,A线程释放了锁:

公平锁:因为释放前B线程比C线程先要获取锁,所以B线程获取到锁。按照先来后到的顺序。

非公平锁:B、C线程一起竞争锁,都有可能获取到锁。随机分配给都想要获取锁的线程。


可重入锁VS不可重入锁

可重入锁:一个线程可以多次获取一把锁,而且不会出现死锁的情况。在Java中,Reentant开头命名的锁都是可重入锁,JDK提供的所有所有现成的Lock实现类也都是可重入的。

不可重入锁:一个线程可以多次获取一把锁,但是出现死锁的情况。


synchronized原理

synchronized特性

根据上面的锁策略,我们可以简单分析以下synchronized所具有的特性。

1. 刚开始是乐观锁,如果锁冲突的概率变大之后就会升级成悲观锁。

2. 刚开始是轻量级锁,如果锁迟迟不释放,就会升级成重量级锁。

3. synchronized的轻量级锁很有可能是自旋锁。

4. 它是互斥锁。

5. 它是非公平锁。

6. 它是可重入锁。


synchronized优化机制

synchronized内部的优化机制需要有大概的认识。


加锁过程优化

加锁过程如下图:

加锁不是一下子就到重量级锁,秉持着能不加就不加,能加轻的就加轻的原则来加锁的。


锁消除

编译器和JVM会自动判断锁是否可以消除,如果可以消除就不进行加锁。

比如在单线程环境下使用一些自身带有synchronized的类,比如StringBuffer。这样加锁和解锁是没有必要的,只会浪费资源。


锁粗化

锁的粒度有粗细之分。

粗:表示这个锁的范围比较大;细则相反。

一般情况下,锁越细越好,这样释放锁的时候别的线程也可以获取到锁。

不过编译器和JVM会判断,如果没有其他线程来抢占该锁,就会自动把锁粗化。这样就不用频繁的加锁和解锁了。


CAS

CAS概念

CAS全称:Compare And Swap  比较和交换  一个CAS涉及到以下的操作。这些操作都是原子性的,相当于CPU当中的一条指令。


CAS原理

操作系统不同,JVM对于CAS实现原理有所不同。但是基本上都是按照以下思路来的:

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

unsafe这个类是靠JVM针对不同的操作系统实现的Atomic::cmpxchg。

Atomic::cmpxchg的实现是靠汇编的CAS的操作 + CPU硬件提供的lock机制,保证了原子性。
归根结底还是靠硬件提供支持的。

CAS应用

CAS相较于synchronized的使用,并不是那么的广泛。主要有自旋锁的实现和原子类。


自旋锁的实现

可以简单的理解为以下两步:

step1:自旋锁中有一个Thread locker引用——目的为了指向加锁的线程。初始值为Null

step2:locker与Null比较,如果相等(意味着这个锁没有被其他线程持有),就把当前线程的引用赋              值给locker;如果不相等(意味着这个锁已被其他线程持有),就继续尝试,直到成功或者变              成挂起等待锁。

原子类

这个类在java.util.concurrent.atomic中。它是为单个变量提供线程安全的编程。 

如下代码实例:

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo30 {

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

        AtomicInteger integer = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                // 这个方法相当于自增
                integer.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                integer.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(integer);
    }

}

 

 CAS在其中的作用就用这个自增方法举例。

在这个自增方法中,其中初始的 V = 0(也就是我们设定的0);

想要自增时,先要记录这个 V的值,用 A 保存(有可能此时V就被其他线程修改了)。

自增的值为 B = V + 1;

要想把B赋值给V,先要检查 A == V ?就把B赋值给V(相等说明未被修改) : 就啥也不干(不相等说明V被修改了)            (这一步是原子的,不怕有线程安全问题)


CAS中的ABA问题

ABA:一个变量本来值为A,然后变成了B,最后又变成A的意思。

在CAS中,当一个线程想要把B赋值给V时,想要用A与V比较。

A这个变量是共享变量。有可能另外一个线程在比较之前把A变了,然后又变回去了。

一般情况下这也不会出bug,但是不排除极端情况。

解决方法是引入版本号。比如给A加个版本,那么这时则是比较的A的版本号有无变化。

在Java中用AtomicStampedReference<V>来实现带有版本的功能。


JUC常用类和接口

JUC是java.util.concurrent的简称,这个包下面大多是和多线程有关的类和接口。


线程池常用类和接口

已在这篇文章中详细介绍了。

【JavaEE】线程池_p_fly的博客-CSDN博客

原子类

在上述CAS问题中也介绍过了。


ReentrantLock类

这个类在java.util.concurrent.locks包中。

这个类的功能与synchronized关键字的功能很像。都是可重入互斥锁,用来保证线程安全的。

构造方法

    // 默认构造方法创建的是非公平锁
    ReentrantLock lock1 = new ReentrantLock();
    // 带有boolean参数的构建方法可以设置是否为公平锁
    // true -- 公平    false -- 非公平
    ReentrantLock lock2 = new ReentrantLock(true);

实例方法

这里只介绍常用的实例方法

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo31 {

    public static void main(String[] args) {
        ReentrantLock lock1 = new ReentrantLock();
        // 第一种加锁方式
        // 获取不到锁就一直等着
        lock1.lock();

        // 第二种加锁方式
        // 到了设定时间获取不到锁,就放弃加锁
        try {
            lock1.tryLock(10, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 解锁  建议放到finally中
        // 因为有可能加完锁后执行不到解锁这个操作
        finally {
            lock1.unlock();
        }
    }

}

synchronized唤醒是通过Object类中的wait和notify来随机唤醒线程的;

而reentrantlock唤醒是通过Condition类来指定唤醒某个线程的。 


Semaphore类

这个类是把信号量封装起来了。信号量简单理解为可用资源的个数,本质上是一个计数器。

信号量最重要的操作就是申请资源(P操作)和释放资源(V操作)。

可以设想一下火车票,一趟火车的总票数就是可用资源。当有人买一张,那么资源就少一个(P);当有人退票,那么资源就会多一个(V)。如果总票数剩余为0,要么候补有人退票,要么放弃这趟火车,另寻它车。

如果资源只有1个的时候,它就变成了锁。拥有了锁,也就把资源变成0了(资源不能为负数),释放了锁,资源又变成1。

构造方法

        // 创建出有4个资源的信号量。获取资源是随机的
        Semaphore semaphore1 = new Semaphore(4);
        
        // 创建出10个资源的信号量。
        // true 尽可能是公平获取资源——先release的先获取(不是一定)
        // false 随机获取,与第一种构造方法一样
        Semaphore semaphore2 = new Semaphore(10, true);

实例方法

这里只介绍P操作和V操作的方法。这些方法都是原子性的,所以在多线程环境下可以使用。

                try {
                    // acquire() 表示获取资源
                    // 同时也要有异常  因为有可能没有资源,这样就得阻塞等待
                    // 只要有阻塞,一般都要有InterruptedException这个异常
                    semaphore1.acquire();
                    System.out.println("获取到一个资源!");
                    // release() 表示释放资源
                    semaphore1.release();
                    System.out.println("释放一个资源!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

使用Semaphore来自增一个变量,使用两个线程自增(保证线程安全)。

import java.util.concurrent.Semaphore;

class SemaphoreLock {
    private int n = 0;
    public Semaphore semaphore = new Semaphore(1);

    public void add (){
        n++;
    }

    public int getN() {
        return n;
    }

}

public class ThreadDemo33 {

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

        SemaphoreLock lock = new SemaphoreLock();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    // 获取到资源后才能自增
                    lock.semaphore.acquire();
                    lock.add();
                    // 自增一次后就释放资源
                    lock.semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    lock.semaphore.acquire();
                    lock.add();
                    lock.semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(lock.getN());
    }

}

  


CountDownLatch类

通过下面的代码来理解这个类具体有什么作用。

构造方法

        // 创建三个定数器
        // 不能为负数,否则会抛出异常
        CountDownLatch count = new CountDownLatch(3);

实例方法 

import java.util.concurrent.CountDownLatch;
//import java.util.concurrent.TimeUnit;

public class ThreadDemo34 {

    public static void main(String[] args) throws InterruptedException {
        // 创建三个定数器(相当于三个选手)
        CountDownLatch count = new CountDownLatch(3);

        // t线程是跑步比赛的裁判
        Thread t = new Thread(() -> {
            try {
                // 这个方法会让 t 线程阻塞
                // 除非计数器被消耗光了才会解除阻塞
                count.await();

                // 这是另外一个会让 t 线程阻塞的方法
                // 有了时间的控制,如果超过设定的时间还在阻塞,就会解除阻塞
                // count.await(100, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("跑步比赛已完成!");
        });
        // t1/2/3线程是三位选手
        Thread t1 = new Thread(() -> {
            // 调用该方法,计数器会减一
            count.countDown();
            System.out.println("t1比赛已完成!");
        });
        Thread t2 = new Thread(() -> {
            count.countDown();
            System.out.println("t2比赛已完成!");
        });
        Thread t3 = new Thread(() -> {
            count.countDown();
            System.out.println("t3比赛已完成!");
        });
        t.start();
        //t.join();
        t1.start();
        t2.start();
        t3.start();
    }

}

  


Callable接口

这个接口和Runnable接口很像,二者都是用来描述一个任务的。

不同之处:①该接口可以返回一个结果,不返回则会抛出异常。

                  ②该接口实现的类不能直接放到Thread类的构造方法,需要包装一下。

import java.util.concurrent.*;

public class ThreadDemo35 {

    public static void main(String[] args) {

        // 在t线程下计算1 + 1,只要结果

        // 由于这个任务需要返回结果,所用用Callable接口描述任务
        // 由于结果的整数,所以用Integer类作为泛型参数
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1 + 1;
            }
        };

        // 任务不能直接放到线程里工作
        // Thread t = new Thread(callable) 这样是不行的

        // 需要使用FutureTask包装以下
        // 这个类实现了Runnable接口,所以可以放到Thread的构造方法中
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();
        try {
            // get()方法是拿到结果
            System.out.println(task.get());
            // 结果不可能立刻出来(1+1还是可以立刻出来的),在没出来之前要进行阻塞等待
            // 阻塞就要有InterruptedException异常
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

}

  


死锁

死锁是什么

死锁是多个线程同时被阻塞,它们中一个或多个线程都在等待锁释放,导致了僵持的场面,程序陷入死循环的局面。

死锁产生的原因

1. 互斥使用。当资源被一个线程使用时,其他线程无法使用。

2. 不可抢占。资源请求者不能强已被获取的资源,只能等拥有者主动释放。

3. 资源保持。当资源拥有者再去请求其他资源时,原本持有的资源也不能放弃。

4. 循环等待。比如A等B释放,B等C释放,C等A释放资源,这样就形成了循环。

当这四个条件都成立的时候才会形成死锁。

解决死锁

上述任何一个条件不成立就可以解决死锁问题。

其中最容易破坏的是 循环等待。通过最常用的死锁阻止技术:锁排序

假设有N个线程获取M把锁,把M把锁进行编号(1、2......M)。当形成死锁时,按照标号由小到大的顺序依次获取锁,这样就可以解决循环等待。


有什么错误评论区指出。希望可以帮到你。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/191755.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vite+Vue3+TypeScript 搭建开发脚手架

Vite前端开发与构建工具 开发环境中&#xff0c;vite无需打包&#xff0c;可快速的冷启动 真正的按需编译&#xff0c;不需要等待整个应用编译完成 一个开发服务器&#xff0c;它基于原生ES模块 提供了丰富的内建功能&#xff0c;速度快模块热更新&#xff08;HMR&#xff0…

2022年10个最流行Blender插件

如果你从事平面设计、动画或 3D 建模&#xff0c;您可能听说过Blender&#xff0c;这是一款开源的一体化 3D 图形软件。Blender 配备了适用于各种领域的工具和功能&#xff0c;包括 3D 动画、计算机辅助设计、纹理编辑、特殊效果等。 Blender 的最新版本3.0于 2021 年 12 月上…

LeetCode题目笔记——1566. 重复至少 K 次且长度为 M 的模式

文章目录题目描述题目难度——简单方法一&#xff1a;模拟代码/C总结题目描述 给你一个正整数数组 arr&#xff0c;请你找出一个长度为 m 且在数组中至少重复 k 次的模式。 模式 是由一个或多个值组成的子数组&#xff08;连续的子序列&#xff09;&#xff0c;连续 重复多次…

【面试】vue组件style中scoped的作用是什么?什么是scoped穿透?

vue组件style中scoped的作用是什么&#xff1f; 在Vue文件中的style标签上有一个特殊的属性——scoped。scoped属性是 HTML5 中的新属性&#xff0c;是一个布尔属性&#xff0c;如果使用该属性&#xff0c;则css样式仅仅只能应用到当前的Vue组件&#xff0c;避免组件之间样式相…

EfficientNet v1 v2

EfficientNet v1 增加网络的深度depth能够得到更加丰富、复杂的特征并且能够很好的应用到其它任务中。但网络的深度过深会面临梯度消失&#xff0c;训练困难的问题。增加网络的width能够获得更高细粒度的特征并且也更容易训练&#xff0c;但对于width很大而深度较浅的网络往往很…

Base64编码

介绍 Base64 编码 Base64 是一种使用 64 个可打印字符来表示二进制数据的编码方式。 Base64 中的 64 个可打印字符包括&#xff1a;大小写字母 a - z、阿拉伯数字 0 - 9&#xff0c;这样共有 62 个字符&#xff0c;另外两个可打印字符在不同的系统中而不同。RFC 4648 标准中&…

程序员可以不看书,但不可以不知道这些网站!

程序员可以不看书&#xff0c;但是不能停止学习。如果你不喜欢看书&#xff0c;这些网站可以先收藏下来&#xff01; 花了两天整理出的程序员常看的网站&#xff0c;纯纯干货来了↓↓ 一、学习网站 ①菜鸟教程 这个网站有HTML、CSS、Javascript、PHP、C、Python等各种基础编…

关于python常用软件用法:Pycharm 常用功能

一.Pycharm的基本使用 1.在Pycharm下为你的Python项目配置Python解释器 &#xff08;1&#xff09;.Setting>Project Interpreter>源码资料电子书:点击此处跳转文末名片获取 二.在Pycharm下创建Python文件、Python模块 1.File>New>Python File2.File>New>P…

Mysql高级部分学习笔记(一)——底层及索引

0. 概述 我们的数据库一般都会并发执行多个事务&#xff0c;多个事务可能会并发的对相同的一批数据进行增删改查操作&#xff0c;可能 就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。 这些问题的本质都是数据库的多事务并发问题&#xff0c;为了解决多事务并发问题…

使用windows电脑SSH客户端链接Iphone手机的sshd服务(免越狱)

最近有需求使用电脑导出手机中特定app的文件。 当然可以直接连上数据线将手机中的文件下载下来。 为了能做到代码自动化导出&#xff0c;将手机作为一台电脑使用&#xff0c;将手机中的文件导出来。 关键问题是如何将手机作为电脑使用&#xff0c;这里有几个步骤(我使用的是…

关于接口测试自动化的总结与思考

关于接口测试自动化的总结与思考 目录&#xff1a;导读 什么是服务端? 什么是接口? 什么是接口测试? 为什么要做接口测试? 如何做接口测试&#xff1f; 什么是接口测试自动化? 为什么要做接口测试自动化? 接口测试自动化的规范 文档准备 明确接口测试自动化需…

CRI 与 ShimV2:一种 Kubernetes 集成容器运行时的新思路

作者|张磊 CRI 与 ShimV2&#xff1a;一种 Kubernetes 集成容器运行时的新思路-阿里云开发者社区 Kubernetes 项目目前的重点发展方向&#xff0c;是为开发者和使用者暴露更多的接口和可扩展机制&#xff0c;将更多的用户需求下放到社区来完成。其中&#xff0c;发展最为成熟…

再探前端低代码的“野路子”

之前码过很多低代码的文章&#xff0c;发现大家口中的低代码挺不一样的&#xff0c;这次心血来潮想探探低代码的野路子。 只需要拖拽操作或者几行基础代码&#xff0c;就能完成以往需要程序员才能搭建的各类应用系统&#xff0c;对效率要求较高的企业而言&#xff0c;是不是很有…

命令执行简介、命令执行函数

数据来源 命令执行简介 01 命令执行漏洞产生原因 02 命令执行漏洞的危害 03 远程代码执行 1&#xff09;远程代码执行- eval函数 2&#xff09;远程代码执行 - assert函数 3&#xff09;远程代码执行 - preg_replace函数 使用方法和一句话木马一样 示例&#xff1a; 在ph…

9 多分类问题

文章目录问题引入网络设计改进网络方法softmax层lossMINIST引入代码实现课程内容来源&#xff1a; 链接课程文本借鉴&#xff1a; 链接以及Birandaの突然发现的也挺好&#xff1a;链接 问题引入 前篇中&#xff0c;对糖尿病数据集的问题是一个二分类问题&#xff0c;但实际问…

vue-node解决 rollbackFailedOptional: verb npm-session fd23ceb3f5797b77进度条卡住的问题

一、文章引导 #mermaid-svg-qv5tmCFBaoUwQojc {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-qv5tmCFBaoUwQojc .error-icon{fill:#552222;}#mermaid-svg-qv5tmCFBaoUwQojc .error-text{fill:#552222;stroke:#55222…

RabbitMQ常见场景问题

RabbitMQ常见场景问题 文章目录RabbitMQ常见场景问题6种工作模式1.直连模式2.发布订阅模式3.Routing路由模式4.Topic通配符模式5.Header模式6.RPC消息不丢失消息发送到交换机失败1.配置文件开启发布确认2.配置回调函数3.测试4.如何处理失败消息RabbitMQ服务器故障持久化消息发送…

存量房贷利率,一种简单估算其自然年利率调整的方法。

1.摘要2022年过去了&#xff0c;总所周知LPR被多次下调&#xff0c;目前有存量房贷的朋友&#xff0c;如果&#xff08;普遍&#xff09;设置的是根据自然年LPR动态调整利率&#xff0c;到2023年2月应该注意到了比较明显的房贷金额变动。这里主要给出一种根据这个变动&#xff…

Plecs电力电子仿真专业教程-第一季 第一节 Plecs简介

Plecs电力电子仿真专业教程-第一季 第一章 Plecs是什么&#xff1f; 第一节 Plecs简介 Plecs是瑞士Plexim GmbH公司开发的系统级电力电子仿真软件PLECS。PLECS是一个用于电路和控制结合的多功能仿真软件&#xff0c;尤其适用于电力电子和传动系统。不管您是工业领域中的开发…

[架构之路-96]:《软件架构设计:程序员向架构师转型必备》-6-需求与用户用例User Case/Senario建模

第6章 需求与用户用例User Case建模备注&#xff1a;严格意义上讲&#xff0c;用户用例属于需求分析领域&#xff0c;不属于架构设计。用户用例是架构设计最重要的输入参考之一。User Case和User Senario是非常重要的描述需求的重要手段6.1 常用的4种用例技术6.1.1 用例图6.1.2…