多线程 (下) - 学习笔记1

简介: 多线程 (下) - 学习笔记

常见锁策略

乐观锁和悲观锁

悲观锁

总是假设最坏的情况, 每次去拿数据的时候都会认为会被别人修改, 因此会上锁, 防止数据在使用过程中被别的线程修改,

乐观锁

假设数据一般情况下不会产生并发冲突,因此在拿数据,操作数据的过程中不加锁, 而在数据进行提交更新的时候, 才会正式对数据是否产生并发冲突进行检测 .

(如何对数据是否产生并发冲突进行检测? 引入版本号)

如果发生并发冲突了, 则返回错误数据给用户,让用户决定如何去解决 .

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

读写锁

多线程之间, 读和读不会产生线程安全问题, 读和写, 写和写之间, 会产生线程安全问题.

如果两种情况下都使用同一种锁, 会产生极大的性能消耗 (得解决线程安全问题吧?), 读写锁就是解决这个问题, 应运而生.

读写锁(readers-writer lock), 把读操作和写操作区分对待

要求在执行加锁操作时, 需要额外表明读写意图

  • 读加锁和读加锁之间不互斥
  • 读加锁和写加锁之间互斥
  • 写加锁和写加锁之间互斥 .

synchronized 不是读写锁

重量级锁和轻量级锁

前置知识

锁的核心特性 “原子性”, 这样的机制追根溯源是 CPU 这样的硬件提供的

  • CPU 提供了 “原子操作指令”
  • OS 基于 CPU 的原子指令, 实现了 mutex 互斥锁
  • JVM 基于 OS 提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类

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

  • 大量的内核态用户切换
  • 很容易引发线程的较低

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

  • 少量的内核态用户切换
  • 不太容易引发线程调度

attention : 如果内核态和用户态进行频繁切换, 效率和资源的消耗就会非常大!

synchronized 是一个轻量级锁, 如果锁冲突比较严重, 就会变成重量级锁. (自动转换)

自旋锁 (Spin Lock)

自旋锁可以解决的情况 :

一般情况下, 抢锁失败后, 进入阻塞状态, 放弃 CPU , 然后进行一堆线程之间的锁竞争(时间可能会很漫长 …)

当抢锁失败后, 由于锁一般会被很快释放, 因此没必要放弃 CPU.(即一旦锁被其他线程释放, 能第一时间获取到锁)

可以通过一个伪代码来大概看出自旋锁如何实现的:

while( 抢锁(Lock) == 失败 ) {}  //一直死循环跑, 直到获取到锁

自旋锁 和 挂起等待锁 是相对的

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

  • 优点 : 没有放弃 CPU , 不涉及线程阻塞和调度 (因此轻量), 一旦锁被释放, 能第一时间获取到锁 .
  • 缺点 : 都 while 循环了, 那么如果其他线程长时间持有锁, 那么 CPU 消耗的资源会很多 (挂起等待是不消耗 CPU 的)

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

公平锁 VS 非公平锁

公平锁 : 遵循先来后到, 先来的一定会比后到的先执行 .

非公平锁 : 不遵循先来后到, 当锁被释放后, 阻塞队列中的所有线程, 一起竞争锁, 谁能抢到谁先用 .

attertion :

  • OS 内部的线程进度可以视为是随机的, 如果要实现公平锁, 就需要依赖额外的数据结构, 来记录线程间的先后顺序
  • 公平锁和非公平锁之间无好坏之分, 只有适用不适用

synchronized 是非公平锁

可重入锁(递归锁) VS 不可重入锁

可重入锁 : 允许同一个线程多次获取同一把锁 (如果一个线程内, 两次对同一个对象加锁, 如果中间没有锁释放, 可重入锁是可以进入的, 不可重入锁就不能进入, 会进行阻塞)

  • Java 中只要以 Reentrant 开头命名的锁, 都是可重入锁, 且 JDK 提供的所有线程的 Lock 实现类, 包括 synchronized 都是可重入锁 .
  • Linux 提供的 mutex 是不可重入锁

CAS

什么是 CAS?

CAS (Compare and swap) : “比较与交换”, 是一种实现并发算法时常用到的技术

相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑

CAS 算法 :

有三个操作数 : 内存中的原数据 (V) , 旧的预期值 (A), 修改后的值 (B)

  1. 比较 A 与 V 是否相等
  2. 如果 A == V , 那么将 B 写入 V
  3. 返回替换操作, 是否成功 (true or false)

伪代码如下 (真实的 CAS 是一个原子操作)

boolean CAS (address, expectValue, swapValue) {
  if(&address == expectValue) {
    &address = swapValue;
    return true;
  }
  return false;
}

attention : CAS 是乐观锁的一种实现方式, 因此该操作不会阻塞其他线程

CAS 是如何实现的?

硬件给予了支持, 软件方面才能对 CAS 进行了实现

  • JVM 中的 CAS : 通过 UnSafe 类来调用, OS 底层的CAS指令实现。

CAS 的应用

  • 实现原子类
  • 实现自旋锁

CAS 的 ABA 问题

什么是 ABA 问题?

假设有两个线程 t1 & t2, 有一个共享变量 num, 初始值为 A

当 t1 想使用 CAS 把 num 的值修改的时候, 会对 num 进行判定

  • 先读取 num 的值, 存储到 oldNum 中
  • 使用 CAS 判定当前 num 的值是否为 A , 如果为 A, 就修改

但是如果在判定过程中 ( if( num == oldNum )), t2 线程对 num 进行了多次操作, 而且其最后 num 的值最终变成了 原值, 即 A -> B -> … ->A, 那这个时候, t1 进行的判定, 是符合的 (非原子操作), 但是理论上, 他们并不相等 (用一个不太精巧的说法, 值相等, 址不相等)

解决方法

引入版本号, 在 CAS 进行数据比对的时候, 也比较版本号是否符合预期

// 伪代码
if(版本号(num) == 版本号(oldNum) && num == oldNum ) { 
  版本号(num)++;
  num = newNum;
}

synchronized 原理

基本特点 (JDK 1.8 版本下)

  1. 开始是乐观锁, 如果锁冲突频繁, 会自动转换成悲观锁
  2. 开始是轻量级锁实现, 如果锁被长时间持有, 会转换成重量级锁
  3. 实现轻量级锁的时候大概率用到自旋锁策略
  1. 不公平锁
  2. 可重入锁
  3. 不是读写锁

加锁工作过程

JVM 将 synchronized 锁分为 无锁, 偏向锁, 轻量级锁, 重量级锁 状态, 会根据情况, 依次升级

偏向锁

偏向锁不是真的加锁, 只是说给一个 “偏向锁标记”, 记录该锁属于哪个线程

如果后续没有其他线程来竞争该锁, 那就不用加锁 (避免加锁解锁的开销)

如果后续有其他线程来竞争该锁, 那么偏向锁就代表我已经被其他线程加锁了, 此时偏向锁取消, 进入轻量锁状态

轻量级锁

轻量锁通过 CAS 实现

  • 通过 CAS 检查并更新一块内存 (eg: NULL -> 该线程引用)
  • 如果更新成功, 就认为加锁成功
  • 如果更新失败, 则认为该锁被占用, 继续自旋式等待 (并不放弃 CPU , 才是轻量级, 放弃了, 就是重量级了)

重量级锁

重量级锁是指用到内核提供的 mutex

  • 执行加锁操作, 先进入内核态
  • 在内核态中判定当前锁是否已被占用
  • 如果该锁没有占用, 则加锁成功, 并切回用户态
  • 如果该锁被占用, 则线程进入该锁的等待队列, 挂起等待

多线程 (下) - 学习笔记2:https://developer.aliyun.com/article/1518510


目录
相关文章
|
1月前
|
网络协议 Linux C++
Linux C/C++ 开发(学习笔记十一 ):TCP服务器(并发网络网络编程 一请求一线程)
Linux C/C++ 开发(学习笔记十一 ):TCP服务器(并发网络网络编程 一请求一线程)
54 0
|
1月前
|
NoSQL 网络协议 关系型数据库
redis-学习笔记(redis 单线程模型)
redis-学习笔记(redis 单线程模型)
31 3
|
1月前
|
安全 Java 编译器
多线程 (下) - 学习笔记2
多线程 (下) - 学习笔记
28 1
|
1月前
|
设计模式 安全 NoSQL
多线程 (上) - 学习笔记2
多线程 (上) - 学习笔记
24 1
|
1月前
|
Java 数据库连接 程序员
【后台开发】TinyWebser学习笔记(2)线程池、数据库连接池
【后台开发】TinyWebser学习笔记(2)线程池、数据库连接池
38 4
多线程学习笔记(一)
创建线程有3种方式:继承Thread类、实现Runnable接口或Callable接口。继承Thread类时,重写run()方法并调用start()启动线程。实现Runnable接口时,实现run()方法,通过Thread的target创建线程对象并用start()启动。
|
1月前
|
Java API 调度
多线程 (上) - 学习笔记1
多线程 (上) - 学习笔记
24 0
|
1月前
|
Java C++
多线程学习笔记(二)
1. 子线程先执行:启动子线程后立即调用`join()`,主线程会等待子线程完成。 `suspend()`方法。 3. `synchronized` vs `Lock`:前者是关键字,后者是接口;前者可用在代码块和方法,后者在代码中显式;前者自动释放锁,后者需`finally`释放;前者无超时/中断控制,后者可设定;前者非公平,后者可公平/不公平,且支持读写锁。 4. `synchronized`底层实现:基于 Monitor 模型,JVM层面的锁定机制,通过 monitors 和 monitorenter/monitorexit 指令实现。
|
10月前
|
前端开发 定位技术
前端学习笔记202305学习笔记第二十三天-地图单线程配置
前端学习笔记202305学习笔记第二十三天-地图单线程配置
72 0
前端学习笔记202305学习笔记第二十三天-地图单线程配置
|
10月前
|
Java
java202303java学习笔记第三十九天自定义线程池详解1
java202303java学习笔记第三十九天自定义线程池详解1
30 0