文章目录
线程安全
说说自己对于synchronized关键字的了解?
- 实现线程同步
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,它解决的是多个线程之间访问资源的同步性问题。
synchronized可以实现线程同步,使用方式有两种同步代码块和同步方法。 - synchronized关键字底层原理属于JVM层面。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者实现细节不一样。
区别 | 方法同步 | 代码块同步 |
---|---|---|
实现细节 | ACC_SYNCHRONIZED修饰(acc_synchronized) | 使用monitorenter和monitorexit指令实现 |
如何在项目中使用synchronized的?
使用方式 | 说明 |
---|---|
修饰实例方法 | 给当前对象实例加锁,进入代码需要获得当前对象的锁 |
修饰静态方法 | 给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得class的锁 |
修饰代码块 | 指定加锁对象,对给定对象/类加锁 synchronized(this/object) 表示进入同步代码库前要获得给定对象的锁 synchronized(类.class) 表示进入代码块前要获得当前class的锁 |
- 示例 修饰实例方法
public synchronized void 方法名(){ 方法体; }
// 确保任意时刻只有一个线程对其进行操作。
- 示例 修饰实例静态方法
public static synchronized void 静态方法名(){ 方法体; }
- 示例 修饰代码块
synchronized(xxx.class || this){ 代码块;}
分析—使用xxx.class
使用类的class对象作为监视器,创建多个线程,同一方法调用同步代码块所在方法,使用的是同一把锁,且同一时间只有一个线程可以取调用increase()方法。
package com.javaface429.test1;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test1 {
public void increase(){
synchronized(Test1.class){
System.out.println(String.format("当前线程" + Thread.currentThread().getName()+"执行时间" + new Date()));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
Test1 te = new Test1();
for(int i = 0; i< 10; i++){
new Thread(()->te.increase()).start();
}
}
//输出结果是:
//当前线程Thread-0执行时间Fri Apr 29 10:30:32 CST 2022
//当前线程Thread-9执行时间Fri Apr 29 10:30:33 CST 2022
//当前线程Thread-8执行时间Fri Apr 29 10:30:34 CST 2022
//当前线程Thread-7执行时间Fri Apr 29 10:30:35 CST 2022
//当前线程Thread-6执行时间Fri Apr 29 10:30:36 CST 2022
//当前线程Thread-4执行时间Fri Apr 29 10:30:37 CST 2022
//当前线程Thread-5执行时间Fri Apr 29 10:30:38 CST 2022
//当前线程Thread-3执行时间Fri Apr 29 10:30:39 CST 2022
//当前线程Thread-2执行时间Fri Apr 29 10:30:40 CST 2022
//当前线程Thread-1执行时间Fri Apr 29 10:30:41 CST 2022
// 使用类的class对象作为监视器,创建多个线程,使用的是同一把锁,且同一时间只有一个线程可以取调用increase()方法。
}
使用类的class对象作为监视器,创建线程使用不同对象调用同步代码块所在的方法,使用的还是同一把锁,且每一个时刻只有一个线程被调用。
package com.javaface429.test1;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test2 {
public void increase(){
synchronized(Test2.class){
System.out.println("当前线程 " + Thread.currentThread().getName() + "当前时间 " + new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
for(int i = 0 ; i< 10;i++){
new Thread(()->{Test2 te = new Test2(); te.increase();}).start();
}
}
// 输出结果:
//当前线程 Thread-0当前时间 Fri Apr 29 10:58:42 CST 2022
//当前线程 Thread-9当前时间 Fri Apr 29 10:58:43 CST 2022
//当前线程 Thread-6当前时间 Fri Apr 29 10:58:44 CST 2022
//当前线程 Thread-8当前时间 Fri Apr 29 10:58:45 CST 2022
//当前线程 Thread-7当前时间 Fri Apr 29 10:58:46 CST 2022
//当前线程 Thread-4当前时间 Fri Apr 29 10:58:47 CST 2022
//当前线程 Thread-5当前时间 Fri Apr 29 10:58:48 CST 2022
//当前线程 Thread-3当前时间 Fri Apr 29 10:58:49 CST 2022
//当前线程 Thread-2当前时间 Fri Apr 29 10:58:50 CST 2022
//当前线程 Thread-1当前时间 Fri Apr 29 10:58:51 CST 2022
// 使用类的class对象作为监视器,创建线程使用不同对象调用同步代码块所在的方法,使用的还是同一把锁,且每一个时刻只有一个线程被调用。
}
分析—使用this
- 同步块使用this作为监视器,创建线程使用同一个对象调用同步块所在方法,使用的是同一个锁,每一个时刻只有一个线程可以访问同步块。
- 同步代码块使用this作为监视器,创建对象采用不同对象调用同步代码块所在的方法,不同对象使用的是不同的锁,所以同一时间会有多个线程。
package com.javaface429.test1;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test3 {
public void increase(){
synchronized(this){
System.out.println("当前线程 " + Thread.currentThread().getName() + "当前时间 " + new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 同一个对象调用同步代码块所在方法
// public static void main(String[] args){
// Test3 te = new Test3();
// for(int i = 0 ; i < 10 ; i++){
// new Thread(() -> te.increase()).start();
// }
// }
// 输出结果:
//当前线程 Thread-0当前时间 Fri Apr 29 11:06:03 CST 2022
//当前线程 Thread-8当前时间 Fri Apr 29 11:06:04 CST 2022
//当前线程 Thread-9当前时间 Fri Apr 29 11:06:05 CST 2022
//当前线程 Thread-7当前时间 Fri Apr 29 11:06:06 CST 2022
//当前线程 Thread-6当前时间 Fri Apr 29 11:06:07 CST 2022
//当前线程 Thread-5当前时间 Fri Apr 29 11:06:08 CST 2022
//当前线程 Thread-4当前时间 Fri Apr 29 11:06:09 CST 2022
//当前线程 Thread-3当前时间 Fri Apr 29 11:06:10 CST 2022
//当前线程 Thread-2当前时间 Fri Apr 29 11:06:11 CST 2022
//当前线程 Thread-1当前时间 Fri Apr 29 11:06:12 CST 2022
// 同步块使用this作为监视器,创建线程使用同一个对象调用同步块所在方法,使用的是同一个锁,每一个时刻只有一个线程可以访问同步块
// 不同对象调用同步代码块所在方法
public static void main(String[] args){
for(int i = 0 ; i < 10 ; i++){
new Thread(() -> {Test3 te = new Test3(); te.increase();}).start();
}
}
// 输出结果:
//当前线程 Thread-2当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-8当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-0当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-3当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-9当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-6当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-5当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-1当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-7当前时间 Fri Apr 29 11:08:51 CST 2022
//当前线程 Thread-4当前时间 Fri Apr 29 11:08:51 CST 2022
// 同步代码块使用this作为监视器,创建对象采用不同对象调用同步代码块所在的方法,不同对象使用的是不同的锁,所以同一时间会有多个线程。
}
扩展2022/4/29
- TimeUnit
TimeUnit是java.util.concurrent包下面的一个类,提供了可读性更好的线程暂停操作。
TimeUnit.SECONDS.sleep是对Thread.sleep方法进行了包装,只是多了时间单位转换和验证。并且TimeUnit枚举成员的方法提供了更好的可读性。枚举成员 = TimeUnit.xxx
枚举成员变量:nanoseconds(毫微秒)、microseconds(微秒)、milliseconds(毫秒)、seconds(秒)、minutes(分钟)、hours(小时)、days(天)。 - String静态方法:
方法名 | 说明 |
---|---|
static String format(String format,Object… args) | 使用指定的格式字符串和参数返回格式化的字符串 |
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
增加了 偏向锁、自旋锁、适应性自旋锁、轻量级锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁的状态依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁的状态可以升级但不可以降级,为的是提高获得锁和释放锁的效率。
偏向锁
- 概念:
偏向锁是针对于一个线程而言的, 线程获得锁之后就不会再有解锁等操作出现。 - 理解:
如果方法没有线程访问过,一个线程首次访问,会采用CAS机制,标记该方法为已经有人在执行;线程退出,不会修改方法的状态,会直接退出,默认认为除了本线程其余线程不会执行该方法。 - 使用场景:
适用于只有一个线程访问同步块的场景。 - 锁升级:
假如有两个线程来竞争该锁话, 那么偏向锁就失效了, 进而升级成轻量级锁。 - 扩展:
在JDK1.6中, 偏向锁的开关是默认开启的。
轻量级锁
- 概念:
线程进入方法不上锁,而是用一个变量做标记,采用CAS机制。方法有线程在使用,线程执行方法状态标记为已经有人在执行、线程退出方法状态标记为没有人在执行。 - 适用场景:
多个线程总是错开时间来获取锁的情况。 - 锁升级:
如果真的遇到竞争,就会把轻量级锁编程重量级锁。
重量级锁
- 概念:
获取不到锁就马上进入阻塞状态的锁,称之为重量级锁。 - 使用场景:
线程竞争激烈的场景。
自旋锁
- 概念:
线程拿不到锁,不会立马进入阻塞状态,而是等待一段时间(线程在做空循环,循环次数固定),如果循环一定的次数还拿不到锁,它才会进入阻塞的状态。 - 理解:
避免当前时刻无锁释放,之后0.0001秒有锁释放,造成线程运行态到阻塞态的转化造成的时间消耗(保存线程的执行状态、上下文等数据、用户态到内核态的转换)。
自适应自旋锁
- 概念:
线程拿不到锁,不会立马进入阻塞状态,且等待的时间是不固定的(线程空循环的次数不固定)。 - 理解:
等待锁的时间会根据最近获取这个锁的情况来调整循环次数(如果线程不久前拿到过这个锁或经常拿到这个锁,那么再次拿到的可能性更大,所以会等待久一点;如果线程从没拿到过这个锁或很少拿到,则等待的时间少一点)。
悲观锁
- 概念:
进入方法之前,就一定要先加锁,认为如果不加就会出事。 - 举例:
重量级锁、自旋锁、自适应自旋锁。
乐观锁
- 概念:
认为不加锁也可以,如果出现冲突再解决。 - 举例:
轻量级锁。
锁消除
- 概念:
虚拟机,即使编译器在运行时,如果检测到有些共享数据不可能存在竞争,那么就执行锁消除。这样可以减少请求锁的时间。
锁粗化
- 概念:
有些情况下,我们希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
谈谈synchronized和ReentrantLock的区别?
- 本质不同:
synchronized是关键字,是非公平锁;
ReentrantLock是类,且实现了Lock接口,是公平锁; - 加锁/解锁:
synchronized是隐式锁,没有明显的加锁和解锁标志,出了作用域自动解锁;
ReentrantLock是显式锁,有加锁和解锁标志,比如lock和unlock方法; - 锁的形式:
synchronized有代码块锁和方法锁;
ReentrantLock只有方法锁; - 底层实现:
synchronized依赖于JVM = java虚拟机;
ReentrantLock依赖于JDK = java程序开发工具;
同步块、同步方法、lock优先使用顺序:lock > 同步代码块 > 同步方法
synchronized和volatile的区别?
区别 | synchronized | volatile |
---|---|---|
本质 | 锁定当前变量,只有当前线程可以访问,其余线程都被阻塞 | 告诉JVM当前变量在寄存器(工作内存、本地内存)中的值是不确定的,需要从主存中读取 |
使用场景 | 变量、方法、类 | 变量 |
修饰变量的性质 | 可以保证变量修改的可见性和原子性 | 实现变量修改的可见性、不能保证原子性 |
线程是否阻塞 | 是 | 否 |
标记变量是否被编译器优化 | 是 | 否 |
扩展
- java内存模型
java虚拟机有自己的内存模型(Java Memory Model,JMM),可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各个平台下都能达到一致的访问效果。 - JVM的作用:
决定一个线程对共享变量的写入何时对另一个线程可见;
定义了线程与主内存之间的抽象关系:共享变量存储在主内存中,每一个线程都有自己的本地内存,其中存储了该线程使用到的主内存中共享变量的副本拷贝;
线程对变量的所有操作都必须在本地内存中进行,不能直接读写主内存的变量。 - 原子性:
表示一个时刻,只有一个线程可以执行一段代码,通过监视器对象(monitor object)保护,从而防止多个线程在更新共享状态的时候发生冲突。 - 可见性:
确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。
谈一下你对volatile关键字的理解?
理解 | 说明 |
---|---|
作用 | 告诉JVM虚拟机寄存器(本地内存)中的值是不确定的,需要从内存中获取 共享变量被申明为volatile变量,写该变量的时候,JVM会把该线程对应的本地内存中的变量值强制刷新到主内存中。写操作会导致其它线程中的缓存无效 |
适用场景 | 修饰变量 |
两个特性 | 实现变量的修改可见性,不能保证原子性 |
线程是否会阻塞 | 多个线程修改变量的时候,不会造成线程阻塞 |
修饰变量是否会被编译器优化 | 否 |
说下对信号量Semaphore的理解?
Semaphore是java.util.concurrent包下的一个类。Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数来指定。许可的值表示同一时刻允许多少个线程访问一个临界区。
Semaphore用于多线程互斥问题,允许多个线程访问一个临界区。比如数据库连接池、对象池等都要求同一时刻允许多个线程同时使用连接池。Synchronized和Lock来说只允许一个线程访问一个临界区。
常用方法:
方法名 | 说明 |
---|---|
availablePermits() | 返回此信号量中当前可用的许可数 |
acquire() | 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断 |
release() | 释放一个许可,将其返回给信号量 |
Semaphore(int permits) | 创建许可数是permits的信号量,默认是非公平锁 |
Semaphore(int permits,boolean fair) | 创建许可数是permits的信号量,fair是true表示公平锁,false表示非公平锁 |
锁
锁的分类?
1 悲观锁和乐观锁
悲观锁 = 当前线程去操作数据的时候,总认为别的线程会来修改数据,所以每次操作都会上锁;比如synchronized;
Synchronized可以通过修饰方法或代码块来实现任意时间只有一个线程对该部分访问;
修饰方法是加在方法的定义上的;修饰代码块是通过花括号实现的,具体的上锁对象是可以自定义Object对象实现、或者使用this;
乐观锁 = 当前线程去操作数据的时候,总认为别的线程不会来修改数据,更新数据的时候才会判断别的线程是否更改了数据,如果数据已经被修改了,则停止更新;比如版本号、CAS机制;
版本号 = 一般在数据表中会加上一个version字段,表示数据被修改的次数,被修改一次就version+1;
CAS机制 = 表示比较与互换,涉及三个变量,V表示要更新的变量、E表示变量的预期值、N表示更新后的值;
当V = E的时候,则变量可以更新为N;如果V != E,表示变量已经被更新过了,当前线程什么也不做,返回更新失败;
悲观锁适合写多的场景;乐观锁适合读多的场景;
2 公平锁和非公平锁
公平锁 = 多个线程通过申请锁的顺序去获得锁;比如ReentrantLock;
ReentrantLock是可重入的互斥锁,并且实现了Lock接口,可以通过lock方法和unlock方法实现上锁和解锁操作;
非公平锁 = 获得锁是随机的,不能保证每个申请锁的线程都获得锁;比如synchronized;
3 可重入锁和不可重入锁
可重入锁 = 也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不会产生死锁;比如synchronized和ReentrantLock;
不可重入锁 = 当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到而被阻塞;
4 自旋锁
一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁;
优点是:不会发生线程状态的切换,一直处于内核态,减少了线程切换上下文的消耗;缺点是:不断的循环判断会消耗CPU资源;
5 共享锁和独占锁
共享锁 = 也叫读锁,可以查看数据,但是不能更改数据;加锁之后,其他用户还可以对该数据进行读取,但是不能修改;
独占锁 = 也叫排它锁,该锁每一次只能被一个线程所占有,加锁后任何先要获取该锁的线程都会被阻塞。
说下对悲观锁和乐观锁的理解?
-
悲观锁:
概念 = 每次拿数据都认为会有别的线程去修改数据;
于是每次修改数据之间将数据锁住,然后再对数据读写,直到读写完成之后再释放锁,下一个线程才可以访问;
实现:synchronized和ReentrantLock;
场景:写多的时候; -
乐观锁:
概念 = 每次拿数据都认为没有别的线程去修改数据;
于是每次修改数据之前不会上锁,当要修改数据的时候才会判断数据是否有修改过;
实现 = 版本号机制、CAS算法;
场景 = 读多的时候;
乐观锁常见的两种实现方式:CAS机制和版本号机制
版本号机制 和 CAS机制
版本号机制
- 概念:
一般会在数据表中加上一个版本号version字段,表示数据被修改的次数,当数据被修改时,version+1.
线程A更新数据的之前,会读取当前的版本号oldCode,修改数据之后会读取新的版本号newCode,两个版本号比较,相同则可以更新,不同则重试更新操作。
CAS机制
- 概念:
CAS = Compare and swap(比较和互换),在没有锁的情况下实现多线程之间的变量同步,也就是同步非阻塞式IO即NIO。 - 具体的实现:
三个操作数:V表示需要更新的变量,E表示预期值,N表示新值。
理解:只有当V = E 的时候,变量V的值才会被更新为N;如果V != E,说明V的值已经被更新了,当前线程不执行更新操作,返回更新失败。内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试);
CAS操作的原子性是通过CPU单条指令完成而保障的,JDK中是通过Unsafe类中的API完成的,底层是借助C/C++调用CPU指令来实现的。
乐观锁的缺点有哪些?
缺点:①只能保证一个共享变量的原子操作;②长时间自旋可能导致开销大;③ABA问题。
- 只能保证一个共享变量的原子操作
CAS只对单个共享变量有效,当操作涉及多个共享变量的时候,CAS无效。
JDK1.5之后,提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放到一个对象里面来进行CAS操作。 - 更新失败会进行不断自旋更新。
预期获取的值是E,现在获取的是V,当E和V不相等的时候会一直不执行,并循环等待,长时间会给CPU带来额外开销。 - ABA问题。
预期获取的是E,现在获取的是v,两者相等都是值A,这样也不能说明该值没有被改过,可能是值经历了A→B→A的过程。JDK1.5之后的AtomicStampedReference类提供了此种能力,方法compareAndSet方法会检查当前引用是否等于预期引用,当前标志是否等于预期标志,全部相等,才会以原子方式将该引用和该标志的值设置为给定的更新至。
ReentrantLock实现可重入锁的原因?
可重入锁 = 即一个线程可以多次(重复)进入同类型的锁而不出现异常,也就是相同类型的锁可以嵌套;
CAS(compare and swape) 和 synchronized 的使用场景?
CAS适用于竞争较少的情况下(写操作较少);synchronized适用于竞争较多的情况下(写操作较多)。
资源竞争较少的情况:使用synchronized同步锁,进行线程阻塞和唤醒切换、用户态内核态间的状态切换操作 会额外浪费cpu资源;CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋率低,因此会有更高的性能。
资源竞争较多的情况:CAS自旋的几率比较大,会导致cpu资源的浪费,效率低于synchronized。
扩展
操作系统对程序的执行权限进行分级,分别是用户态和内核态。
用户态相比内核态有较低的执行权限。
内核态:cpu可以访问计算机所有硬软件资源
用户态:cpu权限受限,只能访问到自己内存中的数据,无法访问到其他资源
说下对ReentrantReadWriteLock的理解?
- 出现的原因:
ReentrantLock在并发的情况下只允许单个线程执行受保护的代码,但大部分应用中都是读多写少,使用ReentrantLock实现这种对共享数据的并发访问控制,将严重影响整体的性能。 - 概念:
提供了读取锁(ReadLock)(共享锁)和写入锁(WriteLock)(排它锁),前者可以实现并发访问下的多读,后者实现每次只允许一个写操作。
也就是说:ReentrantReadWriteLock允许多个读线程同时访问,但是不允许写线程和读线程、写线程和写线程同时操作。 - 两个锁的开启与关闭:
说明 | 读取锁(ReadLock) | 写入锁(WriteLock) |
---|---|---|
上锁 | ReentrantReadWriteLock对象.readLock().lock() | ReentrantReadWriteLock对象.writeLock().lock() |
释放锁 | ReentrantReadWriteLock对象.readLock().unlock() | ReentrantReadWriteLock对象.writeLock().unlock() |
- 案例1:多个线程同时进行读操作
ReentrantLock对象允许多个读操作同时进行。
package offer2.Test424;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteDemo {
// 创建ReentrantReadWriteLock锁
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// static静态方法
public static void main(String[] args) {
// 创建本类对象
ReentrantReadWriteDemo reentrantReadWriteLockTest = new ReentrantReadWriteDemo();
// 采用引用对象的实例方法 创建线程,参数是Runnable接口
new Thread(reentrantReadWriteLockTest::read,"ThreadA").start();
new Thread(reentrantReadWriteLockTest::read,"ThreadB").start();
// 创建了两个线程。
}
// 创建私有read方法
private void read() {
// 这个lock是ReentrantReadWriteLock对象 开启读线程锁
lock.readLock().lock();
// Thread.currentThread().getName() 获得当前正在执行线程的名字;System.currentTimeMillis() 获得当前时间的毫秒数,距离1970年的毫秒数。
System.out.println("获得读锁" + Thread.currentThread().getName() + "时间" + System.currentTimeMillis());
try {
// 让线程暂停5000毫秒、
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读线程锁
lock.readLock().unlock();
}
}
// 获得读锁ThreadA时间1650807624704
// 获得读锁ThreadB时间1650807624704
// 多个线程都执行读操作,获得锁的时间相同,说明readLock.lock允许多个线程同时执行Lock()后面的代码。
}
- 案例2:多个线程同时进行写操作
ReentrantLock对象不允许多个写操作同时进行。
package offer2.Test424;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteDemo2 {
private ReentrantReadWriteLock lockRW = new ReentrantReadWriteLock();
public static void main(String[] args){
ReentrantReadWriteDemo2 ReentrantReadWriteTest = new ReentrantReadWriteDemo2();
new Thread(ReentrantReadWriteTest::write,"ThreadA").start();
new Thread(ReentrantReadWriteTest::write,"ThreadB").start();
}
private void write(){
lockRW.writeLock().lock();
System.out.println("获得锁的是:" + Thread.currentThread().getName() + "时间:" + System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lockRW.writeLock().unlock();
}
}
//获得锁的是:ThreadA时间:1650808532250
//获得锁的是:ThreadB时间:1650808537255
// 两者获得锁的时间差大约是5秒,证明多个线程之间是互斥的。
}
- 案例3:多个线程同时进行读写操作
ReentrantLock对象不允许多个读写操作同时进行。
package offer2.Test424;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteDemo3 {
// 创建ReentrantReadWriteLock锁
private ReentrantReadWriteLock lockRW = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReentrantReadWriteDemo3 test = new ReentrantReadWriteDemo3();
new Thread(test::read,"ThreadA").start();
new Thread(test::write,"ThreadB").start();
}
private void read(){
lockRW.readLock().lock();
System.out.println("获得锁的是:" + Thread.currentThread().getName() + "时间:" + System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lockRW.readLock().unlock();
}
}
private void write() {
lockRW.writeLock().lock();
System.out.println("获得锁的是:" + Thread.currentThread().getName() + "时间:" + System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lockRW.writeLock().unlock();
}
}
// 获得锁的是:ThreadA时间:1650809066120
// 获得锁的是:ThreadB时间:1650809071125
// 两个时间差大概5秒,说明读写线程是互斥的。
}
- 读写锁的特性:
特性 | 说明 |
---|---|
锁不能升级、可以降级 | 读锁不能升级为写锁 写锁里面可以加读锁,这就是锁的降级 |
是否支持interrupt | 读锁、写锁都支持获取中断 且和ReentrantLock一致 |
是否支持Condition(条件变量) | writeLock支持Condition并且与ReentrantLock语义一致 ReadLock则不能使用Condition,否则抛出UnsupportedOperationException 异常 |
构造方法 | 默认构造方法为非公平的,开发者可以指定fair为true设置为公平模式 |
是否支持重入 | 该锁支持重进入,案例:读线程获得锁之后,还可以再获得读锁;写线程获得锁之后,还可以再获取写锁或者读锁 |
扩展
- 条件变量
java中的条件变量都实现了java.util.concurrent.locks.Condition接口,条件变量的实例化是通过一个Lock对象调用newCondition()方法来获取的,这就是实现了条件和一个锁对象的绑定。条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全。
原子类
简答说下对java中的原子类的理解?
Atomic指的是一个操作是不可中断的。即使多个线程一起执行,一个操作一旦开始,就不会被其他线程干扰。
原子类就是具有原子操作特征的类。
原子类都放在java.util.concurrent.atomic下,可以将原子类分成四类:
类型 | 原子类 |
---|---|
更新基本类型 | AtomicInteger、AtomicLong、AtomicBoolean |
更新数组类型 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
更新引用类型 | AtomicReference、AtomicStampedReference、AtomicMarkableReference |
更新字段类型 | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater |
更新基本类型
- 介绍:
AtomicBoolean、AtomicLong、AtomicInteger—原子更新布尔类型、长整型、整型; - 常用方法:
方法名 | 介绍 |
---|---|
int addAndGet(int delta) | 以原子的方式将输入的数值与实例中的值相加,并返回结果 |
boolean compareAndSet(int expect,int update) | 如果当前值等于预期值,则以原子方式将当前值设置为更新的值 |
ing getAndIncrement() | 以原子的方式将当前值加1;返回值是自增前的值 |
void lazySet(int newValue) | 最终会设置成newValue,是哟功能lazySet设置值后,可能会导致其他线程在之后的一小段时间内还是可以读到旧的值 |
int getAndSet(int newValue) | 以原子的方式这是为newValue。并返回旧值 |
- getAndIncrement()如何实现原子操作
获取到旧值,然后把要加的数传递过去,调用getAndAddInt() 进行原子更新操作。
实际最核心的还是compareAndSwapInt()方法,使用CAS进行更新。
另外AtomicBoolean是把Boolean转成整型,再使用compareAndSwapInt方法进行操作的。
原子更新数组
- 介绍:
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray—原子更新整型/长整型/引用类型数组里的元素。 - 常用方法:
方法名 | 说明 |
---|---|
get(int index) | 获取索引为index的元素值 |
compareAndSet(int i,int expect,int update) | 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值 |
- 案例:
static int[] value =new int[]{1,2};
static AtomicIntegerArray ai =new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.compareAndSet(0,1,5);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
// 输出值是:5 1
- 理解:
关于数组value修改了为什么通过AtomicIntegerArray读取和数组读取值不一样?
因为数组value通过构造函数方法传递进去,AtomicIntegerArray会将当前数组复制一份,当AtomicIntegerArray对内部数组元素进行修改的时,不会影响传入的数组。
原子更新引用类型
介绍:
AtomicReference、AtomicReferenceFieldUpdate、AtomicMarkableReferce:原子更新引用类型、引用类型的字段、带有标记位的引用类型
常用方法:
方法名 | 说明 |
---|---|
void set(V newValue) | 将值设置为newValue |
boolean compareAndSet(V expectedValue,V newValue) | 如果当前值 等于expectedValue,将该内容设置为newValue |
案例:
// 将User对象放入AtomicReference中
public static AtomicReference<User> ai = new AtomicReference<User>();
public static void main(String[] args) {
User u1 = new User("pangHu", 18);
ai.set(u1);
User u2 = new User("piKaQiu", 15);
ai.compareAndSet(u1, u2);
System.out.println(ai.get().getAge() + ai.get().getName());
}
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
- 理解:
对象放入AtomicReference中,调用compareAndSet进行原子操作,与AtomicInteger类似,只是调用的是compareAndSwapObject方法。
原子更新字段类
-
介绍:
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedFieldUpdater—原子更新整型/长整型/带版本号引用类型的更新器。 -
常见方法:
方法 | 介绍 |
---|---|
static < U > AtomicIntegerFieldUpdate newUpdater(Class< U > tcalss,String fieldName) | 创建并返回具有给定字段的对象的更新程序 参数1:对象的所属类的Class对象;参数2:需要更新的字段名称 |
- 案例:
//创建原子更新器,并设置需要更新的对象类和对象的属性
private static AtomicIntegerFieldUpdater<User> ai = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
User u1 = new User("pangHu", 18);
//原子更新年龄,+1
System.out.println(ai.getAndIncrement(u1));
System.out.println(u1.getAge());
}
static class User {
private String name;
public volatile int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
- 理解:
原子更新字段有两步:
①原子更新字段类都是抽象类,每次使用都必须使用静态方法newUpdate创建更新器,并且设置想要更新的类和属性
private static AtomicIntegerFieldUpdater<User> ai = AtomicIntegerFieldUpdater.newUpdater(对象所属类的Class对象,属性);
②更新的字段必须使用public volatile修饰
Static class User{
public volatile 要修改的属性;
}
atomic的原理是什么?
Atomic包中类具有的特性:当多个线程同时对单个变量(基本数据类型或引用数据类型)进行操作时,具有排他性。即当多个线程对该变量的值进行更新的时候,仅有一个线程成功,而未成功的线程会像自旋锁一样,继续尝试,一直等到执行成功。
Atomic实现排他性的原理是:CAS(Compare And Swap)乐观锁机制 + 自旋锁。
CAS机制有三个参数,V表示被修改元素的当前值、E表示预期值、N表示更新值。
当V 和 E 相等的时候,将V的值更改为N;当V 和 E 不相等的时候,说明V的值被更改过了,所以线程不执行操作,返回更新失败,且自旋等待。
扩展
- Atomic类保证原子性的原因:
以AtomicInteger为例。在AtomicInteger中有一个被volatile修饰的value变量,是整型。调用getAndIncrement()时,AtomicInteger会通过Unsafe类的getAndAddInt方法对变量value进行一次CAS操作。由于CAS操作具有原子性,则AtomicInteger就保证了操作的线程安全。
AQS
说下对同步器AQS的理解?
AQS = AbstractQueuedSynchronized(抽象队列同步器),该类在java.util.concurrent.Locks包下,是用来构建锁和同步器的框架。
使用AQS可以简单高效地构建出应用广泛的大量同步器,比如:ReentrantLock、Semaphore、ReentrantReadWriteLock、SychronousQueue、FutureTast等都是基于AQS。
同步器中一般包括两种操作:不同的类中对于两种操作的名字定义形式是不同的。
操作名字 | 说明 |
---|---|
acquire | 阻塞调用的线程,直到同步状态允许其继续执行 |
release | 通过某种方式改变同步状态,使得一个或多个被acquire阻塞的线程继续执行 |
内部实现的关键是:先进先出的队列、state状态。核心变量state,是int类型,代表了加锁的状态。
ReentrantReadWriteLock的lock()方法尝试加锁,加锁的过程就是CAS操作将state值从0变到1的过程。
拥有两种线程模式:独占模式 和 共享模式 。
要想使用AQS,就是通过继承AbstractQueuedSynchronizer类实现。
AQS的原理是什么?
AQS的原理是:
它首先是用来构造锁和同步器的框架。通过内置的FIFO队列完成线程获取资源的排队工作,并通过一个int类型的变量表示持有锁的状态。如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁的分配。
具体实现:
通过CLH队列的变体实现,将暂时获取不到锁的线程加入到队列中,该队列是AQS的抽象表示。将请求资源的线程封装成队列的节点(Node),同时会阻塞当前线程。
通过CAS自旋以及LockSupport.park()的方式维护state变量的状态,同时达到同步的效果。当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。共享变量state使用volatile修饰,保证线程可见性。
图示:
CLH队列:
Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列,即不存在队列实例,仅存在节点之间的关联关系。
该队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)。
图示:
AQS对资源的共享模式有哪些?
AQS提供了两种工作模式:独占(exclusive)模式和共享(shared)模式。
它的所有子类中,不能同时实现独占功能API和共享功能API,即便是ReentrantReadWriteLock也是通过两个不同的内部类:读锁和写锁,分别实现两套API。
共享模式:同一时间有多个线程可以拿到锁协同工作,锁的状态大于或等于0。常用类包括:CountDownLatch、ReadWriteLock、Semaphore、CyclicBarrier。
独占模式:同一时间只有一个线程能拿到锁执行,锁的状态只有0和1两种情况。常用类包括:ReentrantLock。
AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗?
抽象队列同步器的设计是基于模板方法模式的,模板方法模式经典的应用就是:使用者继承AbstractQueuedSynchronizer类并重写指定的方法。重写的方法无非就是 对于共享资源state的获取和释放。
模板方法模式是基于继承的,主要是为了不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。
AQS使用了模板方法模式,自定义同步器时需要重写模板方法如下:
方法名 | 说明 |
---|---|
isHeldExclusive() | 该线程是否独占资源。只有用到condition才需要去实现它 |
tryAcquire(int) | 独占方式。获取资源,成功返回true;失败返回false |
tryRelease(int) | 独占方式。释放资源,成功返回true;失败返回false |
tryAcquireShared(int) | 共享方式。尝试获取资源。负数表示失败;0表示成功,且没有资源剩余;正数表示成功,且有资源剩余 |
tryReleaseshared(int) | 共享方式。尝试释放资源。成功则返回true;失败则返回false |
模板方法说明:
默认情况下,每个方法都会抛出UnsupportedOperationException。
这些方法实现必须是内部线程安全的;
AQS类中的其他方法都是final,所以无法被其他类使用,只有上述五个方法可以被其他类使用。
并发容器
CountDownLatch 和 CyclicBarrier 有什么区别?
区别:
区别 | CountDownLatch | CyclicBarrier |
---|---|---|
概念不同 | 一个或多个线程,等待其他多个线程完成某个事情之后才能执行 | 多个线程相互等待,直到到达同一个同步点,再继续一起执行 |
执行次数不同 | 只能使用一次 | 可以使用多次,使用reset()方法可以重置 |
是否会阻塞主线程 | 是 | 否 |
是否会产生线程无限等待的情况 | 是,完成某个事情的多个线程中某个线程中断,则其余线程会一直等待下去 | 否,某个线程中断CyclicBarrier会抛出异常,避免所有线程无限等待 |
CountDownLatch 和 CyclicBarrier 都在java.util.concurrent包下。
CountDownLatch
- 概念:
多线程控制工具类,又被称之为“倒计时器”,允许一个或多个线程一直等待,直到其他线程的操作执行完毕。 - 实现:
通过一个计数器来实现,计数器的初始值为线程的数量。每当一个线程完成自己的任务之后,计数器的值会减少1;当计数器值为0,表示所有线程都执行完任务了,则闭锁上等待的线程就可以恢复执行。 - 特点:
只能一次性使用;
主程序阻塞;
某个线程中断将永远到不了屏蔽点,所有线程都会一直等待。 - 常用方法:
方法名 | 说明 |
---|---|
CountDownLatch(int count) | 构造一个以给定计数初始化的countDownLatch |
void countDown() | 减少锁存器的技术,如果计数达到零,释放所有等待的线程 |
void await() | 导致当前线程等到锁存器向下计数为零,除非线程为interrupted |
boolean awati(long timeout,TimeUnit unit) | 使当前线程等待直到锁存器计数到零为止,除非线程为interrupted,否则指定的等待时间已过 |
思路:
《创建一个类》
1 创建定长线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
2 创建倒计时器对象,前后两者的参数值是相等的
CountDownLatch countDownLatch = new CountDownLatch(3);
3 线程池执行,再减少倒计时器对象的数值 该内容写在一个private方法count中
threadPool.execute(() ->
{
// 线程具体的执行
countDownLatch.countdown();
});
4 执行重写的run方法,该类继承自Runnable接口
this.run();
5 启动有序关闭,之前提交的线程有序执行,不再接收新的线程。
threadPool.shutdown();
6 由于创建的类继承自Runnable接口,需要重写run方法
@Overrride
public void run(){
try {
// 使得当前线程等到锁存器向下计数为零,除非线程为interrupted
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 后续执行的任务。
}
7 main方法
// 记录main线程执行开始时间
long now = System.currentTimeMillis();
// 创建该类对象
类名 类对象 = new 类名();
// 调用count方法 实现线程的创建
类对象.count();
// 记录main线程执行结束时间
long end = System.currentTimeMills();
// 通过最后输出时间差来判断countDownLatch是否阻塞主线程
- 案例:
package com.javaface430.test1;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchTest implements Runnable{
//创建初始化3个线程的线程池 newFixedThreadPool创建一个定长的线程池,可控制线程最大并发数,超出的线程会在队列中等待.
private ExecutorService threadPool = Executors.newFixedThreadPool(3);
//保存每个学生的平均成绩
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 创建倒计时器对象
private CountDownLatch countDownLatch = new CountDownLatch(3);
private void count() {
for (int i = 0; i < 3; i++) {
// Runnable命令使用Lambda表达式 execute在某个时间执行给定的命令
threadPool.execute(() -> {
//计算每个学生的平均成绩,代码略()假设为60~100的随机数
int score = (int) (Math.random() * 40 + 60);
try {
// 由于Math.round四舍五入,Math.random是随机数,0-1之间,则线程让出CPU 0秒 或 1秒
Thread.sleep(Math.round(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
// map集合键 = 线程名字,map集合值 = 分数
map.put(Thread.currentThread().getName(), score);
System.out.println(Thread.currentThread().getName() + "同学的平均成绩为" + score);
// 减少锁存器的计数值
countDownLatch.countDown();
});
}
// 调用本类中的run方法
this.run();
// 启动有序关闭,先前提交的任务有序执行,不再接收新的任务。
threadPool.shutdown();
}
@Override
public void run() {
try {
// 使得当前线程等到锁存器向下计数为零,除非线程为interrupted
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 计算平均分,result 计算总成绩
int result = 0;
Set<String> set = map.keySet();//获取key键的集合
for (String s : set) {//增强for循环实现总成绩计算
result += map.get(s);
}
System.out.println("三人平均成绩为:" + (result / 3) + "分");
}
// main方法,
public static void main(String[] args) throws InterruptedException {
// 返回当前时间 = 距离1970年的毫秒数。
long now = System.currentTimeMillis();
// 创建本类CountDownLatch 对象
CountDownLatchTest cb = new CountDownLatchTest();
// 调用count方法
// cb.count();
// 线程执行暂停100毫秒。
Thread.sleep(100);
// 获取执行结束的时间
long end = System.currentTimeMillis();
// 计算代码执行的总时间
System.out.println(end - now);
}
// 输出结果:
//pool-1-thread-1同学的平均成绩为98
//pool-1-thread-3同学的平均成绩为98
//pool-1-thread-2同学的平均成绩为72
//三人平均成绩为:89分
//1172
// 结果1172,说明会阻塞主线程。
// 2022/4/30 xhj理解:不执行count方法,main方法执行118毫秒,count方法中会创建创建三个线程,也就是说执行该代码有四个线程,主线程main,count中还有三个线程。如果不阻塞主线程main的话,输出的时间应该大约是100毫秒左右;
}
结果分析:
结果1172,说明会阻塞主线程。
2022/4/30 xhj理解:不执行count方法,main方法执行118毫秒,count方法中会创建创建三个线程,也就是说执行该代码有四个线程,主线程main,count中还有三个线程。
如果不阻塞主线程main的话,输出的时间应该大约是100毫秒左右;且应该先输出主线程的执行时间。
CyclicBarrier
-
概念:
是一种同步机制,允许一组线程相互等待,等到所有线程都到达一个屏障点才退出await方法。没有直接实现AQS而是借助ReentrantLock来实现的同步机制。
屏障之所以可以循环,当所有线程释放彼此之后,这个屏障可以重新使用(通过reset方法)。 -
特点:
可循环使用;
走向屏障点await采用的是Acquire方法,该方法会阻塞的(不清楚表达式什么意思?);
只要有一个线程中断,那么屏障点就被打破,所有线程都被唤醒,有效避免因为一个线程中断引起永远不能到达屏障点而导致其他线程一直等待。 -
常用方法:
方法名 | 说明 |
---|---|
cyclicBarrier(int parties) | 创建一个新CyclicBarrier,parties表示有几个线程来参与这个屏障拦截 |
cyclicBarrier(int parties,Ruunable barrierAction) | 当所有线程到达一个屏障点时,优先执行barrierAction这个线程 |
int await() | 某线程调用await,说明该线程已经达到屏障点,说明当前线程被阻塞。 |
void reset() | 将屏障重置为初始状态 |
- 思路:
《创建一个类,继承自Runnable接口》
1 创建定长线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
2 创建CyclicBarrier对象
CyclicBarrier cb = new CyclicBarrier(3,this);
// 创建有三个线程参与的屏障拦截对象CyclicBarrier,执行完毕后执行当前线程的run方法
3 创建方法count 线程池的执行;屏蔽拦截对象.await方法
private void count(){
threadPool.execute(() ->
{
// 参与屏蔽拦截对象的线程需要完成的任务 完成之后执行await方法
// await操作
try{
cb.await();
}catch(InterruptedException e) {
e.printStackTrace();
}
});
// 启动有序关闭,以前提交的线程有序进行、不再接收新任务
threadPool.shutdown();
}
4 该类重写的run方法
@Override
public void run(){
// 等待屏蔽拦截对象 所需线程执行完操作之后,需要执行的操作
}
5 main方法线程
// 记录当前时间,用于测试CyclicBarrier 屏障拦截对象中参与线程的执行是否会阻塞主线程执行
long now = System.currentTimeMillis();
// 创建类对象
CyclicBarrierTest2 cb = new CyclicBarrierTest2();
// 执行线程池的具体操作
cb.count();
Thread.sleep(100);
long end = System.currentTimeMillis();
System.out.println(end - now);
案例:
package com.javaface430.test1;
import java.util.Set;
import java.util.concurrent.*;
public class CyclicBarrierTest2 implements Runnable{
//创建初始化3个线程的线程池
private ExecutorService threadPool = Executors.newFixedThreadPool(3);
//创建有3个线程参与的屏障拦截对象CyclicBarrier,执行完后执行当前类的run方法
private CyclicBarrier cb = new CyclicBarrier(3, this);
//保存每个学生的平均成绩
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
private void count() {
for (int i = 0; i < 3; i++) {
// 执行命令 command采用Lambda表达式
threadPool.execute(() -> {
//计算每个学生的平均成绩,代码略()假设为60~100的随机数
int score = (int) (Math.random() * 40 + 60);
try {
// 线程暂停0-1秒,Math.round = 四舍五入,Math.random = 产生随机数0-1
Thread.sleep(Math.round(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
// 集合中添加元素:key=线程名字,value=分数
map.put(Thread.currentThread().getName(), score);
System.out.println(Thread.currentThread().getName() + "同学的平均成绩为" + score);
try {
//执行完运行await(),等待所有学生平均成绩都计算完毕
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
//线程关闭,
threadPool.shutdown();
}
@Override
public void run() {
int result = 0;
Set<String> set = map.keySet();
for (String s : set) {
result += map.get(s);
}
System.out.println("三人平均成绩为:" + (result / 3) + "分");
}
public static void main(String[] args) throws InterruptedException {
long now = System.currentTimeMillis();
CyclicBarrierTest2 cb = new CyclicBarrierTest2();
cb.count();
Thread.sleep(100);
long end = System.currentTimeMillis();
System.out.println(end - now);
}
// 输出结果:
// 151
//pool-1-thread-3同学的平均成绩为76
//pool-1-thread-2同学的平均成绩为68
//pool-1-thread-1同学的平均成绩为73
//三人平均成绩为:72分
// 2022/4/30:xhj,先输出主线程执行时间,说明没有阻塞主线程。
}
结果分析:
2022/4/30:xhj,先输出主线程执行时间,说明没有阻塞主线程。
说下对Fork 和 Join并行计算框架的理解?
JUC中提供了Fork/Join的并行计算框架,用来处理分治的情况。
分治的思想 = 分而治之,把复杂的问题分解成相似的子问题,然后子问题再分子问题,直到问题分到很简单不必再划分为止,然后层层返回问题的结果。
分治分为两个阶段:分解任务 和 合并结果
第一个阶段:分解任务ForkJoinPool
public class ForkJoinPool extends AbstractExecutorService
把任务分解为一个个小任务直至小任务可以简单的计算返回结果;(Fork分解任务)
ForkJoinPool治理分治任务的线程池,它有多个任务队列(区别于ThreadPoolExecutor线程池),通过ForkJoinPool的invoke、submit、execute提交任务的时候会根据一定规则分配给不同的任务队列(该队列是双端队列)。
ForkJoinPool 有一个机制,当某个工作线程对应消费的任务队列空闲的时候它会去别的忙的任务队列的尾部分担任务过来执行,这种执行又被称之为窃取线程。因为这个性质,所以采用双端队列。
窃取线程如何保证不和被窃取任务的线程冲突。队列的绑定工作线程都从队列头部取任务进行执行,窃取线程会从别的队列的尾部取任务执行。
提交任务方法
提交任务方法 | 说明 |
---|---|
< T > T invoke(ForkJoinTask< T > task) | 指定给定的任务,完成后返回其结果 同步,有返回结果(会阻塞) |
Future< ? > submit(Runnable task) | 提交一个可运行的任务执行,返回一个表示该任务的Future 异步,有返回结果(Future<>) |
void execute(Runnable task) | 在将来的某个时刻执行给定的命令 异步,无返回结果 |
构造方法
ForkJoinPool(int parallelism) —— 使用指定的并行级别创建一个ForkJoinPool,使用所有其他的参数的默认值。
完整的参数介绍如下:
参数 | 说明 |
---|---|
int parallelism | 并行级别 = 也就是设置最大并发数 默认值:Runtime.availableProcessors() |
ForkJoinPool.ForkJoinWorkerThreadFactory factory | 创建新线程的工厂 默认值 defaultForkJoinWorkerThreadFactory |
Thread.UncaughtExceptionHandler Handler | 由于执行任务时遇到不可恢复的错误而终止的内部工作线程的处理程序 默认值 null |
boolean asyncMode | true = 为从未连接的分叉任务简历本地先进先出调度模式;默认值false = 基于本地堆栈的模式 |
int corePoolSize | 核心线程数 = 保留在线程池中的线程数 ;默认值 = 并行级别数 |
int maximumPoolSize | 允许的最大线程数;默认256 |
int minimumRunnable | 未被连接组织的核心线程允许的最小数量;默认值是 1 |
Predicate< ? super ForkJoinPool > saturate | 未被连接阻止的核心线程允许的最小数量;默认情况下,当一个线程即将被阻止连接或ForkJoinPool.ManagedBlocker,但由于超过maximumPoolSize不能被替换,因此抛出RejectExecutionException |
long keepAliveTime | 在线程终止之前自上次使用以来经过的时间;默认值 60 |
unit | keepAliveTime 参数的时间单位 |
双端队列
概念:
限定插入和删除操作在表的两端进行的线性表,两端分别称为端点1和端点2,具有队列和栈性质的数据结构。
普通队列:队列头部删除元素、队列尾部添加元素;
双端队列:队列头部添加元素、队列尾部删除元素。
第二个阶段:合并结果
把每个小任务的结果合并返回得到最终结果。(Join合并结果)
ForkJoinTask,分治任务,等同于Runnable。
抽象类,核心方法是fork和join。fork用来异步执行一个子任务,join会阻塞当前线程等待子任务返回。
ForkJoinTask有两个子类:RecursiveAction 和 RecursiveTask 都是抽象类,通过递归来执行分治任务。
每个子类都有compute抽象方法(任务执行的主要计算量),区别在于RecursiveAction没有返回值、RecursiveTast有返回值。
代码思路
1 创建ForkJoinPool,用于执行分治任务的线程池,parallelism = 并行级别,并发线程数
ForkJoinPool forkjoinpool = new ForkJoinPool(int parallelism);
2 创建具体操作执行的类,需要继承ForkJoinTask类 或者其子类 ,并重写compute()方法
public class ForkJoinTaskxhj类 extends RecursiveTask{
@Override
protected 返回值 compute(){}
// 任务执行的主要计算量
}
3 创建ForkJoinTaskxhj类的对象
ForkJoinTaskxhj类 obj = new ForkJoinTaskxhj类(参数);
4 通过ForkJoinPool对象的invoke方法提交任务并执行
forkjoinpool.invoke(obj);
案例应用:斐波那契数列
斐波那契数列:1-1-2-3-5-8-13-21-34…
公式:F(1) = 1,F(2) = 1,F(n) = F(n-2) + F(n-1) (n>=3,n为正整数)
package offer2.Test52;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class Test2 {
public static void main(String[] args) {
// 治理分治任务的线程池
ForkJoinPool forkJoinPool = new ForkJoinPool(4); // 最大并发数4
// 要计算的斐波那契数列 的 第几个元素
Fibonacci fibonacci = new Fibonacci(20);
// 任务执行开始 的时间
long startTime = System.currentTimeMillis();
// 指定给定的任务,完成返回其结果
// forkJoinPool对象.invoke方法的参数是ForkJoinTask类,则就是它的对象或者其后代对象
// 本例中 fibonacci 是ForkJoinTask类的后代对象。
Integer result = forkJoinPool.invoke(fibonacci);
// 任务执行完毕 的时间
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
// 定义Fibonacci数列方法。继承自RecursiveTask泛型类
// Fibonacci类的定义采用的是成员内部类的形式
static class Fibonacci extends RecursiveTask<Integer> {
// 成员变量
final int n;
// 构造方法
Fibonacci(int n) {
this.n = n;
}
// compute抽象方法重写 任务的主要计算量
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
Fibonacci f1 = new Fibonacci(n - 1);
// 在当前任务运行的池中异步执行此任务
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
// compute任务执行的主要计算量;join 返回isDone的结果;
// isDone执行成功返回true,失败返回false
return f2.compute() + f1.join();
}
}
}
扩展
JUC = java.util.concurrent包的缩写
JUC指的是Java的并发工具包,里边提供了各种各样的控制同步和线程通信的工具类。
RecursiveTask
public abstract class RecursiveTask extends ForkJoinTask< V > 递归返回结果RecursiveTask。
常用方法:
方法名 | 说明 |
---|---|
protected abstract V compute() | 任务执行的主要计算量 |
public final ForkJoinTask< V > fork() ForkJoinTask | 在当前任务正在运行的池中异步执行此任务 |
public final V join() ForkJoinTask | 返回isDone计算的结果 |
boolean isDone() | 如果此任务完成,则返回true |
java中提供了哪些并发容器?
java.util.concurrent包中提供了多种并发容器。
并发类容器是专门针对多线程并发设计的,使用了分段锁技术,只对操作的位置进行同步操作,对于其他没有操作的位置其他线程仍然可以访问,提高了程序的吞吐量。
采用了CAS算法 和 部分代码使用synchronized锁保证线程安全。
有七大并发容器。
容器名 | 对应的非并发容器 | 目标 | 原理 |
---|---|---|---|
CopyOnWriteArrayList | ArrayList | 替换Vector、synchronizedList | 高并发具有读多写少的特性,读操作不加锁,写操作(复制一份到新的集合,在新集合上修改,将新集合赋值给旧的引用,通过volatile修饰数组保证修改数组可见性) |
CopyOnWriteArraySet | HashSet | 替换synchronizedSet | 执行add操作调用的是CopyOnWriteArrayList的addIfAbsent方法,遍历的是当前Object数组,如果数组中有当前元素,直接返回;没有则放入数组尾部,并返回 |
ConcurrentSkipListSet | TreeSet | 替换synchronizedSortedSet | 内部基于ConcurrentSkipListMap实现 |
ConcurrentHashMap | HashMap | 替换Hashtable、synchronizedMap,支持复合操作 | JDK6采用更细粒度的加锁机制segment“分段锁”,JDK8采用CAS无锁算法 |
ConcurrentSkipListMap | TreeMap | 替换synchronizedSortedMap(TreeMap) | Skip list跳表,一种可以替代平衡数的数据结构,默认是按照Key值升序的 |
ConcurrentLinkedQueue | Queue | 不会阻塞的队列 | 基于链表实现的FIFO队列 |
LinkedBlockingQueue ArrayBlockintQueue PriorityBlockingQueue | BlockingQueue | 拓展了Queue,增加了可阻塞的插入和获取等操作 | 通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒 |
扩展
2022/5/3 |
---|
关于CopyOnWriteArraySet中的add方法,调用的是CopyOnWriteArrayList中的addIfAbsent方法 |
package offer2.Test53;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
public class Test1 {
public static void main(String[] args) {
CopyOnWriteArraySet<Integer> safeList = new CopyOnWriteArraySet<>();
System.out.println("是否添加成功:" + safeList.add(57));
}
}
// add源码:
public boolean add(E e) {
return al.addIfAbsent(e);
}
private final CopyOnWriteArrayList<E> al;
谈谈对CopyOnWriteArrayList的理解?
ArrayList是非线程安全的,即不提供同步,当多个线程读写ArrayList的时候可能会出现线程安全问题。
线程安全的ArrayList:
介绍 | SynchronizedList | CopyOnWriteArrayList |
---|---|---|
概念 | Collections实用类提供同步容器包装,将普通的集合包装成线程安全的集合。通过Collections.synchronizedList(List < T >)方法可以把一个非线程安全的List集合变成线程安全的集合。 | 对某个内存进行修改,不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,将原来内存指针指向新的内存,原来的内存就被回收掉了。 |
本质 | 通过对每个方法加同步锁,使得每个线程读写ArrayList只能按序进行。 | 只对写操作加锁,对读操作不加锁。在迭代期间不需要对容器进行加锁或复制 |
缺点 | 每个线程读写ArrayList都需要进行同步,开销大 | 不能保证数据的瞬时一致性 频繁写操作开销较大 |
CopyOnWriteArrayList
针对动态数组,是线程安全版本的ArrayList。
允许元素重复。
只对写操作加锁,对读操作不加锁。
写操作不是在原本数组上操作,而是将其复制为新数组,在新数组上执行操作,之后将旧引用改为新引用。
适用场景:读多写少的场景,即集合中元素不会经常变动。
CopyOnWriteArrayList源码介绍:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L
// 定义监视器对象
final transient Object lock = new Object();
// 使用Volatile修饰数组,保证修改数组的可见性。
private transient volatile Object[] array;
// 使用同步代码块的形式保证 线程的操作安全。
public boolean add(E e) {
synchronized (lock) {}
}
谈谈对BlockingQueue的理解?分别有哪些实现类?
阻塞队列BlockingQueue
- 阻塞队列与普通队列的区别:当队列是空的时候,从队列中获取元素的操作将会阻塞;当队列是满的,往队列里面添加元素的操作会被阻塞。
- 是java.util.concurrent包提供的接口,常用于多线程编程中容纳任务队列。
- 使用阻塞队列的好处:不需要关心什么时候阻塞线程、什么时候唤醒线程,阻塞队列都帮助我们考虑了。
常用实现类
类名 | 说明 |
---|---|
ArrayBlockingQueue | 数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 链表结构组成的有界阻塞队列 |
LinkedTransferQueue | 链表结构组成的无界阻塞队列,和SynchronousQueue类似,还含有非阻塞方式 |
LinkedBlockingDeque | 链表结构组成的双向阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列,即直接提交给线程不保持它们 = 单个元素的队列 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的延迟无界阻塞队列 |
常用方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) 阻塞队列满,再插入元素抛出IllegalStateException:Queue full… | offer(e) 成功返回true,失败返回false | put(e) 阻塞队列满,生产线程持续执行put操作,队列会阻塞生产线程直到put数据成功或响应中断退出 | offer(Element e,long time,TimeUnit unit) 阻塞队列满,队列会阻塞生产线程一定时间,超时后生产线程会退出 |
移除 | remove() 阻塞队列为空,执行remove操作会抛出NoSuchElementException | poll() 成功返回队列元素,队列没有则返回null | take() 阻塞队列为空,消费者线程从队列里take元素,队列会一直阻塞消费者线程直到队列可用 | poll(long timeout,TimeUnit unit) 当队列为空,队列会阻塞消费者线程一段时间,超过时限后消费者线程退出 |
检查 | element() 阻塞队列为空,执行element会抛出NoSuchElementException异常 | peek() 队列为空返回null | 不可用 | 不可用 |
- 使用LinkedBlockingQueue< E > 实现线程同步。
LinkedBlockingQueue< E > 是一个基于已连接节点的,范围任意的阻塞队列。
该队列按照先进先出排序元素。
队列的头部是在队列中时间最长的元素,队列的尾部是在队列中时间最短的元素,新元素插入到队列的尾部。
获取队列元素的操作只会获取头部元素,如果队列满了或者为空会进入阻塞状态。 - 常用方法:
方法名 | 说明 |
---|---|
LinkedBlockingQueue() | 创建一个容量为Integer.MAX_VALIE的LinkedBlockingQueue |
put( E e ) | 在队尾添加一个元素,如果队列满则阻塞当前线程,直到队列有空位 |
size() | 返回列表中的元素个数 |
take() | 移除并返回头部元素,如果队列空则阻塞当前线程,直到取到元素为止 |
谈谈对ConcurrentSkipListMap的理解?
概念
ConcurrentSkipListMap,是一个并发的、可排序的Map。
基于SkipList跳表实现,理论上能够在O(log(n))时间内完成查找、插入、删除。
调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数。
存储结构图:
跳表
跳表是一种用空间换时间思想的数据结构,它会随机地将一些节点提升到更高的层次,以创建一种逐层的数据结构,以提高操作的速度
平衡树 跳表 区别
区别 | 平衡树 | 跳表 |
---|---|---|
插入删除 | 可能导致平衡树进行一次全局的调整 | 只需要对整个结构的局部进行操作即可 |
高并发 | 需要全局锁来保证平衡树的线程安全 | 只需要局部锁即可 |
查询性能 | – | 时间复杂度O(log(n)) 同时维护多个链表、且链表分层 |
谈谈对ThreadLocal(线程变量)的理解?*
理解:
ThreadLocal使用在多线程的场景。
Threadlocal用来隔离数据,保证线程的封闭性,用ThreadLocal保存的数据只对当前线程生效,对其余线程不生效。
ThreadLocal = 线程变量,在ThreadLocal中保存的变量属于当前线程,该变量对其他线程是隔离的;
为什么不采用局部变量呢?因为采用ThreadLocal更能做到线程的隔离,局部变量只在当前变量中有效,无法做到线程内的资源共享。
ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量;也就是每个使用该变量的线程都会初始化一个完全独立的实例副本。
ThreadLocal涉及到的方法
方法名 | 说明 |
---|---|
static < S > ThreadLocal< S > withInitial(supplier < ? extends S > supplier) | 创建线程局部变量 只有一个方法 T get() 获得结果 |
T get() | 返回当前线程局部变量的副本中的值 |
void remove() | 删除此线程局部变量的值 |
void set(T value) | 将当前线程局部变量的副本设置为指定的值 |
protected T initialValue() | 返回当前线程的初始值,一个延迟加载的方法,只有在调用get的时候才会触发 目的是为了让子类覆盖而设计的 |
案例:
// idea_face
package com.javaface4.test2;
import java.util.concurrent.TimeUnit;
public class TestThreadLocal {
// 创建私有的对象
// private static User user = new User("jerry", "123");
// 为了实现线程之间数据的隔离,采用ThreadLocal存储数据
private static ThreadLocal<User> user = ThreadLocal.withInitial(() -> new User("jerry","123"));
// 采用lambda表达式
// 创建一个方法fun,参数是该私有对象
public void fun(User user){
System.out.println(Thread.currentThread().getName() + "开始执行,修改前的username[" + user.getUsername() + "]");
// 输出当前执行线程的名字 + 修改前参数的username
user.setUsername("tom");
// 修改User对象的Username属性
System.out.println(Thread.currentThread().getName() + "执行完毕,修改后的username[" + user.getUsername() + "]");
// 输出当前执行线程的名字 + 修改后参数的username
}
public static void main(String[] args) {
TestThreadLocal testThreadLocal = new TestThreadLocal();
new Thread(() -> {
testThreadLocal.fun(user.get());
}, "线程A").start();
// 使用Lambda表达式 创建线程
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e)
{
e.printStackTrace();
}
testThreadLocal.fun(user.get());
}, "线程B").start();
}
// 不使用ThreadLocal,则线程A对user进行修改,线程B对user的访问就是修改后的结果了。
// 输出结果
//线程A开始执行,修改前的username[jerry]
//线程A执行完毕,修改后的username[tom]
//线程B开始执行,修改前的username[tom]
//线程B执行完毕,修改后的username[tom]
// 使用ThreadLocal存储数据之后,采用ThreadLocal对象.get方法获取存储在ThreadLocal中的变量。
// 输出结果
//线程A开始执行,修改前的username[jerry]
//线程A执行完毕,修改后的username[tom]
//线程B开始执行,修改前的username[jerry]
//线程B执行完毕,修改后的username[tom]
// 两者的区别是 线程A对对象的属性值进行修改,不影响线程B的读取。
}
// user类
package com.javaface4.test2;
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public User() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
ThreadLocalMap
每个ThreadLocal里面只有一个ThreadLocalMap。
每个线程只有一个ThreadLocalMap,但一个线程可以有多个ThreadLocal来保存不同的对象,ThreadLocalMap通过数组来保存多个ThreadLocal。
ThreadLocalMap的 key键=当前的ThreadLocal对象,value值=线程的变量副本(通过get将map的entry.value返回)(存入的对象)。
java的Web项目大部分都是基于tomcat。每次访问都是一个新的线程,每个线程都有独立的ThreadLocal,在接受请求的时候set特定的内容,需要的时候通过get获得。
在哪些场景下会使用到ThreadLocal?*
- 代替参数的显式传递。
Controller层会接受来自前端的参数,将参数存入ThreadLocal中则参数就不是显式传递了。 - 全局存储用户信息在现有的系统设计中。
前后端分离,用户信息的获取是难点。会在拦截器的业务中获取到保存的用户信息,然后存入ThreadLocal,那么线程在任何地方都可以通过ThreadLocal的get方法获得用户信息。 - 框架使用ThreadLocal用来保存数据库连接,用于线程间数据隔离。
spring框架中,业务分为Controller层、Service层、Dao层,Dao层使用单例,负责连接数据库的connection只有一个,每个请求都去连接数据库,就会造成线程不安全的问题。解决:采用ThreadLocal,每个请求线程需要功能Connection的时候,都会从ThreadLocal获取一次,如果为null,说明该请求没有进行数据库连接,连接后存入ThreadLocal。这样每个请求线程都保存有一份自己的Connection,就解决了线程安全问题。
在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束;
进行事务操作,用于存储线程事务信息;
ThreadLocal来做session、cookie的隔离;