并发编程必备核心知识篇

写在前面

这是并发编程必备核心知识篇,本文章共1.6万字,涉及知识包括,各种锁,线程状态等各种并发编程核心知识,可用于八股文复习宝典,也可用于加深知识,建议大家以闯关的模式进行阅读,然后根据内容查漏补缺,欢迎提问相互学习交流

之前已经完成了java入门之
新版javase必备核心知识篇,点击即可学习

后续还将更新
1. 中间件之消息队列
2. Mysql核心知识点
3. http协议核心知识点
4. Spring-Mybatis核心知识点
5. 分布式缓存核心知识点


并发编程必备核心知识篇知识概览
1. 什么是进程、线程、协程,他们之间的关系是怎样的
2. 协程对于多线程的优缺点
3. 并发和并行的区别
4.java里实现多线程的几种方式及不同点
5. java线程常见的基本状态有哪些,这些状态分别是做什么的
6.多线程开发里面常用的方法,sleep/yield/join wait/notify/notifyAll
7. 几个多线程的业务场景
8.在Java中可以有哪些方法来保证线程安全
9. volatile关键字不和synchronized有什么大的区别
10.并发编程三要素
11. 常见进程间的调度算法
12. 常见线程间的调度算法
13. java里面有哪些锁
14. synchronized锁
15. 什么是CAS
16.AQS及其核心思想
17. AQS的几种同步方式,实现同步器⼀般要覆盖哪些方法
18. ReentrantLock和synchronized使用的场景是什么,实现机制有什么不同
19. ReentrantReadWriteLock和ReentrantLock有啥不同?
20. 并发编程里解决生产消费者模型有哪几种方式
21. 阻塞队列BlockingQueue及常见的阻塞队列
22. 非阻塞队列ConcurrentLinkedQueue不,是怎么实现线程安全的
23. 多线程最佳实践
24. 线程池的好处, java里有哪些是常用的线程池
25. 线程池最好不用 Executors 去创建,要通过ThreadPoolExecutor的方式的原因
26. ThreadPoolExecutor构造函数里面各个参数的作用

什么是进程、线程、协程,他们之间的关系是怎样的

进程: 本质上是⼀个独立执行的程序,进程是操作系统进⾏资源分配和调度的基本概念,操作系统进行资源分配和调度的⼀个独立单位

**线程:**是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
⼀个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。
协程: 又称为微线程,是⼀种⽤户态的轻量级线程,协程不像线程和进程需要进⾏系统内核上的上下文切换,协程的上下文·切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,⼀个线程可以多个协程,线程进程都是同步机制,而协程则是异步
Java的原⽣语法中并没有实现协程,目前python、Lua和GO等语⾔⽀持
关系:⼀个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗⼤量的CPU,CPU上真正运行的是线程,线程可以对应多个协程

协程对于多线程的优缺点

优点:

  1. 非常快速的上下文切换,不用系统内核的上下文切换,减小开销单线程即可实现高并发,单核CPU可以支持上万的协程。
  2. 由于只有⼀个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁。

缺点:
协程无法利用多核资源,本质也是个单线程
协程需要和进程配合才能运行在多CPU上目前java没成熟的第三⽅库,存在风险,调试debug存在难度,不利于发现问题。

并发和并行的区别

并发 concurrency:
⼀台处理器上同时处理任务, 这个同时实际上是交替处理多个任务,程序中可以同时拥有两个或者多个线程,当有多个线程在操作时,如果系统只有⼀个CPU,则它根本不可能真正同时进⾏⼀个以上的线程,它只能把CPU运⾏时间划分成若⼲个时间段,再将时间段分配给各个线程执⾏

并行 parallellism:
多个CPU上同时处理多个任务,⼀个CPU执行⼀个进程时,另⼀个CPU可以执行另⼀个进程,两个进程互不抢占CPU资源,可以同时进行。

并发指在⼀段时间内宏观上去处理多个任务。 并行指同⼀个时刻,多个任务确实真的同时运⾏。

例子
⼀个项目经理A和3个程序B C D的故事
单线程: 项目经理先给B讲完后等B完成,再给C讲,等C完成后再给D讲
并发:A给B讲完需求,B自己去实现,期间A继续给C和D讲,不用等待某个程序员去完成,期间项⽬经理没空闲下来

并行:直接找3个项⽬经理分别分配给3个程序员

java里实现多线程的几种方式及不同点

  1. 继承Thread
    继承Thread,重写里面run方法,创建实例,执行start
    优点:代码编写最简单直接操作
    缺点:没返回值,继承⼀个类后,没法继承其他的类,拓展性差

  2. 实现Runnable
    自定义类实现Runnable,实现里面⾯run方法,创建Thread类,使用Runnable接⼝的实现对象作为参数传递给Thread对象,调⽤Strat⽅法
    优点:线程类可以实现多个几接⼝,可以再继承⼀个类
    缺点:没返回值,不能直接启动,需要通过构造⼀个Thread实例传递进去启动

  3. 通过Callable和FutureTask方式
    创建callable接⼝的实现类,并实现call⽅法,结合FutureTask类包装Callable对象,实现多线程
    优点:有返回值,拓展性也⾼
    缺点:jdk5以后才⽀持,需要重写call⽅法,结合多个类⽐如FutureTask和Thread类

  4. 通过线程池创建线程
    自定义Runnable接⼝,实现run⽅法,创建线程池,调用执行方法并传⼊对象
    优点:安全高性能,复用线程
    缺点: jdk5后才支持,需要结合Runnable进行使用

⼀般常用的Runnable 和 第四种线程池+Runnable,简单方便扩展,和高性能 (池化的思想)

java线程常见的基本状态有哪些,这些状态分别是做什么的

首先,JDK的线程状态分6种,JVM里面9种,我们⼀般说JDK的线程状态

  1. New:初始状态,线程被创建,没有调用start()
  2. Runnable:运行状态,Java线程把操作系统中的就绪和运行两种状态统一称为“运行中”,当调用线程对象的start()⽅法,线程就进入就绪状态,但是此刻线程调度还没把该线程设置为当前线程,就是没获得CPU使用权。 如果线程运行后,从等待或者睡眠中回来之后,也会进⼊就绪状态,当程序将处于就绪状态的线程设置为当前线程,即获得CPU使⽤权,这个时候线程进入运行状态,开始运行run里面的逻辑
  3. Blocked:阻塞,线程进入等待状态,线程因为某种原因,放弃了CPU的使用权
    阻塞的几种情况:
    A. 等待阻塞:运行的线程执行了wait(),JVM会把当前线程放入等待队列
    B. 同步阻塞:运行的线程在获取对象的同步锁时,如果该同步锁被其他线程占用了,JVM会把当前线程放入锁池中
    C. 其他阻塞:运行的线程执行sleep(),join()或者发出IO请求时,JVM会把当前线程设置为阻塞状态,当sleep()执行完,join()线程终止,IO处理完毕线程再次恢复
  4. Waiting:进⼊该状态的线程需要等待其他线程做出⼀些特定动作(通
    知或中断)。
  5. timed_waiting:超时等待状态,超时以后自动返回
  6. terminated:终止状态,⼀个线程run方法执行结束,该线程就死亡了,不能进入就绪状态

多线程开发里面常用的方法,sleep/yield/join wait/notify/notifyAll

sleep
属于线程Thread的方法
让线程暂缓执行,等待预计时间之后再恢复
交出CPU使⽤权,不会释放锁
进⼊阻塞状态TIME_WAITGING,睡眠结束变为就绪Runnable

yield
属于线程Thread的⽅法
暂停当前线程的对象,去执⾏其他线程
交出CPU使⽤权,不会释放锁,和sleep类似
作用:让相同优先级的线程轮流执行,但是不保证⼀定轮流
注意:不会让线程进入阻塞状态,直接变为就绪Runnable,只需要重新获得CPU使⽤权

join
属于线程Thread的⽅法
在主线程上运行调用该方法,会让主线程休眠,不会释放已经持有的对象锁
让调用join方法的线程先执行完毕,在执行其他线程
wait
属于Object的⽅法
当前线程调用对象的wait方法,会释放锁,进⼊线程的等待队列
需要依靠notify或者notifyAll唤醒,或者wait(timeout)时间⾃动唤醒
notify
属于Object的⽅法
唤醒在对象监视器上等待的单个线程,选择是任意的
notifyAll
属于Object的⽅法
唤醒在对象监视器上等待的全部线程

几个多线程的业务场景

异步任务:用户注册、记录日志
定时任务:定期备份⽇志、备份数据库
服务器编程:Socket网络编程,⼀个连接⼀个线程

在Java中可以有哪些方法来保证线程安全

  1. 加锁,比如synchronize/ReentrantLock
  2. 使用线程安全类(原子类AtomicXXX,并发容器,同步容器CopyOnWriteArrayList/ConcurrentHashMap等)
  3. ThreadLocal本地私有变量/信号量Semaphore等

volatile关键字不和synchronized有什么大的区别

volatile可以看作是轻量级的synchronized,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象
volatile:保证可见性,但是不能保证原子性
synchronized:保证可见性,也保证原子性
使用场景
1、不能修饰写入操作依赖当前值的变量,比如num++、num=num+1,不是原子操作因为JVM字节码层面不止⼀步
2、由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱

拓展:为什么会出现脏读?
JAVA内存模型简称 JMM
JMM规定所有的变量存在在主内存,每个线程有自己的⼯作内存,线程对变量的操作都在⼯作内存中进行,不能直接对主内存进行操作
使用volatile修饰变量,每次读取前必须从主内存属性最新的值每次写⼊需要立刻写到主内存中
volatile关键字修修饰的变量随时看到的自己的最新值,假如线程1对变量v进⾏修改,那么线程2是可以马上看见的

什么是指令重排

指令重排序分两类 编译器重排序和运行时重排序
JVM在编译java代码或者CPU执⾏JVM字节码时,对现有的指令进行重新排序,主要⽬的是优化运行效率(不改变程序结果的前提)
虽然指令重排序可以提⾼执行效率,但是多线程上可能会影响结果,有什么解决办法?
解决办法:内存屏障
解释:内存屏障是屏障指令,使CPU对屏障指令之前和之后的内存操作执行结果的⼀种约束

什么是happens-before
即先行发生原则,volatile的内存可见性就提现了该原则之⼀,判断数据是否有竞争,是否线程安全的主要依据。
八大原则

  1. 程序次序规则:在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的代码。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。
  2. 管程锁定规则:一个unlock动作先行发生于后续对同一个锁的lock动作 。这里必须强调的是同一个锁,而”后面“是指时间上的先后。
  3. volatile变量规则:对某个volatile字段的写操作先行发生于每个后续对该volatile字段的读操作,这里的”后面“同样指时间上的先后顺序。
  4. 线程启动规则:在某个线程对象 上调用start()方法先行发生该启动了的线程中的任意动作
  5. 线程中断规则:某线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束(任意其它线程成功从该线程对象上的join()中返回),Thread.isAlive()的返回值等作段检测到线程已经终止执行。
  6. 线程终止规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生
  7. 对象终结规则:一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始
  8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

并发编程三要素

1. 原子性:⼀个不可再被分割的颗粒,原⼦性指的是⼀个或多个操作要么全部执⾏成功要么全部执行失败,期间不能被中断,也不存在上下⽂切换,线程切换会带来原⼦性的问题
举例:
int num = 1; // 原子操作
num++; // 非原子操作,从主内存读取num到线程⼯作内存,进行 +1,再把num写到主内存, 除非用原子类,即java.util.concurrent.atomic里的原子变量类
解决办法是可以⽤synchronized 或 Lock(⽐如ReentrantLock) 来把这个多步操作“变成”原子操作,但是volatile,前面有说到不能修饰有依赖值的情况
解决核心思想:把⼀个⽅法或者代码块看做⼀个整体,保证是⼀个不可分割的整体

2. 有序性: 程序执⾏的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序JVM在编译java代码或者CPU执⾏JVM字节码时,对现有的指令进⾏重新排序,主要目的是优化运行效率(不改变程序结果的前提)

可见性: ⼀个线程A对共享变量的修改,另⼀个线程B能够⽴刻看到
// 线程 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能够保证线程可见性

常见进程间的调度算法

先来先服务调度算法:
按照作业/进程到达的先后顺序进⾏调度 ,即:优先考虑在系统中等待时间最长长的作业
排在长进程后的短进程的等待时间长,不利于短作业/进程
短作业优先调度算法:
短进程/作业(要求服务时间最短)在实际情况中占有很大比例,为了使得它们优先执行
对长作业不友好
高响应比优先调度算法:
在每次调度时,先计算各个作业的优先权:优先权=响应比=(等待时间+要求服务时间)/要求服务时间,因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应比=响应时间/要求服务时间,选择优先权高的进行服务需要计算优先权信息,增加了系统的开销

时间片轮转调度算法:
轮流的为各个进程服务,让每个进程在⼀定时间间隔内都可以得到响应
由于高频率的进程切换,增加了开销,且不区分任务的紧急程度

优先级调度算法:
根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理
如果高优先级任务很多且持续产⽣,那低优先级的就可能很慢才被处理

常见的线程间调度算法

