1.锁策略
锁策略,和普通程序员没关系,和"实现锁"的人才有关系
这里提到的锁策略,和Java本身没关系,适用于所有和"锁"相关的情况
1.乐观锁 VS 悲观锁
悲观锁(预期锁冲突的概率很高):
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁(预期锁冲突的概率很低):
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
2.读写锁 VS 普通互斥锁
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
对于普通互斥锁来说: 只需要进行两个操作: 加锁和解锁.只要两个线程针对同一对象加锁,就会产生互斥~~
对于读写锁来说: 则需要进行加读锁 ,加写锁 以及解锁操作,如果只进行读操作,就加读锁加写锁: 如果进行修改操作,就加写锁
- 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
- 两个线程都要写一个数据, 有线程安全问题.
- 一个线程读另外一个线程写, 也有线程安全问题.
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock /unlock 方法进行加锁解锁.
- 读加锁和读加锁之间, 不互斥.
- 写加锁和写加锁之间, 互斥.
- 读加锁和写加锁之间, 互斥.
读写锁特别适合于 “频繁读, 不频繁写” 的场景中.
Synchronized 不是读写锁
3.重量级锁 VS 轻量级锁
重量级锁,就是做了更多的事.开销更大 (一般是悲观锁)
轻量级锁,做的事更少,开销更小 (一般是乐观锁)
在使用的锁中,如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的mutex接口,此时一般认为这是重量级锁)
如果锁是纯用户态实现的,此时一般认为这是轻量级锁
理解用户态 vs 内核态
想象去银行办业务.
在窗口外, 自己做, 这是用户态. 用户态的时间成本是比较可控的.
在窗口内, 工作人员做, 这是内核态.
内核态的时间成本是不太可控的.
如果办业务的时候反复和工作人员沟通, 还需要重新排队, 这时效率是很低的.
4.挂起等待锁 VS 自旋锁
挂起等待锁,往往就是通过内核的一些机制来实现的,往往较重 [重量级锁的一种典型实现]
自旋锁, 往往就是通过用户态代码来实现的,往往较轻 [轻量级锁的一种典型实现]
举个例子:
想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~
挂起等待锁:
陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意, 这个很长的时间间隔里, 女神可能已经换了好几个男票了).
自旋锁:
死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手,那么就能立刻抓住机会上位.
自旋锁:
- 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
- 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU 的).
synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的
5.公平锁 VS 非公平锁
公平锁:多个线程在等待一把锁的时候,谁是先来的,就能先获取到这个锁(遵守先来后到)
非公平锁:多个线程在等待一把锁的时候,每个等待的线程获取到锁的概率是均等的(不遵守先来后到)
假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.
当线程 A 释放锁的时候, 会发生啥呢?
公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.
操作系统来说本身线程之间的调度就是随机的(机会均等的),操作系统提供的mutex这个锁,就是属于非公平锁~
要想实现公平锁,反而要付出更多的代价(得整个队列,来把这些参与竞争的线程给排一排先来后到)
公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
synchronized 是非公平锁.
6.可重入锁 VS 不可重入锁
一个线程,针对一把锁,咔咔连续加锁两次,如果会死锁,就是不可重入锁,反之就是可重入锁
synchronized 是可重入锁
7.面试题总结
- 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.
- 介绍下读写锁?
读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在"频繁读, 不频繁写" 的场景中.
- 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.
- synchronized 是可重入锁么?
是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.
2.CAS
1.什么是CAS
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
CAS 伪代码
下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解CAS 的工作流程
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}
两种典型的不是 “原子性” 的代码
- check and set (if 判定然后设定值) [上面的 CAS 伪代码就是这种形式]
- read and update (i++) [之前我们讲线程安全的代码例子是这种形式]
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。
CAS 可以视为是一种乐观锁. (或者可以理解成 CAS 是乐观锁的一种实现方式)
2.CAS是怎么实现的
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
- java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
- Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
简而言之,是因为硬件予以了支持,软件层面才能做到。
3.CAS 有哪些应用
1. 实现原子类
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
伪代码实现:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}
假设两个线程同时调用 getAndIncrement
- 两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)
2) 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值.
注意:
- CAS 是直接读写内存的, 而不是操作寄存器.
- CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的
3) 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环.
在循环里重新读取 value 的值赋给 oldValue
- 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作
- 线程1 和 线程2 返回各自的 oldValue 的值即可
2.实现自旋锁
基于 CAS 实现更灵活的锁, 获取到更多的控制权.
自旋锁伪代码
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;}
}
4.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, 还是经历了一个变化过程.
这就好比, 我们买一个手机, 无法判定这个手机是刚出厂的新手机, 还是别人用旧了, 又翻新过的手机.
1.ABA 问题引来的 BUG
假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.
我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.
正常的过程
-
存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
-
线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
-
轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.
异常的过程
-
存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
-
线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
-
在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!
-
轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作
这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼!!
2.解决方案
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
-
CAS 操作在读取旧值的同时, 也要读取版本号.
-
真正修改的时候,
-
- 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
-
- 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
这就好比, 判定这个手机是否是翻新机, 那么就需要收集每个手机的数据, 第一次挂在电商网站上的手机记为版本1, 以后每次这个手机出现在电商网站上, 就把版本号进行递增. 这样如果买家不在意这是翻新机, 就买. 如果买家在意, 就可以直接略过
对比理解上面的转账例子
假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.
我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
为了解决 ABA 问题, 给余额搭配一个版本号, 初始设为 1.
存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100, 版本号为 1, 期望更新为 50.
线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.
轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读到的版本号为 1, 版本小于当前版本, 认为操作失败.
在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进行包装, 在内部就提供了上面描述的版本管理功能.
关于 AtomicStampedReference 的具体用法此处不再展开. 有需要的同学自行查找文档了解使用方法即可.
5.相关面试题
- 讲解下你自己理解的 CAS 机制
全称 Compare and swap, 即 “比较并交换”. 相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑.
- ABA问题怎么解决?
给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败.
3.Synchronized 原理
1.基本特点
结合上面的锁策略, 我们就可以总结出, Synchronized 具有以下特性(只考虑 JDK 1.8):
- 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
- 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
- 实现轻量级锁的时候大概率用到的自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
2.加锁工作过程
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁状态,会根据情况升级
-
当对象未被任何线程持有(对象头锁标记为101,线程id为0,是可偏向状态),通过CAS将对象头的线程id指向当前线程。
-
当有线程竞争,判断锁对象的头信息(对象 头锁标 记为101,线程id不为0),头信息的线程id与当前线程id不相等,锁对象已被其他线程占用,此时判断头信息的epoch与锁对象头信息指向klass的epoch是否相等,如果不相等,则说明被批量重偏向,cas修改线程id为当前线程。
-
如果上面第二步epoch相等,判断当前锁对象持有的线程是否需要继续持有锁,如果需要,则锁膨胀为重量级锁,否则偏向锁撤销,将对象头信息锁标识位改为001,然后加轻量级锁,对象头锁标识位改为00
-
偏向锁可偏向状态不可逆,偏向锁升级为轻量级锁后,当锁释放为无锁状态,下次再获取锁,直接是轻量级锁
-
偏向锁只有锁撤销(epoch未过期前提,线程A持有,但是已经不适用了,线程B来竞争,此时偏向锁撤销,加轻量级锁),没有锁释放,但是可以批量重偏向
6.无论是偏向锁还是轻量级锁,只要有资源竞争,都会膨胀为重量级锁,只是在膨胀为重量级锁之后,在线程挂起之前,会自旋来获取锁(挂起前的挣扎)
膨胀过程:
偏向锁->重量级锁
轻量级锁->重量级锁
3.锁消除
锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。
4.锁粗化
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
此处的粗细指的是锁的粒度 锁的粒度- > 加锁代码涉及到的范围,加锁代码涉及到的范围越大,粒度越粗,反之,则相反
如果锁的粒度比较细,多个线程之间的并发性就更高~ 如果锁的粒度比较粗,加锁解锁的开销就更小
5.相关面试题
- 什么是偏向锁?
偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态.
- synchronized 实现原理 是什么?
参考上面的 synchronized 原理 全部内容.
4.Callable 接口
1.Callable 的用法
Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.
创建线程计算 1 + 2 + 3 + … + 1000, 使用 Callable 版本
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo1 {public static void main(String[] args) throws ExecutionException, InterruptedException {//通过Callable 来描述一个这样的任务Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}return sum;}};//为了去让线程执行 callable 中的任务,光使用构造方法还不够,还需要一个辅助的类FutureTask<Integer> task = new FutureTask<>(callable);//创建线程,来完成这里的计算~~Thread t = new Thread(task);t.start();//如果线程的任务没有执行完呢,get就会堵塞//一直堵塞到,任务完成,结果算出来以后~~System.out.println(task.get());}
}
理解 Callable
Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作.
理解 FutureTask
想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 “小票” . 这个小票就是FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没.
2.相关面试题
介绍下 Callable 是什么
Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.
Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作.
5.ReentrantLock
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock 的用法:
- lock(): 加锁, 如果获取不到锁就死等.
- trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
- unlock(): 解锁
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try { // working
} finally { lock.unlock()
}
ReentrantLock 和 synchronized 的区别
- synchronized是一个关键字(背后的逻辑是 JVM 内部实现的,c++),ReentrantLock 是一个标准库中的类(背后的逻辑是Java代码写的)
- synchronized 不需要手动释放锁,出了代码块,锁自然释放,ReentrantLock 必须要手动释放锁,要谨防忘记释放
- synchronized 如果竞争锁的时候失败,就会阻塞等待~但是ReentrantLock除了阻塞等待,还有一手,tylock,失败了直接返回
- synchronized 是一个非公平锁,ReentrantLock 提供了非公平和公平锁两个版本,在构造方法中,通过参数来指定当前是公平锁还是非公平锁
- 基于synchronized 衍生出来的等待机制,是wait notify~~,功能是相对有限的.
基于ReentrantLock 衍生出来的等待机制,是 Condition 类(条件变量),功能要更丰富一些~
6.原子类
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
6.线程池
1.线程池的7种创建方法
import java.time.LocalDateTime;
import java.util.concurrent.*;
public class Demo {// 1. Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;public static void main1(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(2);threadPool.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}});threadPool.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}});}// 2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;public static void main2(String[] args) {ExecutorService service = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {int finalI = i;service.submit(() -> {System.out.println("i : " + finalI + "|线程名称:" + Thread.currentThread().getName());});}}// 3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;// 延迟执行public static void main3(String[] args) {//创建线程池ScheduledExecutorService service = Executors.newScheduledThreadPool(5);System.out.println("添加任务的时间:" + LocalDateTime.now());//执行定时任务(延迟3s执行)只执行一次service.schedule(new Runnable() {@Overridepublic void run() {System.out.println("执行子任务:" + LocalDateTime.now());}}, 3, TimeUnit.SECONDS);}// 固定频率执行public static void main4(String[] args) {//创建线程池ScheduledExecutorService service = Executors.newScheduledThreadPool(5);System.out.println("添加任务时间:" + LocalDateTime.now());//2s之后开始执行定时任务,定时任务每隔4s执行一次service.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {System.out.println("执行任务:" + LocalDateTime.now());}}, 2, 4, TimeUnit.SECONDS);}// 4. Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;public static void main5(String[] args) {ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();System.out.println("添加任务的时间:" + LocalDateTime.now());service.schedule(new Runnable() {@Overridepublic void run() {System.out.println("执行时间:" + LocalDateTime.now());}}, 2, TimeUnit.SECONDS);}// 5. Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;public static void main6(String[] args) {ExecutorService service = Executors.newSingleThreadScheduledExecutor();for (int i = 0; i < 10; i++) {service.submit(new Runnable() {@Overridepublic void run() {System.out.println("线程名:" + Thread.currentThread().getName());}});}}// 6. Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。public static void main7(String[] args) {ExecutorService service = Executors.newWorkStealingPool();for (int i = 0; i < 10; i++) {service.submit(() -> {System.out.println("线程名" + Thread.currentThread().getName());});while (!service.isTerminated()) {}}}// 7. ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置,后⾯会详细讲。static class MyOOMClass {// 1M 空间(M KB Byte)private byte[] bytes = new byte[1 * 1024 * 1024];}public static void main(String[] args) throws InterruptedException {Thread.sleep(15 * 1000);ExecutorService service = Executors.newCachedThreadPool();Object[] objects = new Object[15];for (int i = 0; i < 15; i++) {final int finalI = i;service.execute(new Runnable() {@Overridepublic void run() {try {Thread.sleep(finalI * 200);} catch (InterruptedException e) {e.printStackTrace();}MyOOMClass myOOMClass = new MyOOMClass();objects[finalI] = myOOMClass;System.out.println("任务:" + finalI);}});}}
}
2.线程池的工作流程
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.
corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
keepAliveTime: 临时工允许的空闲时间.
unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
workQueue: 传递任务的阻塞队列
threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
AbortPolicy(): 超过负荷, 直接抛出异常.
CallerRunsPolicy(): 调用者负责处理
DiscardOldestPolicy(): 丢弃队列中最老的任务.
DiscardPolicy(): 丢弃新来的任务.
7.信号量
semaphore
是一个更广义的锁.
锁是信号量里第一种特殊操作,叫做"二元信号量"
每次申请一个可用资源,计数器就-1(称为P操作)
每次释放一个可用资源,计数器就+1(称为V操作)
当信号量的计数已经是0了,再次进行P操作,就会阻塞等待~
锁就可以视为"二元信号量",可用资源就一个,计数器的取值,非0即1~
信号量就把锁推广到了一般情况,可用资源更多的时候,如何处理~~
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
public class Demo3 {public static void main(String[] args) throws InterruptedException {//初始化的值表示可用资源有四个Semaphore semaphore = new Semaphore(4);//申请资源,P操作semaphore.acquire();System.out.println("申请成功!!");semaphore.acquire();System.out.println("申请成功!!");semaphore.acquire();System.out.println("申请成功!!");semaphore.acquire();System.out.println("申请成功!!");semaphore.acquire();System.out.println("申请成功!!");//申请资源,V操作//semaphore.release();}
}
8.CountDownLatch
同时等待 N 个任务执行结束
countDown 给每个线程里面去调用,就表示到达终点了
await 是给等待线程去调用,当所有的任务都到达终点了,await就从阻塞中返回~表示任务完成
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
public class Demo4 {public static void main(String[] args) throws InterruptedException {//构造方法的参数表示有几个选手参赛CountDownLatch latch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {Thread t = new Thread(() -> {try {Thread.sleep(3000);System.out.println(Thread.currentThread().getName() + "到达终点!");latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}});t.start();}//裁判就要等待所有的线程到达//当这些线程没有执行完的时候,await就阻塞,所有的线程都执行完了,await才返回latch.await();System.out.println("比赛结束!");}
}
9.多线程下使用哈希表
HashMap 本身线程不安全~
1.HashTable[不推荐] -> 给关键方法加锁~
2.ConcurrentHashMap[推荐]
操作元素的时候,是针对这个元素所在的链表的头节点来加锁的
如果两个线程操作是针对两个不同的链表上的元素,没有线程安全问题,其实不必加锁~
由于Hash表中,链表的数目是非常多的,每个链表的长度是相对短的,因此就可以保证锁冲突的概率就非常小了
1.ConcurrentHashMap 减少了锁冲突,就让锁加到每个链表的头结点上(锁桶)
2.ConcurrentHashMap 只是针对写操作加锁,读操作没有加锁,而只是使用了volatile~
3.ConcurrentHashMap 中更广泛的使用CAS,进一步提高效率(比如维护size操作)
4.ConcurrentHashMap 针对扩容,进行了巧妙地化整为零
对于HashTable 来说,只要你这次put触发了扩容就一口气搬运完,会导致这次put非常卡顿
对于ConcurrentHashMap 来说,每次操作只搬运一点点,通过多次操作完成了整个搬运的过程
同时维护一个新的HashMap 和一个旧的,查找的时候既需要查旧的也要查新的,插入的时候只插入新的~~
知道搬运完毕再销毁~
10.死锁
1.什么是死锁
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
举个例子:
滑稽老哥和女神一起去饺子馆吃饺子. 吃饺子需要酱油和醋.
滑稽老哥抄起了酱油瓶, 女神抄起了醋瓶.
滑稽: 你先把醋瓶给我, 我用完了就把酱油瓶给你.
女神: 你先把酱油瓶给我, 我用完了就把醋瓶给你.
如果这俩人彼此之间互不相让, 就构成了死锁.
酱油和醋相当于是两把锁, 这两个人就是两个线程
为了进一步阐述死锁的形成, 很多资料上也会谈论到 “哲学家就餐问题”.
-
每个 哲 ♂ 家 只做两件事: 思考人生 或者 吃面条. 思考人生的时候就会放下筷子. 吃面条就会拿起左右两边的筷子(先拿起左边, 再拿起右边).
- 如果 哲 ♂ 家 发现筷子拿不起来了(被别人占用了), 就会阻塞等待
- [关键点在这] 假设同一时刻, 五个 哲 ♂ 家 同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会发现右手的筷子都被占用了. 由于 哲 ♂ 家 们互不相让, 这个时候就形成了 死锁
死锁是一种严重的 BUG!! 导致一个程序的线程 “卡死”, 无法正常工作
2.发生死锁的原因
分为下面4钟原因
互斥条件 | 共享资源 X y 只能被一个线程占有 |
---|---|
占用且等待 | 线程T1占用的共享资源X 他在等待共享Y的时候帮不释放自己的X |
不可抢占 | 其他线程不能去抢占t1线程占有的资源 |
循环等待 | 线程t1 等t2 占有的资源,线程t2 等t1占有的资源 循环等等 |
3.如何避免死锁
破坏循环等待
最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3…M).
N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待
两个线程对于加锁的顺序没有约定, 就容易产生环路等待.
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {// do something...}}}
};
t1.start();
Thread t2 = new Thread() {@Overridepublic void run() {synchronized (lock2) {synchronized (lock1) {// do something...}}}
};
t2.start();
约定好先获取 lock1, 再获取 lock2 , 就不会环路等待
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {// do something...}}}
};
t1.start();
Thread t2 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {// do something...}}}
};
t2.start();
查看全文
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.dgrt.cn/a/2176384.html
如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!
相关文章:
JavaEE初阶学习:多线程进阶(八股文面试重点)
1.锁策略
锁策略,和普通程序员没关系,和"实现锁"的人才有关系 这里提到的锁策略,和Java本身没关系,适用于所有和"锁"相关的情况
1.乐观锁 VS 悲观锁
悲观锁(预期锁冲突的概率很高):
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改……
Delphi清理释放本程序内存的代码
zt http://www.abcxd.com/delphi/abcxddelphi/delphiZY/SetProcessWorkingSetSize.html 明生注:请注意中间那段揭密文章,来按照自己的个人意愿来执行。不要贪一时的快感而影响稳定性 在WinXp,Win2K中应用此方法,不显示主窗体一直运行的程序……
Delphi中destroy, free, freeAndNil, release用法和区别
http://kudick.blog.163.com/blog/static/16660663200931311194482/2009-04-13 11:19:04| 分类: Delphi相关|举报|字号 订阅 1)destroy:虚方法释放内存,在Tobject中声明为virtual,通常是在其子类中override 它,且要加上inherited关键字,才能……
delphi数据库中ADOConnecting位置对查询结果的影响
代码不附,只做记录。 1、大部分数据库的读取部分会写在窗体的onShow事件里,此时数据库的更新会在窗体的每次Show中进行数据的连接,也就是说,如果一个ADOConnection1对应两个ADOQuery1和ADOQuery2,当通过ADOQuery1操作数……
elbow管道的fluent模拟练习
入口速度要小,过大会出现涡流和回流,与实际例子不相符
#include "udf.h"
DEFINE_PROFILE(x_velocity,thread,nv)
{ float x[3]; /* an array for the coordinates */ float y; face_t f; /* f is a face thread index */ begin_f_l……
Fluent使用UDF以及采用visual studio 开发编译udf的环境搭建
已有的操作步骤
在Visual Studio中直接编译Fluent的UDF的总结(串行)_硫酸亚铜_新浪博客
http://blog.sina.com.cn/s/blog_14d64daa10102xqwk.html
在Visual Studio中直接编译Fluent的UDF_硫酸亚铜_新浪博客
http://blog.sina.com.cn/s/blog_14d64daa10102xkg4.html
在Visu……
Fluent使用UDF以及采用visual studio 开发编译udf的环境搭建2
需要说明的是, fluent解释或者编译udf和在vs环境下调试udf是两件事情。
就我的理解,fluent解释或者编译会借助vs的部分功能,这也是在vs下搭建环境的目的(包括添加路径等等操作),然而根据前面的博客可以知道……
fluent里常见基础问题(转)
1 什么叫松弛因子?松弛因子对计算结果有什么样的影响?它对计算的收敛情况又有什么样的影响? 1、亚松驰(Under Relaxation):所谓亚松驰就是将本层次计算结果与上一层次结果的差值作适当缩减,……
错误收集:备忘MPI Application rank 0 exited before MPI_Finalize()nbsp
这种问题是fluent多线程问题,一旦出现这种问题整个fluent就死掉了,所有的数据都无法保存,问题很严重。
但是问题一般情况不是多线程本身的问题,而是因为线程里面运行的计算过程出现了问题。
1、MPI_Finalize() with status 2 原因之一:出现负体积
只要出现负体积,线程的计算……
小白的CFD之旅10 敲门实例-关于网格质量的描述。转自流沙大牛
以下为我需要的内容 Minimum Orthogonal Quality:最小正交质量Max Ortho Skew:最大正交歪斜率Maximum Aspect Ratio:最大长宽比 最小正交质量值范围为0~1,最差为0,最好为1,一般情况下要求网格最小正交质量……
一个python训练
美国:28:麻省理工学院,斯坦福大学,哈佛大学,加州理工学院,芝加哥大学,普林斯顿大学,宾夕法尼亚大学,耶鲁大学,康奈尔大学,哥伦比亚大学,密歇根大学安娜堡分校,约翰霍普金斯大学,西北大学,加州大学伯克利分校,纽约大学,加州大学洛杉矶分校,杜克大学,卡内基梅隆大学,加州大学圣地……
Mybatis03学习笔记
目录 使用注解开发
设置事务自动提交
mybatis运行原理
注解CRUD
lombok使用(偷懒神器,大神都不建议使用)
复杂查询环境(多对一)
复杂查询环境(一对多)
动态sql环境搭建
动态sql常用标签……
编程日记2023/4/16 14:55:50
设置或取得c# NumericUpDown 编辑框值的方法,(注意:不是Value值)
本人在C#开发中使用到了NumericUpDown控件,但是发现该控件不能直接控制显示值,经研究得到下面的解决办法
NumericUpDown由于是由多个控件组合而来的控件,其中包含一个类似TextBox的控件,若想取得或改变其中的值要使用如下方法
N……
编程日记2023/4/16 14:55:46
使用NPOI 技术 的SetColumnWidth 精确控制列宽不能成功的解决办法(C#)
在使用NPOI技术开发自动操作EXCEL软件时遇到不能精确设置列宽的问题。
如
ISheet sheet1 hssfworkbook.CreateSheet("Sheet1");
sheet1.SetColumnWidth(0, 50 * 256); // 在EXCEL文档中实际列宽为49.29
sheet1.SetColumnWidth(1, 100 * 256); // 在EXCEL文……
编程日记2023/4/16 14:55:46
Mysql 数据库zip版安装时basedir datadir 路径设置问题,避免转义符的影响
本人在开发Mysql数据库自动安装程序时遇到个很奇怪的问题,其中my.ini的basedir 的路径设置是下面这样的:
basedir d:\测试\test\mysql
但是在使用mysqld安装mysql服务时老是启动不了,报1067错误,后来查看window事件发现一个独特……
java stream sorted排序 考虑null值
项目里使用到排序, java里没有像C# 里的linq,只有stream,查找stream.sorted源码看到有个
Comparator.nullsLast
然后看了一下实现,果然是能够处理null值的排序,如:minPriceList.stream().sorted(Comparator.comparing(l -> l.g……
spring @EnableConfigurationProperties 实现原理
查看DataSourceAutoConfiguration源码,发现如下代码: Configuration ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) EnableConfigurationProperties(DataSourceProperties.class) Import({ DataSourcePoolMetadataProvidersCon……
postman请求https网址没有响应,但是用浏览器有响应,解决办法
遇到个问题:同一个get请求的url,postman请求https网址没有响应,但是用浏览器有响应
url是https开头的,查看错误描述里有一个SSL的选项: 然后根据描述关掉这个选项: 然后就没问题了,能正常请求及……
java @Inherited注解的作用
看到很多注解都被Inherited进行了修饰,但是这个Inherited有什么作用呢?
查看Inherited代码描述:
Indicates that an annotation type is automatically inherited. If an Inherited meta-annotation is present on an annotation type decl……
spring mvc的两种部署到Servlet容器的方式:web.xml 、WebApplicationInitializer 以及WebApplicationInitializer原理分析
方式一、编写web.xml
通常我们将一个spring mvc程序部署到Servlet容器(例如Tomcat)时,会使用该方式,示例如下:
<web-app><listener><listener-class>org.springframework.web.context.ContextLoad……
编程日记2023/4/16 14:55:43