举例:
#### 并发:是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来
#### 并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情
run()
方法,创建实例,执行startpublic class ThreadDemo1 extends Thread {
@Override
public void run() {
System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
threadDemo1.setName("demo1");
// 执行start
threadDemo1.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
run()
方法,创建Thread类,使用Runnable接口的实现对象作为参数传递给Thread对象,调用Strat方法。public class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo2 threadDemo2 = new ThreadDemo2();
Thread thread = new Thread(threadDemo2);
thread.setName("demo2");
// start线程执行
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
// JDK8之后采用lambda表达式
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
});
thread.setName("demo2");
// start线程执行
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
call()
方法,结合FutureTask类包装Callable对象,实现多线程。call()
方法,结合多个类比如FutureTask和Thread类public class MyTask implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
return "这是返回值";
}
}
public static void main(String[] args) {
// JDK1.8 lambda表达式
FutureTask<Object> futureTask = new FutureTask<>(() -> {
System.out.println("通过Callable实现多线程,名称:" +
Thread.currentThread().getName());
return "这是返回值";
});
// MyTask myTask = new MyTask();
// FutureTask
// FutureTask继承了Runnable,可以放在Thread中启动执行
Thread thread = new Thread(futureTask);
thread.setName("demo3");
// start线程执行
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
try {
// 获取返回值
System.out.println(futureTask.get());
} catch (InterruptedException e) {
// 阻塞等待中被中断,则抛出
e.printStackTrace();
} catch (ExecutionException e) {
// 执行过程发送异常被抛出
e.printStackTrace();
}
}
public class ThreadDemo4 implements Runnable {
@Override
public void run() {
System.out.println("通过线程池+runnable实现多线程,名称:" +
Thread.currentThread().getName());
}
}
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
// 线程池执行线程任务
executorService.execute(new ThreadDemo4());
}
System.out.println("主线程名称:"+Thread.currentThread().getName());
// 关闭线程池
executorService.shutdown();
}
run()
方法,实现Runable接口需要实现run()
方法,而Callable是需要实现call()
方法start()
方法,需要new 一个Thread并发该实现类放入Thread,再通过新建的Thread实例来调用start()
方法。start()
方法。获取返回值只需要借助FutureTask实例调用get()
方法即可!线程有几个状态(6个)!
public enum State {
/**
* 线程新生状态
*/
NEW,
/**
* 线程运行中
*/
RUNNABLE,
/**
* 线程阻塞状态
*/
BLOCKED,
/**
* 线程等待状态,死等
*/
WAITING,
/**
* 线程超时等待状态,超过一定时间就不再等
*/
TIMED_WAITING,
/**
* 线程终止状态,代表线程执行完毕
*/
TERMINATED;
}
##### sleep()
属于线程Thread的方法,让线程暂缓执行,等待预计时间之后再恢复
交出CPU使用权,《不会释放锁》,抱着锁睡觉!
进入超时等待状态TIME_WAITGING,睡眠结束变为就绪Runnable
##### yield()
属于线程Thread的方法,暂停当前线程的对象,去执行其他线程
交出CPU使用权,《不会释放锁》,和sleep类似
作用:让相同优先级的线程轮流执行,但是不保证一定轮流
注意:不会让线程进入阻塞状态BLOCKED,直接变为就绪Runnable,只需要重新获得CPU使用权
##### join()
属于线程Thread的方法,在主线程上运行调用该方法,会让主线程休眠,
《不会释放锁》 让调用join方法的线程先执行完毕,再执行其他线程
类似让救护车警车优先通过!!
##### wait()
属于Object的方法,当前线程调用对象的wait方法,
《会释放锁》,进入线程的等待队列
需要依靠notify或者notifyAll唤醒,或者wait(timeout)时间自动唤醒
##### notify()
属于Object的方法
唤醒在对象监视器上等待的单个线程,《随机唤醒》
##### notifyAll()
属于Object的方法
唤醒在对象监视器上等待的全部线程,《全部唤醒》
线程安全行:
线程安全性包括两个方面,①可见性,②原子性!
volatile特性
参考文章: volatile关键字
volatile保证线程可见性案例:使用Volatile关键字的案例分析
源码分析文章参考:java同步系列之volatile解析
通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
二者对比
使用场景
对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果原子性的;
例:volatile int i = 0;
并且大量线程调用i
的自增操作,那么volatile可以保证变量的安全吗?
不可以保证!,volatile不能保证变量操作的原子性!
自增操作包括三个步骤,分别是:读取,加一,写入,由于这三个子操作的原子性不能被保证,那么n个线程总共调用n次i++
的操作后,最后的i
的值并不是大家想的n,而是一个比n小的数!
解释:
i
的初始值0
,然后就被阻塞了!i
的初始值0
,执行自增操作,此时i
的值为1
0
执行加1
与写入操作,执行成功后,i
的值被写成1
了!2
,可是输出的是1
,输出比预期小!代码实例:
public class VolatileTest {
public volatile int i = 0;
public void increase() {
i++;
}
public static void main(String args[]) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
VolatileTest test = new VolatileTest();
for (int j = 0; j < 10000; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
test.increase();
}
});
thread.start();
threadList.add(thread);
}
// 等待所有线程执行完毕
for (Thread thread : threadList) {
thread.join();
}
System.out.print(test.i);// 输出9995
}
}
总结
volatile不需要加锁,因此不会造成线程的阻塞,而且比synchronized更轻量级,而synchronized可能导致线程的阻塞!volatile由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱!
##### JAVA内存模型简称 JMM
JMM规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存就行操作。
使用volatile修饰变量,每次读取前必须从主内存属性最新的值,每次写入需要立刻写到主内存中,volatile关键字修修饰的变量随时看到的自己的最新值,假如线程1对变量v进行修改,那么线程2是可以马上看见!
JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是为了优化运行效率(不改变程序结果的前提)
int a = 3; // step:1
int b = 4; // step:2
int c =5; // step:3
int h = a*b*c; // step:4
定义顺序: 1,2,3,4
计算顺序: 1,3,2,4 和 2,1,3,4 结果都是一样的
扩展:现行发生原则happens-before(了解即可~)
volatile的内存可见性就体现了先行发生原则!
int num = 1; // 原子操作
num++; // 非原子操作,从主内存读取num到线程工作内存,进行+1,再把num写回到主内存,
// 除非用原子类:即,java.util.concurrent.atomic里的原子变量类
// 解决办法是可以用synchronized 或 Lock(比如ReentrantLock) 来把这个多步操作“变成”原子操作
// 这里不能使用volatile,前面有说到:对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果原子性的
public class XdTest {
// 方式1:使用原子类
// AtomicInteger num = 0;// 这种方式的话++操作就可以保证原子性了,而不需要再加锁了
private int num = 0;
// 方式2:使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作
Lock lock = new ReentrantLock();
public void add1(){
lock.lock();
try {
num++;
}finally {
lock.unlock();
}
}
// 方式3:使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住
public synchronized void add2(){
num++;
}
}
解决核心思想:把一个方法或者代码块看做一个整体,保证是一个不可分割的整体!
int a = 3; // step:1
int b = 4; // step:2
int c =5; // step:3
int h = a*b*c; // step:4
定义顺序: 1,2,3,4
计算顺序: 1,3,2,4 和 2,1,3,4 结果都是一样的(单线程情况下)
指令重排序可以提高执行效率,但是多线程上可能会影响结果!
假如下面的场景:
// 线程1
before();// 处理初始化工作,处理完成后才可以正式运行下面的run方法
flag = true; // 标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
// 线程2
while(flag){
run(); // 执行核心业务代码
}
// -----------------指令重排序后,导致顺序换了,程序出现问题,且难排查-----------------
// 线程1
flag = true; // 标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
// 线程2
while(flag){
run(); // 执行核心业务代码
}
before();// 处理初始化工作,处理完成后才可以正式运行下面的run方法
// 线程 A 执行
int num = 0;
// 线程 A 执行
num++;
// 线程 B 执行
System.out.print("num的值:" + num);
线程A执行 i++ 后再执行线程B,线程B可能有2个结果,可能是0和1。
因为i++
在线程A中执行运算,并没有立刻更新到主内存当中,而线程B就去主内存当中读取并打印,此时打印的就是0
;也可能线程A执行完成更新到主内存了,线程B的值是1
。
所以需要保证线程的可见性:
synchronized、lock和volatile 能够保证线程可见性
volatile保证线程可见性案例:使用Volatile关键字的案例分析
独享锁,是指锁一次只能被一个线程持有。
共享锁,是指锁一次可以被多个线程持有。
ReentrantLock和synchronized都是独享锁,ReadWriteLock的读锁是共享锁,写锁是独享锁。
与独享锁/共享锁的概念差不多,是独享锁/共享锁的具体实现。
ReentrantLock和synchronized都是互斥锁,ReadWriteLock是读写锁
下面三种是Jvm为了提高锁的获取与释放效率而做的优化 针对Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程
线程在获得了锁A并且没有释放的情况下去申请锁B,这时另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环:
public class DeadLockDemo {
private static String locka = "locka";
private static String lockb = "lockb";
public void methodA(){
synchronized (locka){
System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );
// 让出CPU执行权,不释放锁
try {
Thread.sleep(2000);// sleep不释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lockb){
System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );
}
}
}
public void methodB(){
synchronized (lockb){
System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );
// 让出CPU执行权,不释放锁
try {
Thread.sleep(2000);// sleep不释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(locka){
System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );
}
}
}
public static void main(String [] args){
System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());
DeadLockDemo deadLockDemo = new DeadLockDemo();
new Thread(()->{
deadLockDemo.methodA();
}).start();
new Thread(()->{
deadLockDemo.methodB();
}).start();
System.out.println("主线程运行结束:"+Thread.currentThread().getName());
}
}
死锁的4个必要条件:
只要发生死锁,上面的条件都成立,只要一个不满足,就不会发生死锁!
不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞!
public class UnreentrantLock {
private boolean isLocked = false;
// 加锁方法
public synchronized void lock() throws InterruptedException {
System.out.println("进入lock加锁 "+Thread.currentThread().getName());
// 判断是否已经被锁,如果被锁则当前请求的线程进行等待
while (isLocked){
System.out.println("进入wait等待 "+Thread.currentThread().getName());
wait();
}
// 如果还没被加锁,则进行加锁
isLocked = true;
}
// 解锁方法
public synchronized void unlock(){
System.out.println("进入unlock解锁 "+Thread.currentThread().getName());
isLocked = false;
// 唤醒对象锁池里面的一个线程
notify();
}
}
public class Main {
private UnreentrantLock unreentrantLock = new UnreentrantLock();
// 加锁建议在try里面,解锁建议在finally
public void methodA(){
try {
unreentrantLock.lock();
System.out.println("methodA方法被调用");
// methodA()中嵌套调用methodB(),测试methodB()是否能获取锁的执行权
methodB();
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
public void methodB(){
try {
unreentrantLock.lock();
System.out.println("methodB方法被调用");
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
public static void main(String [] args){
// 演示同一个线程下是否可冲入!(如果单线程都是不可重入的话,多线程下就不用说了~)
new Main().methodA();
}
}
// 同一个线程,重复获取锁失败,形成死锁,这个就是不可重入锁
可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁
public class ReentrantLock {
private boolean isLocked = false;
// 用于记录是不是重入的线程
private Thread lockedOwner = null;
// 累计加锁次数,加锁一次累加1,解锁一次减少1
private int lockedCount = 0;
// 加锁方法
public synchronized void lock() throws InterruptedException {
System.out.println("进入lock加锁 "+Thread.currentThread().getName());
// 获取当前线程
Thread thread = Thread.currentThread();
// 判断是否是同个线程获取锁, lockedOwner != thread引用地址的比较
// 如果已经加锁,且当前线程不是之前加锁的线程则阻塞等待!
while (isLocked && lockedOwner != thread ){
System.out.println("进入wait等待 "+Thread.currentThread().getName());
System.out.println("当前锁状态 isLocked = "+isLocked);
System.out.println("当前count数量 lockedCount = "+lockedCount);
wait();
}
// 如果没有加锁,或者当前线程是之前加锁的线程,则:
// 进行加锁,两次线程地址相同,加锁次数++
isLocked = true;
lockedOwner = thread;
lockedCount++;
}
// 解锁方法
public synchronized void unlock(){
System.out.println("进入unlock解锁 "+Thread.currentThread().getName());
// 获取当前线程
Thread thread = Thread.currentThread();
// 线程A加的锁,只能由线程A解锁,其他线程B不能解锁
if(thread == this.lockedOwner){
lockedCount--;
if(lockedCount == 0){
// 解锁
isLocked = false;
lockedOwner = null;
// 唤醒对象锁池里面的一个线程
notify();
}
}
}
}
public class Main {
//private UnreentrantLock unreentrantLock = new UnreentrantLock();
private ReentrantLock reentrantLock = new ReentrantLock();
// 加锁建议在try里面,解锁建议在finally
public void methodA(){
try {
reentrantLock.lock();
System.out.println("methodA方法被调用");
methodB();
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void methodB(){
try {
reentrantLock.lock();
System.out.println("methodB方法被调用");
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
reentrantLock.unlock();
}
}
public static void main(String [] args){
for(int i=0 ;i<10;i++){
// 演示的是同个线程
new Main().methodA();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttX91EiY-1613882316860)(小滴课堂并发与多线程相关面试题总结.assets/image-20210220161315283.png)]
源码分析文章参考:java同步系列之synchronized解析
CAS全称:Compare and Swap 比较并交换
Unsafe实现原理,参考文章:java魔法类之Unsafe解析
CAS属于乐观锁,性能较悲观锁有很大的提高!
AtomicXXX 等原子类底层就是CAS实现,一定程度比synchonized好,因为后者是悲观锁!
小滴老师讲这块的时候,对于第一次接触CAS的萌新有些不好理解,这里我参考狂神老师在介绍CAS的时候的一些理解:以一个案例入手:
案例:
public class CASDemo {
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,
// 就不更新, CAS 是CPU的并发原语!
System.out.println(atomicInteger.compareAndSet(2020, 2021));// true
System.out.println(atomicInteger.get());// 2021
//atomicInteger.getAndIncrement()// 看底层如何实现 ++
System.out.println(atomicInteger.compareAndSet(2020, 2021));// false
System.out.println(atomicInteger.get());// 2021
}
}
我们来看一下getAndIncrement()
方法的底层实现:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// UnSafe类,底层是调用C++:Java无法操作内存,所以这里借助C++来操作内存
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 获取内存偏移值valueOffset
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex); }
}
// value被volatile修饰,避免指令重排,且保证线程可见性和有序性
private volatile int value;
...
public final int getAndIncrement() {
// 参数:
// this: 当前对象
// valueOffset:当前对象的内存偏移地址
// 1:值
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
大致了解UnSafe后,我们继续点进getAndIncrement()方法中,unsafe调用的getAndAddInt()方法查看:
// 位于UnSafe类中
// 参数:var1 当前对象,var2 当前对象的内存偏移地址,var4 值(1)
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
// 这里用到了自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁。
do {
// 获取内存地址中的原对象的值
var5 = this.getIntVolatile(var1, var2);
// 借助CAS比较并交换,来实现getAndIncrement()方法的自增+1功能!
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
...
// 调用C++,执行比较并交换
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
CAS : 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就
一直循环!
狸猫换太子
public class CasAbaTest {
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
/*
* 类似于我们平时写的SQL:乐观锁
*
* 如果某个线程在执行操作某个对象的时候,其他线程若操作了该对象,
* 即使对象内容未发生变化,也需要告诉我。
*
* 期望、更新:
* public final boolean compareAndSet(int expect, int update)
* 如果我期望的值达到了,那么就更新,否则,就不更新,
* CAS 是CPU的并发原语!
*/
// ============== 捣乱的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// ============== 期望的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
输出结果:
true
2021
true
2020
true
6666
上述案例中:假设我们期望的线程本来是需要将2020更换成6666,然而有一个捣乱的线程抢在期望线程之前执行,先把2020更换为了2021,然后又将2021更换回2020!
这样看上去当期望线程执行时,初始值仍为2020没有改变,但是实际上在捣乱线程中已经执行过2次更换操作了,而我们的期望线程并不知情!这就是ABA问题!
本质上相当于采用乐观锁策略解决ABA问题!
public class CASDemo {
/**
* AtomicStampedReference 注意,
* 如果泛型是一个包装类,就需要注意对象的引用问题
* 正常在业务操作,这里面比较的都是一个个对象
*/
// 参数1:初始值100
// 参数2:初始对应的版本号 initialStamp=1
static AtomicStampedReference<Integer> atomicStampedReference =
new AtomicStampedReference<>(100,1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
// 线程A:
new Thread(()->{
// 线程执行时,先获得initialStamp版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("A线程第1次拿到的版本号为:"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// cas比较并交换:100--->101
atomicStampedReference.compareAndSet(
100,
101,
atomicStampedReference.getStamp(),// 获得最新版本号
// 更新版本号
atomicStampedReference.getStamp() + 1);
System.out.println("A线程第2次拿到的版本号为:"
+atomicStampedReference.getStamp());
// cas比较并交换:101--->100
System.out.println("A线程第2次是否执行了CAS:" +
atomicStampedReference.compareAndSet(
101,
100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("A线程第3次拿到的版本号为:"
+atomicStampedReference.getStamp());
},"A").start();
// 乐观锁的原理相同!
// 线程B:
new Thread(()->{
// 获得版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("B线程第1次拿到的版本号为:"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// cas比较并交换:100--->99
System.out.println("B线程第1次是否执行了CAS:" +
atomicStampedReference.compareAndSet(
100,
99,
stamp,
stamp + 1));
System.out.println("B线程第2次拿到的版本号为:"
+atomicStampedReference.getStamp());
},"B").start();
}
}
这样,在版本号initialStamp的限制下,每执行一次CAS,都会将版本号+1,这样即使出现了 “狸猫换太子” 情况,期望线程也能及时知道!
输出结果如下:
A线程第1次拿到的版本号为:1
B线程第1次拿到的版本号为:1
A线程第2次拿到的版本号为:2
A线程第2次是否执行了CAS:true
A线程第3次拿到的版本号为:3
B线程第1次是否执行了CAS:false
B线程第2次拿到的版本号为:3
总的来说,与MySQL的乐观锁表中加一个version字段原理相同!
注意:
Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;
下面是阿里巴巴开发手册的规范点:
所以上面的案例,如果使用大于-128-127范围的数字时候就会出现2个flase的情况!这里小伙伴一定要注意下~
参考文章:AQS面试详解
AQS的全称为(AbstractQueuedSynchronizer)抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。这个类在java.util.concurrent.locks包下面。
它是一个Java提高的底层同步工具类,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的!
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物!
ReentrantReadWriteLock
1、读写锁接口ReadWriteLock接口的一个具体实现,实现了读写锁分离
2、支持公平和非公平,底层也是基于AQS实现
3、允许从写锁降级为读锁:
流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁
4、重入:
读锁后还可以获取读锁;
获取了写锁之后既可以再次获取写锁又可以获取读锁
读锁是共享的,写锁是独占的!读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,主要是提升了读写的性能 !
ReentrantLock是独占锁且可重入的,相比synchronized而言功能更加丰富也更适合复杂的并发场景,但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防止线程A在写数据, 线程B在读数据造成的数据不一致; 但线程A在读数据,线程C也在读数据,读数据是不会改变数据没有必要加锁,但是ReentrantLock还是加锁了,降低了程序的性能,所以就有了ReadWriteLock读写锁接口!
BlockingQueue阻塞队列
BlockingQueue: juc包下的提供了线程安全的队列访问的接口,并发包下很多高级同步类的实现都是基于阻塞队列实现的!
常见的阻塞队列
Integer.MAX_VALUE
,FIFO先进先出顺序;
扩展:你知道非阻塞队列ConcurrentLinkedQueue吗,它怎么实现线程安全的?
参考文章:Java并发编程之ConcurrentLinkedQueue详解
使用线程池的好处:
重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能
类别:
【阿里巴巴编码规范】 线程池不允许使用 Executors 去创建,要通过 ThreadPoolExecutor的方式原因?
Executors创建的线程池底层也是调用 ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等
如果使用不当,会造成资源耗尽问题
直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险
##### 常见的线程池问题:
newFixedThreadPool和newSingleThreadExecutor:
队列使用LinkedBlockingQueue,队列长度为 Integer.MAX_VALUE,可能造成堆积,导致OOM
newScheduledThreadPool和newCachedThreadPool:
线程池里面允许最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM
ThreadPoolExecutor构造函数里面的参数,能否解释下各个参数的作用?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
:核心线程数,线程池也会维护线程的最少数量,默认情况下核心线程会一直存活,即使没有任务也不会受存keepAliveTime控制!
坑:在刚创建线程池时线程不会立即启动,到有任务提交时才开始创建线程并逐步线程数目达到corePoolSize
maximumPoolSize
:线程池维护线程的最大数量,超过将被阻塞!
坑:当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程
keepAliveTime
:非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于corePoolSize
unit
:指定keepAliveTime的单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS
workQueue
:线程池中的任务队列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
threadFactory
:创建新线程时使用的工厂
handler
:RejectedExecutionHandler是一个接口且只有一个方法,线程池中的数量大于maximumPoolSize,对拒绝任务的处理策略,默认有4种策略: