赞
踩
程序由指令
和数据
组成,这些指令
要运行,数据要读写,就必须将指令加载至 CPU
,数据
加载至内存
。
当一个程序被运行,从磁盘加载这个
程序的代码至内存
,这时就开启了一个进程。
二者对比
进程中包含了线程
,每个线程执行不同的任务
不同的进程使用不同的内存空间
,在当前进程下的所有线程
可以共享内存空间
线程上下文切换
成本一般上要比进程上下文切换低
(上下文切换指的是从一个线程切换到另一个线程)现在都是多核CPU,在多核CPU下
并发
是同一时间
应对多件事情的能力
,多个线程
轮流使用一个或多个CPU
并行
是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
继承Thread类
实现Callable接口
线程池创建线程(项目常用)
参考回答:
没有返回值
call方法有返回值
,是个泛型
,和Future、FutureTask
配合可以用来获取异步执行的结果
允许抛出异常
;而Runnable接口的run()方法的异常只能在内部消化
(try catch),不能继续上抛run 是一个普通方法
,而 start 是开启新线程的方法
。run 方法
会立即
执行任务,调用 start 方法
是将线程的状态
改为就绪
状态,不会立即执行。(调用start()方法的时候线程不是立即执行,而是进入到一个等待状态,等待CPU进行调度
)不能被重复调用的原因
是,线程的状态是不可逆的
,Thread 在 start的实现源码中做了判断,如果线程不是新建状态 NEW
,则会抛出非法线程状态异常
IllegalThreadStateException。 新建(NEW)、
可运行(RUNNABLE)、
阻塞(BLOCKED)、
等待( WAITING )、
时间等待(TIMED_WALTING)、
终止(TERMINATED)
新建状态
可执行状态
终止状态
可执行状态
的过程中,如果没有
获取CPU的执行权
,可能会切换其他状态
没有获取锁
(synchronized或lock)进入阻塞状态
,获得锁再切换为可执行状态其他线程调用notify()唤醒后
可切换为可执行状态计时等待状态
,到时间后可切换为可执行状态可以使用线程中的join方法解决
t.join()
阻塞调用此方法的线程进入timed_waiting
直到线程t执行完成后
,此线程再继续执行
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个
wait 线程
共同点
wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权
,进入阻塞状态
不同点
不同点
sleep(long) 是 Thread 的静态方法
而 wait(),wait(long) 都是 Object 的成员方法
,每个对象都有
执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
wait(long) 和 wait() 还可以被 notify 唤醒
,wait() 如果不唤醒
就一直等下去
它们都可以被打断唤醒
wait 方法的调用必须先获取 wait 对象的锁
,而 sleep 则无此限制
wait 方法执行后会释放对象锁
,允许其它线程获得该对象锁
(我放弃 cpu,但你们还可以用)
也就是只要执行到
wait()方法不管wait方法有没有执行完
都会立即释放锁
,别的线程就可以拿到锁进而执行自己的业务。
而 sleep 如果在 synchronized 代码块中执行
,并不会释放对象锁
(我放弃 cpu,你们也用不了)
也就是
只有sleep()方法执行结束了
,才会去释放锁
,别的线程才能拿到锁,执行自己的业务
有三种方式可以停止线程
也就是给线程配置一个退出运行状态标志,能让run方法执行完成后正常退出
使用stop方法强行终止(不推荐,方法已作废)
使用interrupt方法中断线程(原理和第一种是一样的)
打断阻塞的线程(
sleep,wait,join
)的线程,线程会抛出InterruptedException异常
打断正常的线程,可以根据打断状态
来标记是否退出线程
current.isInterrupted()默认是false
只有在线程调用interrupt();方法
才会将current.isInterrupted()设置为true
Synchronized【对象锁】采用互斥的方式
让同一时刻至多只有一个线程能持有【对象锁】
,其它线程
再想获取这个【对象锁】时就会阻塞
住.
synchronized锁是基于monitor锁实现的
例如这一段代码加锁之后进行反编译查看class字节码信息:
synchronized锁
修饰方法
和代码块
时底层实现上是一样的,但是在修饰方法时
,不需要JVM编译出的字节码完成加锁操作
,而synchronized在修饰代码块时
,是通过编译出来的字节码生成的monitorenter和monitorexit指令来实现的。
Monitor(监视器):结构包括三部分
Monitor 被翻译为监视器,是由jvm提供,c++语言实现
存储当前获取锁的线程的
,只能有一个线程可以获取没有抢到锁
的线程,处于Blocked状态的线程
wait方法
的线程,处于Waiting状态的线程JMM(Java Memory Model)Java内存模型
,定义了共享内存
中多线程程序读写操作的行为规范
,通过这些规则来规范对内存的读写操作
从而保证指令的正确性
通俗的来说,就是
保证不同的线程对共享内存的值进行改变是的透明的
(也就是当两个线程同时拿到共享内存的一个变量,其中一个线程对变量进行了更改,那么此时就会和共享内存做一个同步操作,然后共享内存会对所有拿到这个变量的线程做出一个同步操作)
总结:
多线程程序读写
操作的行为规范,保证读写指令的正确性线程的工作区域(工作内存
),一块是所有线程的共享区域(主内存)
相互隔离
,线程跟线程交互需要通过主内存
CAS的全称是: Compare And Swap(比较再交换
),它体现的一种乐观锁
的思想,在无锁情况
下保证线程操作共享数据的原子性。
举例:【Java】CAS数据交换流程
CAS 底层实现
依赖于一个 Unsafe 类
来直接调用操作系统底层的 CAS 指令
乐观锁和悲观锁
乐观锁的思想
:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,改了反正要同步到主内存的,别的线程CAS失败可以进行自旋在拷贝一份主内存的共享变量数据,再执行自己的业务。悲观锁的思想
:最悲观的估计,得防着其它线程来修改共享变量
,我上了锁你们都别想改,我改完了解开锁,你们才有机会。总结:
CAS缺点:
ABA问题
:如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了
。
ABA问题的解决思路就是使用版本号。
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
循环时间长开销大
:如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大
且没必要的开销。
可以破坏掉for死循环,当超过一定时间或者一定次数时
,return退出。
保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时
,循环CAS就无法保证操作的原子性
,这个时候就可以用锁
,或者有一个取巧的办法,就是把多个共享变量合并
成一个共享变量来操作。
一旦一个共享变量
(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:.
用 volatile 修饰共享变量,能够防止编译器等优化发生
,让一个线程对共享变量的修改
对另一个线程可见
例如下面的代码:
解决方案一(不推荐
):在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)
解决方案二:在修饰stop变量的时候加上volatile
,当前告诉 jit
,不要对 volatile 修饰的变量做优化
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障
,阻止其他读写操作越过屏障
,从而达到阻止重排序的效果
例如下面的代码在并发环境中,由于存在多个线程同时访问共享变量的情况,可能会导致可见性问题和指令重排
等影响程序正确性的行为。
解决办法就是在变量上添加volatile
,禁止指令重排序
,则可以解决问题
当然也不能随便加volatile关键字,要根据实际情况去加。如果都禁用了,指定效率肯定不高。
全称是 AbstractQueuedSynchronizer,即抽象队列同步器
。它是构建锁或者其他同步组件的基础框架
所谓抽象,其实目的就是把具体的逻辑交给子类去实现,这样就可以实现不同的特性的锁:
例如:AQS常见的实现类
ReentrantLock阻塞式锁
Semaphore信号量
CountDownLatch倒计时锁
AQS内部维护了一个先进先出的双向队列
,队列中存储的排队的线程
在AQS内部还有一个属性state
,这个state就相当于是一个资源
,默认是0
(无锁状态),如果队列中的有一个线程修改成功了state为1
,则当前线程就相等于获取了资源
在对state修改的时候使用的cas操作
,保证多个线程修改的情况下原子性
参考链接:【Java并发】什么是AQS?
ReentrantLock表示支持重新进入的锁
,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞
ReentrantLock主要利用CAS+AQS队列
来实现
支持公平锁和非公平锁
,在提供的构造器的中无参默认是非公平锁
,也可以传参设置为公平锁
构造方法接受一个可选的·
公平参数(默认非公平锁
),当设置为true时
,表示公平锁
,否则为非公平锁
。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
参考链接:【Java并发】ReentrantLock的实现原理
语法层面
synchronized 是关键字
,源码在 jvm 中,用 c++ 语言实现
Lock 是接口,源码由 jdk 提供,用 java 语言实现
(API)
使用 synchronized 时,退出同步代码块锁会自动释放
,而使用 Lock 时,需要手动调用 unlock 方法释放锁
功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如
公平锁(参考ReentrantLock)
可打断
可超时
也就是使用tryLock()时加入时间参数,如果超过这个时间拿不到锁,就自动放弃抢锁(放弃阻塞),反之在规定时间能抢到锁,那就正常执行自己的逻辑
多条件变量
Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)
性能层面
在没有竞争时
,synchronized 做了很多优化
,如偏向锁、轻量级锁,性能不赖
在竞争激烈时
,Lock 的实现通常会提供更好的性能
产生的四个必要条件如下:(缺一不可)
互斥条件
:一个资源同一时间能且只能被一个线程访问;不可掠夺
:当资源被一个线程占用时,其他线程不可抢夺该资源;请求与等待
:当资源被一个线程占用时,其他线程只能等待资源的释放再拥有;循环等待
:指的是若干线程形成头尾相接的情况,将所有资源都占用导致的整体死锁或局部死锁。一个线程需要同时获取多把锁,这时就容易发生死锁
前三条其实就是作为锁的条件,第四条(循环等待)就是造成死锁的主要原因
循环等待
也就是双方的锁都锁住了对方,并且都在等待对方的解锁,造成死循环(类似springbean的循环依赖)
如何进行死锁诊断?
参考链接:【Java并发】如何进行死锁诊断?
ConcurrentHashMap 是一种线程安全
的高效Map集合
底层数据结构:
分段数组+链表
实现数组+链表/红黑二叉树。
JDK1.7中ConcurrentHashMap:
采用分段数组+链表
实现
Segment数组的每一个元素都存储这一个HashEntry 数组的地址值,并且segment数组定义好了就不能扩容了。
向ConcurrentHashMap添加元素的流程
底层使用了ReentrantLock锁
保证并发下的线程安全。
但是这种方式效率并不高,每一次添加元素进去都要枷锁,解锁。效率不高
JDK1.8中ConcurrentHashMap
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表
控制数组节点的添加
(CAS操作保证一个共享变量的原子操作
)只锁定当前链表
或红黑二叉树的首节点
,只要hash不冲突,就不会产生并发的问题 , 效率得到提升1.7
底层采用分段的数组+链表
实现1.8
采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
Segment分段锁
,底层使用的是ReentrantLock
CAS添加新节点
,采用synchronized锁定链表
或红黑二叉树的首节点
,相对Segment分段锁粒度更细
,性能更好
Java并发编程三大特性
1. 原子性(加锁)
一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
可见性(共享变量加volatile关键字)
内存可见性:让一个线程对共享变量的修改对另一个线程可见
有序性(共享变量加volatile关键字)—会在读、写共享变量时加入不同的屏障
指令重排:指令重排虽然在单个线程内保持了语义一致性,但在多线程环境下,并发读写操作的顺序可能被改变,从而引发问题。例如,在多线程中对共享变量进行读取和写入操作时,如果指令重排改变了读取和写入的顺序,并且没有适当的同步机制来保证顺序性,就可能导致线程读取到失效的数据或产生不符合预期的结果。
资源管理:线程池可以有效地管理系统中的线程资源。线程创建和销毁的开销比较大,如果在每个任务执行时都手动创建和销毁线程,会产生较高的开销。而线程池可以在程序启动时预先创建一定数量的线程,并对其进行复用,避免了线程频繁的创建和销毁,从而提高了系统性能和资源利用率。
控制线程数量:线程池可以限制同时执行的线程数量,可以通过设置线程池的大小来控制并发度。这样可以避免线程数量过多导致系统负载过重,以及线程数量过少导致资源浪费。线程池会自动管理线程的调度和执行,保证线程数量在合理范围内。
提高响应速度:线程池可以提高任务的响应速度。当有新的任务到达时,线程池中的空闲线程可以立即执行任务,而不需要等待新线程的创建。
这样可以降低任务的等待时间,提高整体的响应性能。
避免资源竞争:线程池可以避免由于资源竞争而引起的性能问题。线程池可以通过适当的同步机制来管理共享资源的访问,避免多个线程同时对共享资源进行修改而导致的竞争和冲突。
统一管理和监控:线程池可以提供统一的管理和监控接口,方便对线程的状态、执行情况、异常处理等进行统一管理和监控。可以通过线程池的API来获取线程池中线程的状态或取消执行中的任务等操作。
生存时间内没有新任务,此线程资源会释放
没有空闲核心线程
时,新来任务会加入
到此队列排队
,队列满会创建救急线程执行任务
1.AbortPolicy:直接抛出异常,
默认策略
;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;(调用主线程来完成任务)
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前(待在队列最久的任务)的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
线程池线程执行流程:
workQueue - 当没有空闲核心线程时
,新来任务会加入到此队列排队
,队列满会创建救急线程执行任务
阻塞队列有四种:(1,2常用,3,4了解即可)
1.ArrayBlockingQueue:基于数组结构
的有界阻塞队列,FIFO(先进先出)。
2.LinkedBlockingQueue:基于链表结构
的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
ArrayBlockingQueue的LinkedBlockingQueue区别(其实也就是数组和链表的区别以及有界无界和出入队列锁的数量不同
)
注意:
CPU核数指的是物理核心的数量,而线程数包括物理核心和虚拟核心的总数。在多核处理器中,线程数通常等于核心数,而在带有超线程技术的处理器中,线程数可以大于核心数。
① 高并发、任务执行时间短
( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
IO密集型的任务 (CPU核数 * 2 + 1)(java程序通常是这种)
计算密集型任务 ( CPU核数+1 )
③ 并发高、业务执行时间长
,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)
固定线程数
线程池):创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
无需救急,人人有份
,相当于KFC知道今天会有多少个顾客来点餐,那KFC事先备好餐品的数量。
单线程化
线程池):创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行适用于按照顺序执行的任务
KFC只有一个窗口,
排队取餐
,来一个人点完单不做,先滚后面排队去,到你了才给你做餐
可缓存
线程池):创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程KFC有0元领鸡腿活动,鸡腿做很快,来一个单先做好这个单的鸡腿,在接下一个单。并且做鸡腿的都是临时员工
相当于提前预定,到了预定时间就去做餐,或者接到单了,但是先不做,可以摸鱼晚点做
参考阿里开发手册《Java开发手册-嵩山版》
其实就是,使用Executors创建的线程池要么就是阻塞队列太长了,要么就是允许创建的线程数量最大化,都会导致堆积问题,导致堆内存溢出错误(OMM)
推荐使用(根据实际业务情况,定制化线程池)
使用 ThreadPoolExecutor
类来手动创建线程池
,并根据实际需求进行配置
,以更好地控制线程池的行为。通过自定义线程池的参数,例如核心线程数、最大线程数、队列容量和拒绝策略等,可以更好地适应不同的业务场景
,并避免上述潜在问题。
CountDownLatch原理:
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作
,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行
)
初始化等待计数值
等待计数归零
减一
例如 es数据批量导入
现在我需要将50w的数据从数据库同步到es索引库中,一次性读取数据肯定不行(oom异常
),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制
,就能避免一次性加载过多
,防止内存溢出
通俗的来说,比如要将数据库50w的数据导入到es索引库中,一次性导入肯定不行(omm),那么可以通过线程池搭配CountDownLatch来分批次导入es,提前固定每页条数2000条,计算50w的数据一共有多少页,设置CountDownLatch的计数值为总页数,然后从线程池拿到核心线程一页2000的数据(分批次)的导入到ES当中,然后循环处理,直到计数值减为0(页数),说明已经导入完成。
在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息
、包含的商品
、物流信息
;这三块信息都在不同的微服务
中进行实现的,我们如何完成这个业务呢?
一般的处理方式就是让一个线程去按顺序
查询订单、商品、物流信息,然后再封装到一起返回
这样存在一个问题就是耗时等于微服务执行总长。
若采用线程池,采用多线程异步的方式
,则耗时
就等于耗时最长的那一个微服务。
实际场景例如:报表汇总
四个模块代表四个微服务,可以通过线程池+future(线程中可以使用future对象的get方法可以得到每一个线程的返回结果)获得每一个微服务返回的数据,在整体封装到一起汇总返回
案例:
在搜索文章的时候,保存用户的历史搜索记录
这个时候,在搜索文章的时候,只需要正常返回文章数据,而保存用户历史搜索记录这个事情交给线程池异步开启一个线程去做。
所以在调用这个保存历史记录的方法,会在调用的时候,从线程池中开启一个线程去异步完成任务,
这样整体的效率会变的更好(用户在搜索的时候只关心搜索到的内容)
Semaphore 信号量,是JUC包下的一个工具类,底层是AQS
,我们可以通过其限制执行的线程数量
使用场景:
通常用于那些资源有明确访问数量限制的场景
,常用于限流
。
举例:停车场常只有3个车位,每停一辆车,车位-1,全部停满后,别的车就不能停了,只能别的车开走了,才能往里面停,也就达到限流的效果。
代码:
总结:
在多线程中提供了一个工具类Semaphore
,信号量
。在并发的情况下,可以控制方法的访问量
创建Semaphore对象,可以给一个容量
acquire()可以请求
一个信号量,这时候的信号量个数-1
release()释放
一个信号量,此时信号量个数+1
ThreadLocal是多线程中对于解决线程安全的一个操作类
,它会为每个线程都分配一个独立的线程副本
从而解决了变量并发访问冲突
的问题。ThreadLocal 同时实现了线程内的资源共享
,也就是ThreadLocal使得线程与线程产生了隔离,互不影响
实现原理和内存泄露问题参考链接:【Java并发】ThreadLocal的实现原理&源码解析
总结:
【资源对象】的线程隔离
,让每个线程
各用各的【资源对象】,避免争用引发的线程安全问题线程内的资源共享
存储资源对象
ThreadLocal 自己作为 key
,资源对象
作为 value
,放入当前线程的 ThreadLocalMap 集合
中ThreadLocal 自己作为 key
,到当前线程中查找
关联的资源值
ThreadLocal
自己作为 key
,移除
当前线程关联的资源值key 是弱引用
,值为强引用
; key 会被GC 释放内存
,关联 value 的内存并不会释放
。建议主动 remove
释放 key,value更新中-------------
参考来自黑马程序员
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。