线程调度是指系统为线程分配CPU使用权的过程,主要分两种

  1. 协同式线程调度(分时调度模式):线程执⾏时间由线程本身来控制,线程把自己的⼯作执行完之后,要主动通知系统切换到另外⼀个线程上。最⼤好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果⼀个线程有问题,可能⼀直阻塞在那里
  2. 抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有⼀个线程导致整个进程阻塞

Java线程调度就是抢占式调度,优先让可运⾏池中优先级⾼的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择⼀个线程
所以我们如果希望某些线程多分配⼀些时间,给⼀些线程少分配⼀些时间,可以通过设置线程优先级来完成。

同时JAVA的线程的优先级,以1到10的整数指定。当多个线程可以运行时,VM⼀般会运行最⾼优先级的线程(Thread.MIN_PRIORITY⾄Thread.MAX_PRIORITY)
在两线程同时处于就绪runnable状态时,优先级越⾼的线程越容易被系统选择执⾏。但是优先级并不是100%可以获得,只不过是机会更大而已。
有⼈会说,wait,notify不就是线程本身控制吗?
其实不是,wait是可以让出执行时间,notify后无法获取执行时间,随机等待队列里面获取而已

java里面有哪些锁

  1. 悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized

  2. 乐观锁:每次去拿数据的时候都认为别⼈不会修改,更新的时候会判断是别⼈是否回去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,比如CAS是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
    小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会⽐悲观锁多

  3. 公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说 如果⼀个线程组里,能保证每个线程都能拿到锁 比如ReentrantLock(底层是同步队列FIFO:First Input First Output来实现)

  4. 非公平锁:获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronized、ReentrantLock
    小结:⾮非公平锁性能⾼于公平锁,更能重复利用CPU的时间

  5. 可重入·锁:也叫递归锁,在外层使⽤锁之后,在内层仍然可以使用,并且不发生死锁

  6. 不可重⼊锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
    小结:可重⼊锁能⼀定程度的避免死锁 synchronized、ReentrantLock 重⼊锁

  7. 自旋锁:⼀个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有⼀个执行单元获得锁.
    小结:不会发生线程状态的切换,⼀直处于用户态,减少了线程上下⽂切换的消耗,缺点是循环会消耗CPU
    常见的自旋锁:TicketLock,CLHLock,MSCLock

  8. 共享锁:也叫S锁/读锁,能查看但⽆法修改和删除的⼀种数据锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享

  9. 互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁/ 该锁每⼀次只能被⼀个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得互斥锁的线程即能读数据又能修改数据

  10. 死锁:两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的⼀种阻塞的现象,若无外力作用,它们都将⽆法让程序进行下去

下⾯三种是Jvm为了提⾼锁的获取与释放效率而做的优化 针Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程,
偏向锁:⼀段同步代码⼀直被⼀个线程所访问,那么该线程会自动获取锁,获取锁的代价更低,
轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点
重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会⼀直循环下去,当自旋⼀定次数的时候且还没有获取到锁,就会进⼊阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进⼊阻塞,性能也会降低

死锁代码:

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);
 } 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);
 } 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 FixDeadLockDemo {
 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);
 } 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);
 } 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());
 FixDeadLockDemo deadLockDemo = new FixDeadLockDemo();
 new Thread(()->{
 deadLockDemo.methodA();
 }).start();
 new Thread(()->{
 deadLockDemo.methodB();
 }).start();
 System.out.println("主线程运⾏结束:"+Thread.currentThread().getName());
 }
}

synchronized锁

synchronized是解决线程安全的问题,常⽤在 同步普通方法、静态方法、代码块中
非公平、可重⼊
每个对象有⼀个锁和⼀个等待队列,锁只能被⼀个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出⼀个并唤醒,唤醒哪个线程是不确定的,不保证公平性
两种形式:

方法(public synchronized void method(){}):生成的字节码文件中会多⼀个 ACC_SYNCHRONIZED 标志位,当⼀个线程访问⽅法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,⽅法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同⼀个monitor对象,也叫隐式同步

代码块(synchronized(XXX){}):加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和monitorexit 两条指令,每个monitor维护着⼀个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,当⼀个线程获执行monitorenter后,该计数器自增1;当同⼀个线程执行monitorexit指令的时候,计数器再自减1。当计数器为0的时候,monitor将被释放.也叫显式同步

两种本质上没有区别,底层都是通过monitor来实现同步, 只是⽅法的同步是⼀种隐式的⽅式来实现,⽆需通过字节码来完成

什么是CAS

