Java
2024-09-25 11:07:10 3 举报
AI智能生成
并发的普通知识点
作者其他创作
大纲/内容
多线程
线程的实现和启动
线程的实现
实现 Runnable
避免了单继承的局限性,方便一个对象同时被多个线程使用
继承Thread类,其本质上是实现Runnable接口
实现Callable接口
使用线程池的概念,实现这个接口,重现的是的call方法,new Executors.newFixed...... 然后submit提交
使用线程池
线程启动方式
star方式启动是启动了线程并且将线程调整到了就绪态(这时只要获得时间片就可以启动了,然后再调用run的方法)
run的方法调用相当于main的线程调用了方法
sleep() 方法和 wait() 方法区
不同点
两者的主要区别:sleep方法没有释放锁,wait方法会先释放锁
wait必须在同步代码块中使用,但是sleep可以在任何地方使用
共同点
两者都会暂时停止线程的运行
wait常用于线程之间的交互和通信,sleep常用于线程的暂停
wait不能够自己唤醒,必须要进行notify或者notifyall来唤醒,sleep到时间了可以自己唤醒自己
synchronized和lock
区别
是否响应中断,lock中可以使用interrupt中断等待,但是synchronize只能等待获取锁
lock是一个接口synchronize是一个关键字
lock可以通过trylock来判断是否获取了锁,但是synchronize不能
synchronize在发生异常的时候会自己释放锁,但是lock不会自己释放锁
synchronized是可重入锁,不能中断是非公平锁;Lock是可重入锁,可以判断锁,公平性可以自由选择
synchronize适合少量的代码同步问题,lock适用于大量的代码同步问题
Lock
公平锁
十分公平,严格遵守先来后到的顺序
非公平锁(默认)
可以进行cpu的插队操作
死锁
死锁具备的条件
互斥条件:该资源在同一时刻只能有一个线程占用
请求与保持条件:请求资源时被阻塞同时保持自己的资源不放
不可被剥夺条件:自己获得的资源没用完之前不能被强行的剥夺掉
循环等待:多个资源之间形成一个环形的资源等待关系
如何避免死锁的发生(破坏上述至之一就行)
破坏互斥条件:这东西不能破坏
破坏请求与保持:直接一下子申请所有资源,不请求了
破坏不剥夺条件:自己要的资源申请不到了就直接摆烂,自己的也释放了
破坏循环等待条件:资源进行排序,按照次序进行访问
子主题
并发和并行、线程和进程
线程和进程
线程
cpu调度的最小单位
线程之间共享进程的资源
线程切换的消耗比较低
进程
操作系统分配资源的最小单位
一个进程可以含有多个线程
并发和并行
并发
在一个时间段内,交替执行
并行
同一时刻一起发生
Condition同步监视器
使用这个的优势在哪
可以使线程交替执行
Lock.new Condition可以产生
condition.await使线程进入等待的状态
condition.signalAll唤醒所有的线程
指定唤醒某一个线程的方法
new 多个Condition然后直接用new 出的Condition.signal()
生产者和消费者使用if判断可能会产生虚假唤醒的可能,使用while可以避免这种情况
锁谁的几种情况
普通方法不受锁的限制
synchronized锁的是对象的的调用者(new 的就算是一个)
锁的对象为static方法时,锁的时CLass
线程安全的集合类
copyOnWriteArrayList
逻辑为使用lock锁进行数组复制再插入
copyOnWriteArraySet
CooncurrentHashMap
Collections类中有一系列sync开头的方法转化为线程安全的
Calable
作用:用来替换Runnable的功能,重写的方法叫做call可以解决runnable没有返回类型的问题
Calable不能和Thread直接交互
解决办法
FutureTask为Runnable的一个实现类,Calable可以当做入参给FutureTask
获取返回值的办法
使用futureTask.get()可以的得到返回值
get的方法可能会产生阻塞,因为他要等待返回结果
常用的辅助类
用来计数的
CountDownLatch减法计数器
允许一个或多个线程等待直到其他下线程中的一组操作完成的同步辅助
CountDownLatch.coutDown()实现数量减一
CountDownLatch.await()等待计数器归零,再往下执行
CycllicBarrier加法计数器
new 是传入参数(多少次加法运算),一个线程(运算结束后执行该线程)
CycllicBarrier.await
Semaphore信号量
设置可以获得信号的线程数量
Semaphore.acquire()获得信号量
如果当前的信号量已经都被使用,线程进入等待
Semaphore.release()释放信号量,放到finally函数中
信号量+1,唤醒等待的线程
读写锁
ReentrantReadWriteLock是读写锁一个实现类,其中有一对关联锁,一个只用于读,一个只用于写
读可以被多线程同时读,写的时候只能够有一个线程去写
读可以被多线程同时读,写的时候只能够有一个线程去写
读锁
加锁:ReentrantReadWriteLock().readLock().lock()
解锁:ReentrantReadWriteLock().readLock().unlock()
写锁
加锁:ReentrantReadWriteLock().writeLock().Lock()
解锁:ReentrantReadWriteLock().writeLock().unLock()
Queue
含义’
Collection的子类,与List和set类似
子主题
实现类
阻塞队列BlockingQueue
ArrayBlockingQueue
同步队列SynchronizeQueue
写了只能等待拿出来后才能继续写
put向里面放元素 take从里面取元素
LinkedBlockingQueue
双端队列Deque
非阻塞的队列AbstractQueue
Stream流式运算
子主题
线程池
Executors
四种创建的方式
newSigleThreadExector
创建一个单线程的线程池,池中只有一个线程在直接,保证线程执行的顺序
newFixedThreadPool
创建一个固定大小的线程池,大小一旦到最大时就会保持不变
newCachedThreadPool
遇强则强,不对大小进行限制,是服务器最大的线程数
newScheduledThreadPool
定时周期的执行任务
为什么建议使用原生的创建线程池的方法
FixedThreadPool和SigleThreadExector请求队列的最大长度设置的长度过大,可能会造成OOM溢出的问题
CachedThreadPool和ScheduledThreadPool最大线程数设置的过大,可能回导致OOM异常
使用原生创建方式时的七个参数
核心线程数量(corePoolSize)
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。
最大线程的数量(maximumPoolSize)
线程池同时存在的最大线程数量,当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。
工作队列的长度(workQueue)
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务
空闲线程存活时间(keepAliveTime)
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
空闲线程存活时间单位(unit)
keepAliveTime的计量单位
线程的拒绝策略(handler)
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,应该怎么处理就是拒绝策略
线程工厂(threadFactory)
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
线程池的四种拒绝策略
AbortPolicy
是ThreadPoolExecutor的默认拒绝策略,当任务无法被提交给线程池时,会直接抛出RejectedExecutionException异常,适用于对任务提交失败要求敏感的场景,需要明确知道任务是否被接受并执行。
CallerRunsPolicy
当任务无法被提交给线程池时,会由提交任务的线程自己执行该任务,适用于对任务提交失败要求较低的场景,通过调用线程来执行任务,避免任务丢失。
DisCardolddesPolicy
当任务无法被提交给线程池时,会丢弃最早的一个任务,然后尝试再次提交,适用于对新任务优先级比较高的场景,可以丢弃旧的任务以保证及时处理新任务。
DisCardPolicy(默认的)
当任务无法被提交给线程池时,会直接丢弃该任务,没有任何提示或处理,适用于对任务提交失败不敏感的场景,对任务丢失没有特殊要求。
JMM
JMM
JMM时一种想想出来的线程内存关系模型 ,其中包含了大概四对操作
read
将主内存中的数据取出来
load
将read取出来的操作加载到工作内存中
use
从工作内存中的数据放到执行内存中使用
assign
将执行引擎运行后的结果放到工作内存中
write
将工作内存中的数据放写入回去
store
将写入的结果存储起来
volate
Java虚拟机提供的轻量级的同步机制
可见性
一旦被标记的变量发生改变后,立刻回显到主内存和其他线程的工作内存中去
不保证原子性
延申问题:如果不用synchronization如何保证原子性
使用automic(原子类)、底层的操作都是调用的native方法,直接在内存中进行操作
CAs
Atomic integer.compareand set
禁止指令重排
指令重排是什么
程序最终的执行顺序可能由于处理器在考虑了参数的依赖顺序之后进行重新排序的操作,虽然不会改变结果,但是有可能会调整代码的执行顺序
代码的最终执行是需要经过几个步骤的:元代码->编译器优化的重排->指令并行也可能进行重排->内存系统也会进行重排->执行
为何能禁止指令重排
cpu在执行数据时有一层内存屏障
CAS
用当前值和主内存中的值进行比较,如果是期望的则进行修改,如果不是期望的则不进行修改
JAVA8的一些东西
Lambda
语法注意的点
参数的类型可以省略
如果方法只有一个参数()可以省略
如果是无返回值的且只有一行代码{}也可以省略
如果有返回值且只有return语句的省略{}的同时,return也必须省掉
方法引用
语法
F1 f1=其他类::其他方法;
含义
相当于其他类的其他方法替换了F1中的方法
JVM
类加载的过程
类加载过程:加载、链接(验证、准备、解析)、初始化。这个过程是在类加载子系统完成的。
加载
一、通过类的全限定名获取该类的二进制字节流
二、将这个字节流的静态存储结构,转化为方法区的运行时数据结构。包括创建运行时常量池,将类常量池的部分符号引用放入运行时常量池
三、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。
二、将这个字节流的静态存储结构,转化为方法区的运行时数据结构。包括创建运行时常量池,将类常量池的部分符号引用放入运行时常量池
三、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。
链接
验证
确保代码符合JAVA虚拟机规范和安全约束。包括文件格式验证、元数据验证、字节码验证、符号引用验证。
准备
为类变量(即static变量)分配内存并赋零值。
准备内存(划分内存的方法)
指针碰撞
这种分配内存的方式是默认的分配方式,,比较适合内存比较规整的情况,相当于已经使用的内存和未使用的内存有一个指针,需要时就指针后移将数据放到里面
空闲列表
当Java堆中的内存不连续的时候就不能使用指针碰撞,虚拟就必须维护一个列表记录那些地址可以用,那些地址不能用
解决并发时内存争抢的问题
本地线程分配缓冲(TLAB)
给每个线程在内存空间中先分配一点点的空间,如果所使用的空间超过开始分配的则都搬迁到伊甸园区中
CAS+失败重试的方式
多个线程先试用CAS进行资源争抢争抢到的先用,没有争抢到的则重试继续争抢
解析
将方法区-运行时常量池内的符号引用(类的名字、成员名、标识符)转为直接引用(实际内存地址,不包含任何抽象信息,因此可以直接使用)。
初始化
类变量赋初值、执行静态语句块。
对象创建的过程
初始化值
为对象设置初始化的值,int设置为0、对象就设置为null啥的
对象头的设置
锁的状态表示:有没有被占用、hashCode啥的
init初始化方法
JVM的参数
-XX:DoEscapeAnalysis
防止逃逸分析
逃逸分析就是我在这方法中创建了一个对象,这个对象就在方法中用,或者没有return这个对象给其他的方法使用,这个对象的范围就没有超出这个方法,那么这个对象就不会在堆中进行创建
对象内存分配的方式
JVM给对象再堆中划分出一块确定大小的内存,因为对象所需的内存大小在加载的时候就能知道
内存是否规整
内存规整是指的已经使用的内存和未被使用的内存是分开的
指针碰撞(G1垃圾回收器是用的这种)
堆内存规整
分配的过程
在使用的过程中使用未被使用的内存中间有一个指针,分配内存时指针向未被使用的一侧移动需要分配的大小
空闲列表(CMS就是这样)
用一个空闲的列表来存储未被使用的内存信息,在使用时找到未被使用的空闲内存将对象放进去
缺点
基本上不会找到正合适的内存大小,会形成内存碎片
优点
在进行垃圾回收的时候,将对象回收不用向指针碰撞那样进行内存整理
JVM的垃圾回算法
标记清除算法
最基本的算法,先STW然后进行标记,标记完成之后在进行垃圾回收,会产生大量的内存碎片
标记压缩
解决标记清除会产生内存碎片的问题,在标记清除之后,再将剩余的对象进行压缩,使其内存连续。不造成内存的浪费
复制算法
将内存空间划分成两个同等大小的内存空间,然后将正在使用的对象复制到另一个空间中,将原空间中没有被引用的对象清理掉,这样不会形成内存碎片,但是总有一半是空的,会有空间浪费的现象
引用计数
维护着一个计数器,每个对象被引用一次就对该对象的引用进行+1的操作,引用放弃的时候就会进行减一的操作,如果计数器为0的时候进行GC处理
分代算法
分代收集算法是一种基于对象存活周期的垃圾回收算法。它将内存分为新生代和老生代两个区域。新生代通常包含大量新创建的对象,老生代包含长时间存活的对象。垃圾回收器根据不同代的特点采用不同的回收策略。新生代采用复制算法,老生代采用标记-压缩算法。这种算法能够提高垃圾回收的效率,减少不必要的内存清理。
老年代达到百分之八十或者九十就会进行垃圾回收
STW
标记垃圾的时候,会暂停程序先进行标记
三色标记法
优点
在垃圾回收的时候将SWT升级为并发标记
避免重复的标记,提高标记的速度
什么是三色标记算法
三色标记算法模型来讲是 白、灰、黑三种颜组成
白色
代表没有标记过的对象(及垃圾对象)
灰色
及该对象已经被标记过了但是下属对象没有被标记完成,GC的消除的对象就因该在这个里面
黑色
该对象已经被标记过了,并且其下属对象也已经被标记过了,及程序所需的对象
存在的问题
因为并发标记的时候程序还在跑,所以可能会出现漏标或者错标的情况
浮动垃圾
并发标记的时候,已经被标记了灰色或者黑色的对象被断开了引用,不能及时标记出来,在GC时就会出现遗漏
对象的漏标问题
在GC处理已经别标记为白色的数据时,白色的数据突然被引用不能被及时的标记出来,这样正在使用的对象就会被GC
CMS与G1两种回收器都是使用的三个标记法,针对这个问题CMS在增加引用的环节进行了处理,G1在删除的环节进行了处理
垃圾回收器
CMS
垃圾回收分为四步
初始标记
单线程运行,需要stop the word(让程序停止运行),标记GC Root能够直达的对象
并发标记
无停顿,和用户类型同时运行着,从第一个步骤中标记出的遍历整个对象图
重新标记
多线程同时运行,需要stop the word(让程序停止运行,标记并发标记阶段产生的对象)
并发清除
无停顿,和用户线程同时继续进行,清除掉标记阶段标记的死亡对象
G1
把连续的Java堆划分成多个大小相等的独立区域(Region)
每一个独立的区域都可以根据需要扮演新生代中的Eden空间,Survivor空间,或者老年代
垃圾回收期能根据每个Region扮演的角色不同采用不同的角色去处理
为什么分区
可以进行更加精密的控制
发生GC时可以直接拿到正在使用的伊甸园区进行处理,不需要像以前一样拿到整个的堆区进行处理
可以预测停顿的时间
可以设置SWT的市场,在这个时常内选择性的回收
内存碎片的控制
每个region都是使用指针碰撞的形式分配空间
为什么CMS不用内存碰撞
CMS不分这么小,每一次指针碰撞所需的时间都比较长
优先级处理
可以设置优先回收内存较大的区域
G1的垃圾回收的过程
初始标记
单线程运行,需要stop the word(让程序停止运行),标记GC Root能够直达的对象
并发标记
无停顿,和用户类型同时运行着,从第一个步骤中标记出的遍历整个对象图
最终标记
多线程同时运行,需要stop the word(让程序停止运行,标记并发标记阶段产生的对象),但是这个比重新标记的范围更小
筛选回收
将伊甸园中的不需要回收的对象重新整理处理,复制到空的Region中,再清理掉旧的内容
集合
Collection
ArrayList
简单概括其特点,有序可重复可为null,底层是数组结构,查找快但是增删比较慢,为了追求效率没有使用synchronized关键字进行修饰
自动扩容
插入数据是会进行长度判断,如果长度达到了初始长度则会调用nsureCapacity()进行自动扩容,一般情况下扩容时按照原来的1.5倍进行扩容,但是由于
扩容的操作是将原有的数据复制到信的数组中,这样付出的代价会比较高,如果数量较多的话最好先设置一个初始量
扩容的操作是将原有的数据复制到信的数组中,这样付出的代价会比较高,如果数量较多的话最好先设置一个初始量
LinkedList
简单概括其特点:有序可重复、底层为双向的链表、增删快但是查找比较慢、不是线程安全的,如果需要使用的话可以将其使用Collections.synchronizedList()进行修饰
双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。注意这里没有所谓的哑元,当链表为空的时候first和last都指向null。
Stack
Java里有一个叫做Stack的类,却没有叫做Queue的类(它是个接口名字)。当需要使用栈时,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque
Queue(这个不是类只是一个接口)
Queue接口继承自Collection接口
Map
HashMap
简单的概括其特点:key-value的形式,key可以为null但是不可以重复,如果放置重复的值时会将原来的值进行覆盖,存放的数据也没有顺序可言
7和8的差异
7采用的是头插法,8采用的是尾插法
HashMap的扩容机制
JDK1.7来说
扩容只扩容数组,先生成一个新的数组,通常是原来的两倍
遍历老数组上面每一个链表的每一个元素
取每个元素的key值,计算其在新数组上的位置
将链表中的属性添加到新的数组添加到新的数组中去
转移完成之后重新将数组赋值给原来的table属性
JDK1.8来说
生成新的数组
遍历老数组上的链表或者红黑树
value复制
链表时
如果是链表将链表上的每个元素重新计算下标
红黑树时
先遍历红黑树,计算出红黑树上每个元素对应的下标位置
统计下标位置元素的个数
超过八个就生成为一个红黑树,将根节点添加到数组对应的位置中去
没有超过八个就生成为一个链表,将链表的头节点添加到对应的位置中去
所有的转移完了把数组赋值到map的table属性中
HashMap的put方法
根据key通过hash算法与与运算算出数组的下标
数组下标位置是否为空
是
此时将key和value封装为一个对象放到这里
否
1.7中首先判断是否需要扩容,如果需要就扩容不需要就使用头插法插进去,然后生成实体
JDK1.8
先判断node是链表还是红黑树
链表
使用尾插法,先进行遍历,遍历时是否有value有就替换,插入后就判断是否需要转为红黑树
红黑树
将key和value封装为一个红黑树节点添加到红黑树中
上面的操作结束后会再次判断是否要进行扩容,不需要时就结束了
如何解决哈希冲突
处理hash冲突的四种方法
链式寻址
hash冲突后将他们放到一个坐标下的链表中
再哈希
再进行一次hash运算
开放寻址
寻找数组中下一个空的坐标将其方进去
公共区域
将所有产生冲突的数据都放到一个公共的空间中
扩容
hashmap为什么要进行扩容
hashMap如何进行扩容
扩容大小是原来的两倍(临界值=负载因子(0.75)*容量的大小)
HashMap的初始容量是16
hashmap和hashtable的区别
hashtable的底层是以数组加链表的形式(链表用来解决hash冲突的问题:链式寻址)初始容量是11
hashmap是JDK1.2引入的线程不安全,底层是以数组加链表的形式但是再1.8后做了一些优化,初始的容量时是16
优化的内容
数组的长度大于64和链表的长度大于8时都会由链表转化为红黑树
notify和notifyall的区别:notify在等待池中随机去唤醒一个,notifyAll就是唤醒线程池中所有的线程
锁
轻量级锁
种类
自旋锁
含义
不需要操作系统进行调度的锁
重量级锁
含义
在锁队列中等待的锁,如果需要执行则需要操作系统进行调度的锁
轻量级的锁一定比重量级的锁效率高吗?
不一定,轻量级进行自选需要占用CPU的资源,如果程序执行的时间较长或现线程较多的时候不如重量级的锁
0 条评论
下一页