高并发知识梳理
2020-04-27 15:28:13 5 举报
AI智能生成
高并发知识梳理
作者其他创作
大纲/内容
基础知识
线程和进程的区别
进程
是系统运行程序的基本单位。系统运行一个程序即是从创建进程、运行到消亡的过程。
多个进程时,每个进程都有自己独立的内存空间。
运行Java程序Main线程时,就是在系统启动了一个进程。
线程
是CPU执行和调度的基本单位。
线程存在于进程之中,一个进程至少有一个线程;多个线程通过线程上下文切换并发执行,并共享进程的内存空间。
运行Main方法后,产生了一个Java进程,其中有一个Main线程,其中Main线程是非守护线程。
JVM中,程序计数器、虚拟机栈、本地方法栈线程独有,堆,方法区是共享的。
并行和并发
并行:多处理器同时处理多个任务。真正的同时进行。
并发:多任务在一个CPU上,按照细分的时间片轮流交替执行。逻辑上的同时进行。
线程的生命周期
初始状态
new,还未start
可运行
执行了start
运行
获取CPU时间片来执行任务
休眠
Blocked
进入:等待获取synchronize监视器锁
可运行:获取到锁
Waiting
进入:调用了wait,join
可运行:当被其他线程notify或者notifyAll时
Time-Waiting
进入:调用了wait(time),join(time),sleep(time)
可运行:时间到或者当被其他线程notify或者notifyAll时
终止
线程运行结束
wait和sleep区别
相同点
两者都会让当前线程休眠,并让出CPU执行权
不同点
1、API不一样。sleep是Thread类中的方法。wait是Object类中的方法。
2、使用地方不一样。sleep可以在任何地方使用。wait只能在同步方法或者同步代码块中使用。
3、线程对锁的持有状态的影响。如果当前线程持有锁,sleep不会影响,但是wait会让当前线程释放锁。
4、sleep只是在定时期间内暂时让出了CPU,定时结束是直接继续执行的。而wait()要么需要其他线程来唤醒;要么就通过wait(time)继续去抢夺锁资源。
notify和notifyAll区别
释放一个还是释放全部休眠状态的线程的区别,被释放的线程可以去竞争锁。
生产者消费者程序
wait/notify模型来实现
ReentrantLock工具实现。
Semaphore工具实现。
死锁
多个线程同时被阻塞,互相在等待对方锁的释放
形成死锁的条件
1、互斥:一个资源在任意时刻只能由一个线程持有
2、请求和保持:线程在请求其他资源时,对自己持有的资源不放
3、不可剥夺:线程正在工作时,资源不能被其他线程所抢夺,只有自己执行结束,资源才会释放。
4、循环等待:多线程之间形成了一种循环等待资源的情况
解决死锁的手段
1、让线程一次性获取所有的资源
2、当线程获取其他资源失败时,尝试释放自己的资源,而非一直等待。
3、按照顺序获取资源,并逆序释放。
并发编程的三要素
原子性:synchronized、Lock可以解决原子性。
可见性:synchronized、Lock、volatile,final可解决可见性。
有序性:volatile和Happens-Before解决有序性。
守护线程和用户线程的区别
main是用户线程,还有好多守护线程,GC就是其中之一。
查找cpu占用率最高的线程
top 找占用最高的进程pid。
jstack pid号 导出堆栈信息。
top -H -p pid 找指定进程中最高的线程
将线程转为16进制,在堆栈信息中查看。
jstack pid号 导出堆栈信息。
top -H -p pid 找指定进程中最高的线程
将线程转为16进制,在堆栈信息中查看。
创建线程的四种方式
继承Thread、实现Runnable,Callable、Executors工具类创建线程池
Runnable和Callable的区别
Runnable无法抛出异常,需要线程自己捕获。如果是unchecked异常,可以使用setUnCatchedExceptionHandler来处理
如何优雅的停止一个正在运行的线程?
1、打断
当调用了sleep,wait,join方法时,会有打断异常,通过捕获这些异常来做操作让线程结束。
2、条件判断
一般都是要执行一些循环方法的,通过条件判断,在合适的时候结束线程。
3、设置为守护线程
进程中的所有非守护线程都结束时,守护线程自然都会随着JVM停止而结束的。
4、ThreadPoolExecutor中提供的方法关闭线程池
shutdown
shutdownNow
并发理论
什么是线程安全,如何让线程安全
不安全的线程
线程安全的方法
耍花招,理想化的方法
私有的东西就不该让别人知道
大家不要抢,人人有份
只能看,不能摸
现实点的方法
没有规则,那就先入为主
相信世界充满爱,即使被伤害
并发关键字和锁
synchronized
三种使用方式
同步代码块
自定义锁对象
同步普通方法
this锁
同步静态方法
Class锁
使用场景
双重判断的懒汉式单例
底层原理总结
同步代码块原理。指代Synchronized重量级锁的加锁过程
1、同步代码块被编译后,开始和结束地方都增加了两条指令。分为被MonitorEnter和MonitorExit。
2、对象头的MarkWord中,可记录锁标志位和重量级锁(ObjectMonitor对象)的指针
3、ObjectMonitor和锁对象的关系,一对一,随着锁对象的创建和消亡而存在和消失,也可以当线程尝试获取锁对象时而建立。
4、ObjectMonitor对象的数据结构中有锁计数器,waitSet(wait方法释放了锁而休眠的线程)和EntrySet队列(在外面等待竞争锁的线程)
5、线程检查锁对象,当时无锁状态时,建立ObjectMonitor监视器锁对象,锁计数器加1,并将监视器锁对象地址写入锁对象的Markword中。完成加重量级锁。
6、其他线程来时,检查锁计数器是否为0,如果不为零,则进入EntrySet等待竞争锁。
7、锁释放。线程执行结束,释放锁,并将锁计数器清零。
同步普通方法原理
从jdk1.6之后所做的底层优化有哪些,锁升级
偏向锁的工作过程
轻量级锁的工作过程
自旋和自适应自旋基本原理
锁的状态
无锁
偏向锁
是在无竞争场景下完全消除同步,连CAS也不执行。
轻量级锁
通过CAS来避免进行开销较大的互斥操作。
重量级锁
和ReentrantLock对比
相同点
两者都是可重入锁
不同点
1、Synchronized是JVM层面,是个关键字。ReentrantLock是API层面的,是个类。
2、Synchronize使用简洁。只需要指定同步块,或者同步方法即可,不需要考虑锁释放(JVM来保证)。ReentrantLock需要配置try-finally手动保证锁的释放。
3、ReentrantLock多了几个高级功能
1、ReentrantLock提供了一种能中断等待锁的线程的机制,避免死锁。lock.lockInterruptibly()
2、ReentrantLock可以设定为公平锁(默认为非公平),而synchronized只能是非公平锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降
3、ReentrantLock配合Condition可以指定唤醒绑定到Condition上的线程,如果执行Condition实例中的notifyAll,将会唤醒绑定到此Condition中的全部线程,实现选择性通知。
如何选择
如果不是特别需要ReentrantLock的高级功能,推荐直接选择synchronize。而且虚拟机在未来的优化中,会更偏向于Synchronized的。因为JVM可以在线程和对象的元数据中记录锁的相关信息,而使用ReentrantLock的话,JVM很难得知哪些锁对象是由特定线程所持有的。
根据情况选择。因为Synchronized的锁只能升级不能降级。当竞争激烈时膨胀微重量级锁,当竞争不激烈时,还是重量级锁。不灵活。此时,ReentrantLock的锁性能就比Synchronized锁性能高了。
锁消除
锁粗化
volatile
JMM的理解
JMM是围绕并发三大特性来建立的,说一下内存模型,然后说三大特性
volatile和synchronized关键字的区别
1、volatile是Java提供的最轻量级的同步,但是只能修饰变量。Synchronize只能修饰方法或者代码块。
2、多线程访问volatile变量并不会发生阻塞,而synchronized可能会发生阻塞。
3、volatile能保证变量在多线程见的可见性,但无法保证原子性。而synchronized两者都可以保证。
何时使用volatile?
可能造成的问题
ReentrantLock
使用方法和Condition高级特性
公平和非公平锁
和Synchronized的区别
可重入锁
指同一个线程可以多次获取同一把锁,不会因为之前已经获取过还没释放而阻塞。
r和s都是可重入锁
有点是可以一定程度上避免死锁。
并发容器
ConcurrentHashMap
1、为什么是线程安全的?
JDK 1.7
使用分段锁来保证线程安全,并提供并发性。分段锁是Segment(继承了ReentrantLock)
JDK 1.8
已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)
2、底层结构
JDK 1.7
ConcurrentHashMap 底层采用 分段的数组+链表 实现
JDK 1.8
采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
HashMap为什么是线程不安全的?
1、put操作时,多线程之间数据覆盖的问题。
2、1.7是存在的问题,put中resize因为头插法引起的环形链表从而导致死循环。1.8时改为了尾插,没有这个问题了。
线程池
Runnable和Callable区别
execute和submit方法的区别是什么?
execute用于提交不需要返回值的任务,也无法判断任务是否被线程池执行完成。
submit用于提交需要返回值的任务,线程池会返回一个Future类型的对象。
ThreadPoolExecutors的构造方法参数
core
定义了可以同时运行的最少线程数量。线程池会维持这个数据量的线程。
不过刚初始化的时候是没有的。除非手动调用prestartCoreThread()方法来预加载核心线程。
max
定义了线程池中最大线程数量。核心线程都在忙,且任务队列满时,会建立新的线程,线程总数最多到最大线程数据量。
threadFactory
创建线程时会使用。推荐自定义,可以给线程起名字,并设置为守护线程。
blockingQueue
1、直接提交
SynchronousQueue。不存储任务,要等之前的任务被接受处理,才会去接新的任务。
2、有界队列
ArrayBlockingQueue
3、无界队列
LinkedBlockingQueue
4、一个具有优先级的无限阻塞队列
PriorityBlockingQueue
handler
1、AbortPolicy。抛出 RejectedExecutionException来拒绝新任务的处理
2、DiscardPolicy。悄无声息的丢弃新的任务。
3、DiscardOldestPolicy。将丢弃最早的未被处理的任务。
4、CallerRunsPolicy。调用执行自己的线程来执行新的任务。新任务的提交速度会受到影响。这种情况不会丢弃任务。
keepAliveTime
当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
JDK内置的线程池
newSingleThreadExecutor
core = max = 1
LinkedBlockingQueue。无界队列。可能会堆积大量请求,从而导致OOM
newFixedThreadPool
core = max
LinkedBlockingQueue。无界队列。可能会堆积大量请求,从而导致OOM
newCachedThreadPool
core = 0, max = Integer.MAX_VALUE
SynchronousQueue。同步队列。不会堆积请求,每个请求创建一个线程。可能会创建大量线程,从而导致OOM。
ScheduledThreadPool
core, max = Integer.MAX_VALUE
DelayedWorkQueue
一些并发工具和其他
ThreadLocal
原理
应用
用来解决数据库连接,存放conn对象,不同对象存放各自session。
解决simpleDateFormat线程安全问题。
可能会出现内存泄露,需要显示remove。不要与线程池配合,因为worker往往是不会退出的。
可能出现内存泄露的问题
父子线程中,ThreadLocal不具有继承性。
原子类
重点理解下AtomicInteger是如何保证线程安全的, volatile(可见性)+CAS(乐观锁)
CAS原理
CAS全称为Compare And Swap。CAS操作是原子性的。线程先读取主内存中的变量到工作内存,对其修改后,在写回到主内存前会再读取一次,并和之前读到的值比较,如果是一致的,说明可以安全的写回主内存。否则,就重复之前的操作,一直到成功写回主内存中。
CAS的问题
ABA问题,可以使用AtomicStampedReference带有戳的原子类引用解决
如果CAS失败,自旋会给CPU带来压力。只适用于并发小的情景。
只能保证一个变量的可见性操作,i++这种不能保证原子性。
CountDownLatch
允许一个或多个线程等待其他线程完成操作。如果在串行化的过程中,有些部分是可以并行化的,并且并行化结束后再继续串行化,那么就可以使用这个来实现。比如:从数据库取出来的数据,可以并行的取处理。
CyclicBarrier
让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞。
和CountDownLatch区别
CountDownLatch不能reset,而CyclicBarrier是可以循环使用的。
CountDownLatch:CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行。CyclicBarrier:一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
Exchanger
两个配对线程之间信息的交互。
Semaphore
信号量,可以用来实现互斥锁和生产者消费者模型
Phaser
可以在运行时期动态的增加parties中成员的数量,并且内部的计数器是可以重复使用的。不同于CountDownLatch以及CyclicBarrier的实现方式,前者计数器无法复用,后者虽可以复用计数器,但是parties中的成员数量是不可变的。
AQS
介绍
AbstractQueuedSynchronizer,是一个用来构建锁和同步器的框架。使用AQS可以构建很多高性能的同步工具,比如ReentrantLock,CountDownLatch、Semaphore等。我们也可以通过AQS来构建自己所需的同步器。
CLH双向队列结构
CLH原理
是一个虚拟的双向队列,不存在队列实例。仅存在节点之间的关联关系。
将每个请求共享资源的线程都封装成一个Node节点,来实现锁的分配。
AQS原理
核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞和唤醒时锁竞争的机制。这个机制AQS是通过CLH双向队列锁来实现的,即将暂时获取不到锁的线程加入到队列中。
原理概览
1、状态。使用一个被volatile关键字修饰的int类型变量来表示同步状态。状态修改则是使用CAS来操作。
2、AQS定义了对资源的两种共享方式
1、独占
ReentrantLock
2、共享
Semaphore、CountDownLatch、CyclicBarrier等。
AQS使用了模板设计模式,自定义同步器需要重写下面几个AQS提供的模板方法。
1、isHeldExclusively( ) 该线程是否正在独占资源。只有用到Condition才需要去实现它。
2、tryAcquire() 独占方式。尝试获取资源。成功释放资源返回true,失败返回false。
3、tryRelease( ) 独占方式。尝试释放资源。成功释放资源返回true,失败返回false。
4、tryAcquireShared( ) 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;1表示成功,且有剩余资源。
5、tryReleaseShared( ) 共享方式。尝试释放资源。成功释放资源返回true,失败返回false。
0 条评论
下一页