《Java-线程-多线程-并发》
一、线程简述(百度百科)
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
对操作系统来说,资源分配的基本单位是进程,而调度的基本单位是线程。
Java是单线程编程语言,不主动创建线程,默认只有主线程。
二、线程-实现方式
1. 继承Thread类
Thread 类本质上是实现了Runnable 接口的一个实例,代表一个线程的实例。
MyThread extends Thread
2. 实现Runnable 接口
类已经extends 另一个类,就无法直接extends Thread,此时,可以实现一个Runnable 接口。
MyThread extends Thread
3. 线程池
线程的频繁创建与销毁比较浪费资源,因此引入线程池概念,即使用缓存的策略。
ExecutorService threadPool = Executors.newFixedThreadPool(10);
三、线程池-4种实现方式
线程池接口ExecutorService。
线程池核心类ThreadPoolExecutor,参数如下
corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。
maximumPoolSize:最大线程池大小。
keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。
unit:销毁时间单位。
workQueue:存储等待执行线程的工作队列。
threadFactory:创建线程的工厂,一般用默认即可。
handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。
1. newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒。
2. newCachedThreadPool
带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小。
调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。
3. newSingleThreadExecutor
单线程线程池,核心线程数和最大线程数均为1。
每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。
4. newScheduledThreadPool
调度线程池,即按一定的周期执行任务,即定时任务。
四、线程生命周期(状态)
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。
1. 新建状态(NEW)
new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM 为其分配 内存,并初始化其成员变量的值。
2. 就绪状态(RUNNABLE)
start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
3. 运行状态(RUNNING):
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状 态。
4. 阻塞状态(BLOCKED):
阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。
直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
1) 等待阻塞(o.wait->等待对) JVM 会把该线程放入等待队列(waitting queue) 中。
2) 同步阻塞(lock->锁池) JVM 会把该线程放入锁池(lock pool)中。
3) 其他阻塞(sleep/join) JVM 会把该线程置为阻塞状态。
5. 线程死亡(DEAD)
线程结束后就是线程死亡(DEAD)。
1) 正常结束 run()或call()方法执行完成,线程正常结束。
2) 异常结束 线程抛出一个未捕获的Exception 或Error。
3) 调用stop 调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁
(可使用Interrupt方法)
六、线程终止4种方式
1. 正常运行结束
程序运行结束,线程自动结束。
2. 使用退出标志退出线程
使用一个变量来控制循环,例如设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。
3. Interrupt 方法结束线程
1) 线程处于阻塞状态
先捕获InterruptedException 异常之后通过break 来跳出循环,才能正常结束run 方法。
2) 线程未处于阻塞状态
使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的。
4. stop方法终止线程
线程不安全,不推荐使用。
调用thread.stop()后创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。
因此导致了该线程所持有的所有锁的突然释放(不可控制),就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果。
七、线程基本方法
线程相关的基本方法有wait,notify,notifyAll,sleep,join,yield 等。
1. 线程等待(wait)
调用wait方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用wait()方法后,会释放对象的锁。
因此,wait 方法一般用在同步方法或同步代码块中。
2. 线程睡眠(sleep)
sleep 导致当前线程休眠,sleep 不会释放当前占有的锁,sleep(long)会导致线程进入TIMED-WATING 状态,而wait()方法会导致当前线程进入WATING 状态
3. 线程让步(yield
yield 会使当前线程让出CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。
4. 线程中断(interrupt)
中断一个线程其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
5. Join 等待其他线程终止
join() 方法,等待其他线程终止,当前线程转为阻塞状态。
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到join() 方法。
6. 线程唤醒(notify)
Object 类中的notify() 方法,唤醒在此对象监视器上等待的单个线程
7. 其它方法
sleep():强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
getPriority()::获得一个线程的优先级
八、线程上下文切换
任务的状态保存及再加载, 这段过程就叫做上下文切换。
进程是指一个程序运行的实例。
上下文是指某一时间点CPU寄存器和程序计数器的内容。
寄存器是CPU内部的数量较少但是速度很快的内存。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
程序计数器是一个专用的寄存器,用于表明指令序列中CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
九、线程池原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务。
如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
他的主要特点为:线程复用;控制最大并发数;管理线程。
1. 线程复用
可以继承重写Thread类,在其start方法中添加不断循环调用传递过来的Runnable对象。这就是线程池的实现原理。
循环方法中不断获取Runnable 是用Queue 实现的,在获取下一个Runnable 之前可以是阻塞的。
2. 线程池的组成
a. 线程池管理器:用于创建并管理线程池
b. 工作线程:线程池中的线程
c. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
d. 任务队列:用于存放待处理的任务,提供一种缓冲机制
3. 拒绝策略
a. AbortPolicy : 直接抛出异常,阻止系统正常运行。
b. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
c. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
d. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口.
十、CyclicBarrier、CountDownLatch、Semaphore 的用法
1. CountDownLatch(线程计数器)
CountDownLatch 类位于java.util.concurrent 包下,利用它可以实现类似计数器的功能。CountDownLatch 是不能够重用的。
比如有一个任务A,它要等待其他4 个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
2. CyclicBarrier(回环栅栏-等待至barrier 状态再全部同时执行)
通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用。
调用await()方法之后,线程就处于barrier 了。
3. Semaphore(信号量-控制同时访问的线程个数)
Semaphore 是一种基于计数的信号量。
Semaphore 可以控制同时访问的线程个数,通过acquire() 获取一个许可,如果没有就等待,而release() 释放一个许可。
一般用于控制对某组资源的访问权限。 此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能。
Semaphore 基本能完成ReentrantLock 的所有工作。
十一、ConcurrentHashMap并发原理
Segment的大小也被称为ConcurrentHashMap的并发度。减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。
减小锁粒度是一种削弱多线程锁竞争的有效手段
ConcurrentHashMap 是由Segment 数组结构和HashEntry 数组结构组成。
Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。
每个Segment 守护一个HashEntry 数组里的元素,当对HashEntry 数组的数据进行修改时,必须首先获得它对应的Segment 锁。
十二、Java 中用到的线程调度
1. 抢占式调度
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片(较长或较短或得不到时间片)。
在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
2. 协同式调度
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,
但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
3. JVM 的线程调度实现(抢占式调度)
java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片。
十三、进程调度算法
1. 优先调度算法
先来先服务调度算法(FCFS) : 算法比较简单,可以实现基本上的公平。
短作业(进程)优先调度算法(SJF/SPF) : 选出估计运行时间最短的作业或进程。
2. 高优先权优先调度算法
非抢占式优先权算法
抢占式优先权调度算法: 只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行
高响应比优先调度算法
3. 基于时间片的轮转调度算法
十四、AQS(抽象的队列同步器
AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器。
AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
AQS 定义两种资源共享方式
Exclusive 独占资源-ReentrantLock Exclusive(独占,只有一个线程能执行,如ReentrantLock)。
Share 共享资源-Semaphore/CountDownLatch Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现。
同步器的实现是ABS 核心
a. 以ReentrantLock 为例,state 初始化为0,表示未锁定状态。A 线程lock()时,会调用tryAcquire()独占该锁并将state+1。
此后,其他线程再tryAcquire()时就会失败,直到A 线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁
b. 以CountDownLatch 以例,任务分为N 个子线程去执行,state 也初始化为N(注意N 要与线程个数一致)。
这N 个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS 减 1。
等到所有子线程都执行完后(即state=0),会unpark()主调用线程。