全称是Compare And Swap,即比较再交换,是实现并发应用到的⼀种技术底层通过Unsafe类实现原子性操作操作包含三个操作数 —— 内存地址(V)、预期原值(A)和新值(B)。
如果内存位置的值与预期原值相匹配,那么处理器会⾃动将该位置值更新为新值 ,若果在第⼀轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

CAS这个是属于乐观锁,性能较悲观锁有很大的提⾼
AtomicXXX 等原子类底层就是CAS实现,⼀定程度比synchonized好,因为后者是悲观锁

CAS会存在的问题

  1. 自旋时间长CPU利用率增加,CAS里面是⼀个循环判断的过程,如果线程⼀直没有获取到状态,cpu资源会⼀直被占用
  2. 存在ABA问题
    如果⼀个变量V初次读取是A值,并且在准备赋值的时候也是A值,那就能说明A值没有被修改过吗?其实是不能的,因为变量V可能被其他线程改回A值,结果就是会导致CAS操作误认为从来没被修改过,从而赋值给V
    给变量加⼀个版本号即可,在比较的时候不仅要比较当前变量的值 还需要⽐较当前变量的版本号。
    在java5中,已经提供了AtomicStampedReference来解决问题,检查当前引⽤是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原⼦的⽅式将引用和标志都设置为新值

AQS及其核心思想

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。它是⼀个Java提高的底层同步⼯具类,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的
只要搞懂了AQS,那么J.U.C中绝⼤部分的api都能轻松掌握
简单来说:是用⼀个int类型的变量表示同步状态,并提供了⼀系列的CAS操作来管理这个同步状态对象
⼀个是 state(⽤于计数器,类似gc的回收计数器)
⼀个是线程标记(当前线程是谁加锁的),
⼀个是阻塞队列(⽤于存放其他未拿到锁的线程)

浅析部分源码方法
acquire(int arg) ,好比加锁lock操作
tryAcquire()尝试直接去获取资源,如果成功则直接返回,AQS里面未实现但没有定义成abstract,因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared,类似设计模式⾥⾯的适配器模式

addWaiter() 根据不同模式将线程加⼊等待队列的尾部,有Node.EXCLUSIVE互斥模式、Node.SHARED共享模式;如果队列不为空,则以通过compareAndSetTail⽅法以CAS将当前线程节点加⼊到等待队列的末尾。否则通过enq(node)⽅法初始化⼀个等待队列 acquireQueued()使线程在等待队列中获取资源,⼀直获取到资源后才返回,如果在等待过程中被中断,则返回true,否则返回false
release(int arg) 好比解锁unlock
独占模式下线程释放指定量的资源,里面是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了;在自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false
unparkSuccessor方法用于唤醒等待队列中下⼀个线程

AQS的几种同步方式,实现同步器⼀般要覆盖哪些方法

独占式: 比如ReentrantLock
共享式:比如Semaphore
存在组合:组合式的如ReentrantReadWriteLock,AQS为使⽤提供了底层支撑,使⽤者可以自由组装实现

  1. boolean tryAcquire(int arg)
  2. boolean tryRelease(int arg)
  3. int tryAcquireShared(int arg)
  4. boolean tryReleaseShared(int arg)
  5. boolean isHeldExclusively()

不需要全部实现,根据获取的锁的种类可以选择实现不同的方法,比如
实现支持独占锁的同步器应该实现tryAcquire、 tryReleaseisHeldExclusively
实现支持共享获取的同步器应该实现tryAcquireSharedtryReleaseShared、
isHeldExclusively

ReentrantLock和synchronized使用的场景是什么,实现机制有什么不同

ReentrantLock和synchronized都是独占锁
synchronized:
1、是悲观锁会引起其他线程阻塞,java内置关键字,
2、⽆法判断是否获取锁的状态,锁可重⼊、不可中断、只能是非公平
3、加锁解锁的过程是隐式的,用户不用⼿动操作,优点是操作简单但显得不够灵活
4、⼀般并发场景使用足够、可以放在被递归执行的⽅法上,且不用担心线程最后能否正确释放锁
ReentrantLock:
1、是个Lock接⼝的实现类,是悲观锁,
2、可以判断是否获取到锁,可重⼊、可判断、可公平可不公平
3、需要⼿动加锁和解锁,且 解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
4、在复杂的并发场景中使用在重⼊时要却确保重复获取锁的次数必须和重复释放锁的次数⼀样,否则可能导致 其他线程无法获得该锁。
5、创建的时候通过传进参数true创建公平锁,如果传⼊的是false或没传参数则创建的是非公平锁
6、底层不同是AQS的state和FIFO队列来控制加锁

