内容摘录自《并发编程之美》
线程的基础问题
线程的创建
创建线程有三种方式
线程的名字可以是入参的第二个,可以是String
继承Thread类
需要重写run()方法,然后在实例化后调用对象名.start()
来启动线程。
这里提到一个注意的点,start()方法调用后只是处于就绪状态,当cpu资源分配了之后才执行run(),执行完了之后线程就终止了。
- 好处:run()方法内可以直接使用this获取到对象。传参方便。
- 缺点:不可继承其他类
实现Runnable接口
thread.start()
是Thread的类方法,此类要调用来new Thread(Runnable thread).start();
实现runnable接口,重写run方法
- 好处:多个线程可以共用一个task逻辑
- 缺点:没有返回值
实现Callable接口
流程:
- 实现callable接口,重写call方法
- 在主方法内,
new FutureTask(new callObject());
建立任务对象 new Thread(futureTask).start()
调用start方法futureTask.get()
返回线程的返回结果
线程的一些方法
wait()
Object类的通知和等待方法
某个线程,调用共享变量的wait()方法时,该线程会被吊起进入阻塞状态且释放锁(该线程持有的该共享变量的锁)。
唤醒阻塞的线程:
1、其他线程调用共享变量的notify()
或notifyAll()
2、其他线程调用阻塞线程的interrupt()
,阻塞线程抛出异常返回
在调用此方法前,需要先获取到对象的监视器锁,不然会抛出异常
1、synchronized
修饰共享变量为入参的代码块
2、synchronized
修饰共享变量的方法
为了避免非通知行为的虚假唤醒,通常会在循环中调用wait
synchronized (obj) {
while (!express) {//条件不满足
//进入代码体则此时已经获取到了obj的锁
obj.wait();//释放锁进入阻塞状态
}
}
notify()
Object类的通知和等待方法
某个线程调用了共享变量上的notify()
则会唤醒在此共享变量上阻塞的一个线程,唤醒谁是随机的
notifyAll()
Object类的通知和等待方法
全部唤醒
join()
此方法是Thread的类方法,无参,无返回
作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在**子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
sleep()
此方法是Thread的类方法,无参,无返回
暂时让出CPU的执行权,但是不会释放监视器资源(锁),睡眠时间过了之后进入就绪状态
sleep()
Thread的类方法
暗示CPU自己的权限可以不用了,请求直接开始下一次资源分配。
interrupt()
Thread的类方法
上下文切换
CPU是时间片轮转分配给线程资源的。
一个线程的时间片资源消耗完之后就会进入就绪状态,让出CPU资源给别的线程使用,这就是上下文切换。
除了CPU资源使用完的情况,被其他线程中断也会产生上下文切换。
死锁
多个线程因争夺资源造成的互相等待的现象。
产生死锁的四个基本条件:
- 资源互斥
- 资源不可剥夺
- 保持请求
- 形成环路
如何避免线程死锁:
破坏保持请求和形成环路两种条件
核心在于,梳理资源的有序性申请。
线程种类
守护线程(daemon):
JVM启动的时候除了main函数所在的用户线程,还会启动包括GC线程之类的守护线程。
设置守护线程的方法:
1、实例化一个线程对象
2、对象调用setDaemon(true)
用户线程(user)
JVM启动的时候调用的main函数所在的就是一个用户线程。
二者之间的区别:最后一个用户线程结束的时候JVM会退出。不考虑守护线程的情况。
ThreadLocal
多个线程在访问一个共享变量的时候,容易产生并发问题。比如都要进行写入的时候。为了保证线程安全,会在访问变量的时候进行适当的同步。
同步措施一般是加锁,但是加锁的资源开销很不好控制。
ThreadLocal
提供了线程的本地变量,当你创建一个ThreadLocal
变量,每个访问这个变量的线程都会有一个此变量的本地副本。这样他们操作的都是自己本地的量了。
常见方法
set(value)
,get()
,remove()
,清楚当前线程的本地变量
底层实现
ThreadLocalMap
一个定制化的HashMap
,为了一个线程可以关联多个本地变量
并发编程
并发和并行
**并发:**一起发生,一起结束。
**并行:**同时运行
共享变量的内存可见性
Java内存模型规定,所有的变量都存放在主内存种,线程使用变量的时候会把主存的变量复制到本地工作空间,读写操作都是自己的本地变量。
当一个线程修改的值,对另一个线程不可见的时候(比如另一个线程的缓存有之前的值),这就是共享变量的内存不可见
synchronized关键字
简单介绍,synchronized是Java提供的一种原子性的内置锁,每个对象都可以将之当成同步锁来使用。这类Java内置的使用者看不到的锁内成为内部锁或者监视器锁。
线程的使用效果,线程的执行代码进入synchronized代码块前会获取锁,别的线程访问这个代码块就会被阻塞挂起。拿到锁的线程在正常退出或者抛出异常或者调用wait方法后会释放锁。这个锁是排他锁。Java的线程是和OS的线程对应的,所以OS要执行阻塞操作就要从用户态切换成内核态,开销是非常大的。
内存语义,synchronized块会把块内使用到的变量从线程的工作空间抹除,新读取主存的值,退出的时候刷新主存中的值。
**作用,**这样的加锁和释放锁的操作就解决了共享变量内存可见性问题。除此外通常还能被用来实现原子性操作。
volatile关键字
作用,当一个变量被声明为volatile变量时,线程在写入变量时,会直接刷新回主存内,而不是缓存中或工作空间内。其他线程在读取的时候也会从主存内读取而不是缓存或工作空间内。
虽然和synchronized完成的功能类似,但是这是非阻塞算法,不会强行挂起其他的线程。但是它只保证了可见性而不保证原子性。
原子性操作
**定义,**执行一系列操作时,要不全执行,要不全部不执行,不存在执行一部分的情况。
**缺陷,**synchronized可以实现线程安全性(内存可见性和原子性),但是他是独占锁,是阻塞性的算法。类似于读操作其实不会引起线程安全问题,会极大的降低并发性。但是直接去掉独占锁就不能保证可见性了。
CAS
操作
作用,因为synchronized的带来的阻塞性问题,volatile只能保证一个变量的可见性问题,没办法保证读写的原子性。所以通过CAS
(compare ans swap)操作来保证更新操作的原子性。
定义,CAS
是JDK
的unsafe类提供的非阻塞原子性操作。通过硬件保证比较更新操作的原子性。
**操作,**有四个操作数:对象的内存位置,对象变量的偏移量,变量预期值,新的值。如果对象偏移量等于预期值,那么就用新的值替换预期值。
**缺陷,**ABA问题,线程2,在线程1比较后更新前,先一步修改这个值的话就会导致问题。解决方法就是加一个时间戳,用来保证变量的值只能往一个方向转换。
unsafe类
提供了很多方法
伪共享
**定义,**缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
修改方法,JDK8
之前通过字节填充解决,即创建一个变量的时候使用字段填充此缓存行,避免将在多个变量存放在同一个缓存中。JDK8
之后提供了一个注解@Contended
锁的种类
1.乐观锁悲观锁通常是数据库角度引入的名词
**乐观锁:**认为数据不容易被别的线程修改,不会主动对数据加锁。只在数据提交的时候检测是否会出现冲突。
**悲观锁:**认为数据很容易被别的线程修改,所以数据处理前要先加锁。处理数据就要加排他锁,加不上就说明数据已经被拿了
2.线程抢占机制
**公平锁:**线程获取锁的顺序是根据请求时间排序的。
**非公平锁:**会在运行时闯入,不是先来先得
3.锁的排他性
**独占锁:**任何时候只能有一个线程能得到锁
**共享锁:**可以索格线程同时持有,比如可以多个线程同时读
4、阻塞过的线程再获取
**可重入锁:**一个线程获取已经被持有的独占锁时会被阻塞,而当已经拥有锁的线程再次获得已经得到的锁不被阻塞。
synchronized就是可重入锁,内部关联计数器,获得锁就+1,释放锁就-1,0的死后锁内的线程标识会被重置,其他线程会被唤醒竞争。
5、反复申请
**自旋锁:**申请的锁已经被占有不立刻阻塞,而是在不放弃CPU资源的情况下多次尝试获取。当默认指定次数结束后才会阻塞。
普通的锁一个线程尝试申请锁失败后,会被切换到内核态并被挂起,获得到锁的时候又要切换到内核态唤醒。反复的上下文切换开销巨大。
ThreadLocalRandom
**原因,**每个Random
实例对象里面都会有一个原子性的种子变量来记录当前的种子值,每当生成新的随机数的时候要根据当前的种子计算新的种子并更新原子变量,但是因为多个线程计算随机数计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新操作时CAS
操作,所以只会有一个线程成功,其他线程自选,会降低性能。
**解决方法,**原理其实和ThreadLocal
一样,在自己的本地空间维护一个种子
线程安全的数据结构
CopyOnWriteArrayList
ArrayList
的线程安全版本,修稿操作都是在数组快照(赋值的)上操作的。底层时一个数组存,使用ReentrantLock
独占锁保证只有一个对象对他修改。
AQS
AQS(AbstractQueuedSynchronizer)
抽象同步队列,是锁的底层支持。本质是一个双向队列
线程池
ThreadPoolExecutor
线程池解决的问题是主要有两个:
第一:当执行大量异步任务的时候,线程池能提供较好的性能,不使用的时候,每次执行异步操作都要new一个线程,线程的生命周期的两端都是资源的消耗。而线程池中的线程是可复用的。
第二:线程池提供了资源限制和管理的手段,比如限制线程数量,动态新增线程等
线程池的状态
RUNNING
,接受新任务且处理阻塞队列中的任务SHUTDOWN
,拒绝新任务,但是处理阻塞队列中的任务STOP
,拒绝新任务,中断正在处理的任务TIDYING
,所有任务都执行完后,当前线程池中活动线程为0,将要调用terminatedTERMINATED
,终止状态。调用terminated方法
状态之间的转换
running->shutdown
,调用shutdown()
running/shutdown->stop
,调用shutdownNow()
shutdown->tidying
,线程池和阻塞队列为空的时候
stop->tidying
,线程池为空
tidying->terminated
,terminated()
线程池的参数
七个参数,很重要
corePoolSize
,池中核心线程的数量workQueue
,阻塞队列maximunPoolSize
,线程池最大的线程数量ThreadFactory
,创建线程的工厂RejectedExecutionHandler
,包和策略,数量到上限的时候怎么处理(抛出异常、丢弃任务还是怎么样)keeyAliveTime
,存活时间,线程数量大于核心线程数且闲置,则处理存活时间TimeUnit
,存活时间的时间单位
线程池的类型
三种线程池的类型,很重要
newFixedThreadPool
创建一个线程池:
核心线程个数和最大线程个数都是nThreads
的,
阻塞队列长度是Integer.MAX_VALUE,
keepAliveTime
= 0,只要有闲置的就会瘦
newSingleTHreadExecutor
创建一个线程池:
核心线程个数和最大线程个数都是1
的,
阻塞队列长度是Integer.MAX_VALUE,
keepAliveTime
= 0,只要有闲置的就会瘦
newCachedThreadPool
创建一个线程池:
初始线程数是0,最大线程数是Integer.MAX_VALUE,
阻塞队列长度是1
,同步队列里面只能最多有一个任务
keepAliveTime
= 60,60s后限制回收
内存泄漏
如果线程池内设置了ThreadLocal
变量,要定时清理,因为核心线程是始终存在的,不清理的话核心线程的threadLocals
变量会始终持有ThreadLocal
变量
TaskFuture
对象
某些包和策略下,被拒绝的任务的taskFuture.get()
会导致线程一直阻塞