Java并发编程
2020-09-18 09:28:02 57 举报
AI智能生成
java并发编程
作者其他创作
大纲/内容
阻塞队列
BlockingQueue
什么情况下我们会使用 阻塞队列
多线程并发处理,线程池!
添加和移除使用
线程池
三大方法、7大参数、4种拒绝策略
先创建核心线程数,核心线程数满了放队列,然后队列满了,才会去创建最大线程数,最大线程数达到最大的时候,就会启用拒绝策略
reject策略
有界队列
无界队列
调用超时,队列变得越来越大,此时会导致内存飙升起来,而且还可能会导致你会OOM,内存溢出
要根据具体的场景以及具体的压测数据,来设定这些参数
可以手动去实现一个拒绝策略,将请求持久化一下,然后后台线程去等线程池负载降下来了后再读出来继续执行
如果后续慢慢的队列里没任务了,线程空闲了,超过corePoolSize的线程会自动释放掉,在keepAliveTime之后就会释放
七个参数
corePoolSize的数量,maximumPoolSize,队列类型,最大线程数量,拒绝策略,线程释放时间
最大线程数调得太大可能OOM
最大线程数改为Integer.MAX_VALUE的话,会导致jvm创建的线程超过系统允许创建的最大线程数限制而产生OOM
锁
锁优化
减少锁的时间
减少锁的粒度
锁升级
公平锁、非公平锁
公平锁(Fair)
非公平锁(Nonfair)
ReentrantLock
默认是非公平锁
悲观锁、乐观锁
乐观锁
自旋锁
自旋锁尽可能的减少线程的阻塞
自旋锁的优缺点
锁的竞争不激烈
自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗
锁的竞争激烈
不适合使用自旋锁
自旋锁在获取锁前一直都是占用cpu做无用功
线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费
自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理
自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能
自旋锁的开启
ABA问题
引入版本号,每次变量更新都把版本号加一
偏向锁
Java6引入的一项锁优化
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能
持有偏向锁的线程将永远不需要再进行同步
在物竞争情况下把整个同步都消除掉,连CAS操作都不做
当有另外一个线程去尝试获取这个锁的时候,偏向模式就结束了
适用场景
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用
一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作
jvm开启/关闭偏向锁
轻量级锁
由偏向所升级来的
偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的释放
悲观锁
重量级锁
Synchronized
可以把任意一个非NULL的对象当作锁
作用于方法时,锁住的是对象的实例(this)
当作用于静态方法时,锁住的是Class实例
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块
Synchronized是非公平锁
synchronized机制对锁的释放是隐式的
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
互斥锁、读写锁
互斥锁
一次最多只能有一个线程持有的锁
synchronized
Lock
会造成线程阻塞,降低运行效率,并有可能产生死锁、优先级翻转等一系列问题
读写锁
默认情况下也是非公平锁
ReadWriteLock
实现类ReentrantReadWriteLock
读取锁(ReadLock)
允许多个reader线程同时持有
写入锁(WriteLock)
最多只能有一个writer线程持有
允许一次读取多个线程,但一次只能写入一个线程
如果一个线程已经持有了写入锁,则可以再持有读锁。相反,如果一个线程已经持有了读取锁,则在释放该读取锁之前,不能再持有写入锁
使用场合
读取数据的频率远大于修改共享数据的频率
死锁
实例
避免死锁的几个常见方法
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
ThreadLocal
创建线程局部变量的类
问题
内存数据结构
ThreadLocal在项目中的应用场景
实现单个线程单例以及单个线程上下文信息存储,比如交易id等
主要解决多线程中数据因并发产生不一致问题
可能出现的内存泄漏问题
常用的方法
ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改
http://www.threadlocal.cn/
与Synchronized区别
Synchronized用于线程间的数据共享
ThreadLocal用于线程间的数据隔离
Synchronized用于实现同步机制
ThreadLocal和Synchonized都用于解决多线程并发访问
synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问
ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,
这样就隔离了多个线程对数据的数据共享
这样就隔离了多个线程对数据的数据共享
Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享
ThreadLocal不能使用原子类型,只能使用Object类型
采取空间换时间
ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,
但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度
但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度
volatile
volatile是轻量级的synchronized
比synchronized的使用和执行成本更低
不会引起线程上下文的切换和调度
保证可见性
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
主要是lock指令+MESI协议,让修改后的值强制刷入主存,并且让其他CPU的换成全部失效
总线嗅探会有什么问题?
重排序
为了提高处理器的运用效率,处理器会保证重排序后执行代码的结果跟重排序前是一样的
保证有序性
happens-before原则保证有序性
8个规则
该规则规定好了在一些特殊情况下,不允许编译器,指令器对你写的代码进行指令重排序,必须保证你代码的有序性
主要是因为有内存屏障
三个概念
原子性
概念
一个或多个操作。要么全部执行完成并且执行过程不会被打断,要么不执行
例子:i++/i--操作。不是原子性操作,如果不做好同步性就容易造成线程安全问题
使用循环CAS实现原子操作
CAS实现原子操作的三大问题
ABA问题
解决思路就是使用版本号
在变量前面追加上版本号,每次变量更新的时候把版本号加1,
那么A→B→A就会变成1A→2B→3A
那么A→B→A就会变成1A→2B→3A
Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
只能保证一个共享变量的原子操作
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,
就可以把多个变量放在一个对象里来进行CAS操作
就可以把多个变量放在一个对象里来进行CAS操作
使用锁机制实现原子操作
可见性
多个线程访问同一个变量,一个线程改变了这个变量的值,其他线程可以立即看到修改的值
可见性的问题,有两种方式保证
一是volatile关键字
二是通过synchronized和lock
有序性
程序执行的顺序按照代码的先后顺序执行
通过volatile关键字,还有synchronized和lock来保证有序性
volatile通过禁止指令重排序来保证
synchronized和lock保证每个时刻只有一个线程执行同步代码,使得线程串行化执行同步代码,保证了有序性
Java内存模型(JMM)
内存模型基础
并发编程模型的两个关键问题
线程之间如何通信
通信机制有两种
共享内存
Java的并发采用的是共享内存模型
线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信
消息传递
线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信
线程之间如何同步
在共享内存并发模型里,同步是显式进行的;
程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行
程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
抽象结构
堆内存在线程之间共享
局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响
Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见
JMM定义了线程和主内存之间的抽象关系
结构图和通信图
结构示意图
通信图
这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证
JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证
指令序列的重排序
重排序分3种类型
编译器优化的重排序
指令级并行的重排序
内存系统的重排序
从源码到最终执行的指令序列的示意图
1属于编译器重排序,2和3属于处理器重排序
可能会导致多线程程序出现内存可见性问题
JMM禁止重排序
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,
通过内存屏障指令来禁止特定类型的处理器重排序
通过内存屏障指令来禁止特定类型的处理器重排序
happens-before
程序中的代码如果满足这个条件,就一定会按照这个规则来保证指令的顺序
重排序
数据依赖性
as-if-serial语义
不管怎么重排序,程序的执行结果不能被改变
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序
如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序
程序顺序规则
重排序对多线程的影响
volatile
volatile写的内存语义
volatile内存语义实现
JMM会分别限制这两种类型的重排序类型
锁
锁的释放和获取的内存语义
内存模型的理解
示意图
六个过程
read(主内存读取数据) ->load(加载到工作内存) ->use(操作共享变量)
->assign(计算好的值重新赋给变量) ->store(尝试往主内存写) ->write(正式刷回到主内存)
->assign(计算好的值重新赋给变量) ->store(尝试往主内存写) ->write(正式刷回到主内存)
并发编程基础
线程
6种状态
NEW(初始状态)
new一个实例出来,线程就进入了初始状态,还没有调用start方法
RUNNABLE(运行状态)
调用线程的start()方法,此线程进入就绪状态,等待操作系统底层的调用
当前线程sleep()方法结束,其他线程join()结束,某个线程拿到对象锁,这些线程也将进入就绪状态
BLOCKED(阻塞状态)
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态
WAITING(等待状态)
处于这种状态的线程不会被分配CPU执行时间
它们要等待被显式地唤醒,否则会处于无限期等待的状态(傻傻的等)
TIMED-WAITINT(超时等待状态)
处于这种状态的线程不会被分配CPU执行时间
不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒(过时不候)
TERMINAED(终止状态)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常
线程状态变迁
Daemon线程(守护线程)
主要被用作程序中后台调度以及支持性工作
当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出
Thread.setDaemon(true)将线程设置为Daemon线程
消费者和生产者
消费者遵循如下原则
获取对象的锁
如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
条件满足则执行对应的逻辑
条件满足则执行对应的逻辑
生产者遵循如下原则
获得对象的锁
改变条件
通知所有等待在对象上的线程
对应的伪代码
JAVA中的锁
Lock
ReentrentLock(可重入锁)
默认是非公平锁(可插队)
new ReentrentLock(true)可变公平锁,效率较低
aks
ReadLock
WriteLock
和Synchronized区别
Synchronized是内置java关键字,Lock是一个java类
Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
Synchronized会自动释放锁,Lock必须要手动释放锁,不释放会进入死锁
Synchronized 线程1(获得锁 阻塞) 线程2(等待 傻傻的等);Lock不一定会等待下去
Synchronized 可重入锁,不可中断,非公平;
Lock 可重入锁,可以判断锁,非公平(可以自己设置)
Lock 可重入锁,可以判断锁,非公平(可以自己设置)
Synchronized 适合锁少量的代码同步问题;
Lock适合锁大量的同步代码
Lock适合锁大量的同步代码
Condition
任何一个新的技术,不仅能解决老技术的问题,还能解决老技术不能解决的问题!
精准通知和唤醒线程
生产者和消费者问题
3个线程按顺序打印ABC
Synchronized
Java1.6为Synchronized做了优化,为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”
如何判断锁的是谁
锁多个static修饰的同步方法,锁的是同一个对象class
静态同步方法,锁的是当前类的Class对象
普通的同步方法,锁的是当前实例对象
对于同步方法块,锁是Synchonized括号里配置的对象
Java对象头
synchronized用的锁是存在Java对象头里的
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位
32位JVM的Mark Word的默认存储结构
锁的升级与对比
锁一共有4种状态,级别从低到高依次是
无锁状态
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
获得锁过程
偏向锁的撤销
获得和撤销流程
在Java 6和Java 7里是默认启用的
关闭偏向锁
-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态
会存储当前线程id,所以不需要cas,只需要简单判断一下是否当前线程
对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁
此时Mark Word 的结构也变为偏向锁结构
轻量级锁
轻量级锁加锁
轻量级锁解锁
适用于线程交替执行同步块的场合
此时Mark Word 的结构也变为轻量级锁的结构
重量级锁
如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁
一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态
当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住
争夺锁导致的锁膨胀流程图
锁可以升级但不能降级
目的是为了提高获得锁和释放锁的效率
不适合流量峰值不稳定的场景(如滴滴,外卖 都有高峰期)
锁的优缺点对比
synchronized可以修饰方法或者以同步块的形式来进行使用;
主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性
主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性
底层原理
synchronized代码块底层原理
使用的是monitorenter 和 monitorexit 指令
monitorenter指令指向同步代码块的开始位置
monitorexit指令则指明同步代码块的结束位置
synchronized方法底层原理
同步方法 不是通过monitor指令,而是通过ACC_SYNCHORNIZED关键字,判断方法是否同步
可重入锁
synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1
monitor 存在于对象头的Mark Word 中(存储monitor引用指针)
等待唤醒机制
sleep
只让线程休眠并不释放锁
wait
会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行
notify/notifyAll
方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁
CAS(Compare And Swap)
乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
3个基本操作数:内存地址V,旧的预期值A,要修改的新值B
更新变量
只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B
缺点
循环时间长开销很大
CAS 通常是配合无限循环一起使用的,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销
多变量原子问题
当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。
通过以下两种办法来解决:1)使用互斥锁来保证原子性;2)将多个变量封装成对象,通过 AtomicReference 来保证原子性
ABA问题
java并发包中提供了一个带有标记的原子引用类AtomicStampedReference
通过控制变量值的版本号来保证CAS的正确性,比较两个值的引用是否一致,如果一致,才会设置新值
在java.util.concurrent.atomic包中,Java为我们提供了很多方便的原子类型,它们底层完全基于CAS操作
例
集合类
ArrayList
java.util.ConcurrentModificationException 并发修改异常
解决方案
List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
Set
不安全
new HashSet<>()
底层是new HashMap()
解决方案
Set<String> set = Collections.synchronizedSet(new HashSet<>());
map
new HashMap()等价于new HashMap<>(16,0.75)
ConcurrentHashMap的原理
Callable
可以有返回值
可以抛出异常
方法不同,run()/call()
FutureTask(适配类)
常用的辅助类
CountDownLatch(计数器)
假如房间有6个人,只有当这6个人都出来之后,才能执行这个关门操作;demo
原理
countDownLatch.countDown(); // 数量-1
countDownLatch.await(); // 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续
执行
执行
允许一个或多个线程等待其他线程完成操作
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响
CyclicBarrier(加法计数器)
假如聚餐吃饭,先到的人都需要进入等待,
只有当10个人都到齐之后才能进行吃饭;demo
只有当10个人都到齐之后才能进行吃饭;demo
调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞
CountDownLatch与CyclicBarrier区别
Semaphore(信号量)
所有资源都被占用之后,其他线程只能等待,直到有线程释放资源;例如抢车位demo
原理
semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!
semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用
多个共享资源互斥的使用!并发限流,控制最大的线程数!
ReadWriteLock(读写锁)
实现类ReentrantReadWriteLock
读可以被多个线程同时读
写的时候只能有一个线程写
自定义缓存demo
并发问题
synchronized和ReentractLock的原理和区别
区别和应用场景
哪些框架和源码用了
从硬件层面到java层面的区别
wait,notify,condition await() signal()
锁的原理
偏向锁/轻量级锁/重量级锁的原理
能否从偏向锁直接升级成重量级锁
juc包里有哪些类,如何使用
线程池原理和参数配置
多线程的线程数的设置
volatile原理
threadlocal原理和使用
CAS的理解,和它存在的问题
ConcurrentHashMap的锁机制
Synchronized是如何实现锁的
修饰方法
JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
synchronized修饰的方法并没有monitorenter指令和monitorexit指令
修饰代码块
使用的是monitorenter 和 monitorexit 指令
面试问题
AQS
队列同步器 AbstractQueuedSynchronizer
主要使用方式是继承
同步器是实现锁(也可以是任意同步组件)的关键
锁是面向使用者的,它定义了使用者与锁交
互的接口(比如可以允许两个线程并行访问),隐藏了实现细节
互的接口(比如可以允许两个线程并行访问),隐藏了实现细节
同步器面向的是锁的实现者,它简化了锁的实现方式,
屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作
屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作
锁和同步器很好地隔离了使用者和实现者所需关注的领域
接口与示例
同步器的设计是基于模板方法模式的
使用者需要继承同步器并重写指定的方法,
随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法
随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法
如下3个方法来访问或修改同步状态
compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
setState(int newState):设置当前同步状态
getState():获取当前同步状态
提供了一个volatile修饰的状态变量和一个双向的同步队列。提供模板方法对于独占锁和共享锁的获取和释放,至于公平锁和非公平锁是它的实现类去覆盖抽象方法做的事情,和AQS无关
核心是被volatile修饰的state+双向链表Node+Condition单向队列
0 条评论
下一页