ReentrantReadWriteLock和ReentrantLock有啥不同

ReentrantReadWriteLock
1、读写锁接⼝ReadWriteLock接⼝的⼀个具体实现,实现了读写锁的分离,
2、支持公平和非公平,底层也是基于AQS实现
3、允许从写锁降级为读锁
流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁

4、重⼊:读锁后还可以获取读锁;获取了写锁之后既可以再次获取写锁⼜可以获取读锁
核心:读锁是共享的,写锁是独占的。 读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,主要是提升了读写的性能

ReentrantLock是独占锁且可重⼊的,相比synchronized⽽⾔功能更加丰富也更适合复杂的并发场景,但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防⽌线程A在写数据, 线程B在读数据造成的数据不⼀致; 但线程A在读数据,线程C也在读数据,读数据是不会改变数据没有必要加锁,但是还是加锁了,降低了程序的性能,所以就有了ReadWriteLock读写锁接口
场景:读多写少,比如设计⼀个缓存组件 或 提高Collection的并发性

并发编程里解决生产消费者模型有哪几种方式

核心:要保证生产者不会在缓冲区满时放⼊数据,消费者也不会在缓冲区空时消耗数据
常用的同步方法是采用信号或加锁机制
1、wait() / notify()⽅法
2、await() / signal()方法
用ReentrantLock和Condition实现等待/通知模型
3、Semaphore信号量
4、BlockingQueue阻塞队列
ArrayBlockingQueue
LinkedBlockingQueue
put方法用来向队尾存⼊元素,如果队列满,则阻塞
take方法用来从队⾸取元素,如果队列为空,则阻塞

阻塞队列BlockingQueue及常见的阻塞队列

BlockingQueue: j.u.c包下的提供了线程安全的队列访问的接⼝,并发包下很多⾼级同步类的实现都是基于阻塞队列实现的
1、当阻塞队列进行插⼊数据时,如果队列已满,线程将会阻塞等待直到队列非满
2、从阻塞队列读数据时,如果队列为空,线程将会阻塞等待直到队列⾥⾯是非空的时候
常见的阻塞队列

  1. ArrayBlockingQueue:
    基于数组实现的⼀个阻塞队列,需要指定容量大小,FIFO先进先出顺序
  2. LinkedBlockingQueue:
    基于链表实现的⼀个阻塞队列,如果不指定容量大小,默认Integer.MAX_VALUE, FIFO先进先出顺序
  3. PriorityBlockingQueue:
    ⼀个⽀持优先级的⽆界阻塞队列,默认情况下元素采⽤⾃然顺序升序排序,也可以自定义排序实现 java.lang.Comparable接⼝
  4. DelayQueue:
    延迟队列,在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素,⾥⾯的对象必须实现 java.util.concurrent.Delayed 接⼝并实现CompareTo和getDelay方法

** 非阻塞队列ConcurrentLinkedQueue不,是怎么实现线程安全的**
线程安全原因:
ConcurrentLinkedQueue是基于链表实现的⽆界线程安全队列,采用FIFO进行排序
保证线程安全的三要素:原子、有序、可见性
3. 底层结构是Node,链表头部和尾部节点是head和tail,使用节点变量和内部类属性使用volatile声明保证了有序和可见性
4. 插⼊、移除、更新操作使⽤CAS⽆锁操作,保证了原子性
5. 假如多线程并发修改导致 CAS 更新失败,采用for循环插⼊保证更新操作成功

** 多线程最佳实践**

  1. 给不同模块的线程起名称,方便后续排查问题
  2. 使用同步代码块或者同步的⽅法的时候,尽量减⼩同步范围
  3. 多用并发集合少⽤同步集合
    同步集合:Hashtable/Vector/同步⼯具类包装Collections.synXXX
    并发集合:ConcurrentHashMap、CopyOnWriteArrayList
  4. 线上业务需要使用多线程,优先考虑线程池是否更加合适,然后判断哪种线程池比较好,最后才是自己创建单⼀线程

线程池的好处, java里有哪些是常用的线程池

好处:重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能
类别
newFixedThreadPool
⼀个定长线程池,可控制线程最⼤并发数
newCachedThreadPool
⼀个可缓存线程池
newSingleThreadExecutor
⼀个单线程化的线程池,用唯⼀的⼯作线程来执⾏任务
newScheduledThreadPool
⼀个定⻓线程池,支持定时/周期性任务执行

线程池最好不用 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种策略AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工藤学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值