Java多线程编程核心技术
一、Java多线程技能
1、概念
- 进程:受操作系统管理的基本运行单元。
- 线程:进程中独立运行的子任务
- 优点:最大限度地利用CPU的空闲时间来处理其他任务,提高CPU利用率
2、创建线程
- 继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
super.run();
}
}
- 实现Runnable接口
public class MyThread implements Runnable{
public void run() {
}
}
3、常用API
- Thread.currentThread()方法
- 用处:查看当前正在调用代码段的线程信息
- isAlive()方法
- 用处:判断当前线程是否存活
- Thread.sleep()方法
- 用处:在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。tips:这个“正在执行的线程”是指this.currentThread()返回的线程
- getId()方法
- 用处:获取线程的唯一标识
- interrupt()方法
- 用处:在当前线程打(停止)标记
- interrupted()方法
- 用处:测试线程Thread对象是否已经被标记,执行后状态标志清除为false
- isInterrupted()方法
- 用处:测试线程Thread对象是否已经被标记,但不清楚状态标志
- stop()方法(作废/过期)
- 用处:立即停止线程
- 弊端:强制结束线程可能会带来一些不可预知的后果。例如数据不一致、资源来不及释放
- suspend()方法(作废/过期)
- 用处:暂停当前线程
- 弊端:
1、暂停后,不释放锁,使得其他线程无法访问公共同步对象。
2、暂停后,任务暂停执行,可能会导致数据不同步
- resume()方法(作废/过期)
- 用处:继续当前线程
- yield()方法
- 用处:放弃当前CPU资源,将它让给其他任务取占用COU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上 又获得CPU时间片
- setPriority()方法
- 用处:设置线程的优先级,优先级分为1~10
- 继承性:子线程具有与父线程相同的优先级。例如:A线程启动B线程,则B线程的优先级与A是一致的。
- 规则性:CPU会尽量将执行资源让给优先级较高的线程
- 随机性:优先级较高的线程会获得较多的CPU资源,但不一定每一次都先执行完
- setDaemon()方法
- 用处:标记当前线程为守护线程
- 守护线程:一种特殊的线程,为其他线程的运行提供便利服务。当进程中不存在非守护线程了,则守护线程自动销毁。常见应用:GC(垃圾回收器)
- join()方法
- 用处:使当前线程进行阻塞,等待join方法所属线程执行任务并销毁,再继续执行。
- 知识点
- join方法是一个synchronized修饰的同步方法,因此需要获取对象锁之后,才会执行
- 方法join的功能在内部是使用wait(long)方法实现的,所以join(long)方法具有释放锁的特点。这也是与sleep方法的差异
- 方法join具有使线程排队运行的作用,有些类似同步的运行效果。join与synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理做同步
- 在join过程中,如果当前线程对象被中断(interrupt方法),则当前线程出现异常
- join(long)方法
- 用处:使当前线程进行阻塞,等待join方法所属线程给定时间,再继续执行。
4、拓展
- 4.1、线程的状态
- 初始(NEW)
- 新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE)
- Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED)
- 表示线程阻塞于锁
- 等待(WAITING)
- 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
- 定时等待(TIMED_WAITING)
- 该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED)
- 表示该线程已经执行完毕
- 4.2、线程组
- 作用:批量的管理线程或线程组对象,有效地对线程或线程组对象进行组织。线程组中可以有线程对象,也可以有线程组,组中还可以有线程。是一个类似于树的组织结构
- new Thread(ThreadGroup, name)
- 线程组中新建线程
- new ThreadGroup(ThreadGroup, name)
- 线程组中新建线程组
- 4.3、异常处理
- 线程
- setUncaughtExceptionHandler()方法
- 对指定线程对象设置默认的异常处理器
- 线程组
- 覆盖ThreadGroup中的uncaughtException方法
- 线程组中个别线程出现异常,不会影响其他线程执行
- 知识点
- 优先级:线程 > 线程组 。线程中的异常,会先被线程的对象异常处理捕获,之后才会被线程组异常处理捕获
二、对象及变量的并发访问
1、synchronized关键字
- 1.1、synchronized同步方法
- 用处:从方法的维度,解决“非线程安全”的问题。
1、对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
2、同一时间只有一个线程可以执行synchronized同步方法中的代码 - 弊端:范围大,耗时长,效率低
- 知识点
- 多个对象多个锁:关键在synchronized取得的皆是当前对象锁,而不是把一段代码或方法(函数)当作锁
- A线程先持有Object对象的对象锁,B线程可以以异步的方式调用object对象中的非synchronized类型方法
- A线程先持有Object对象的对象锁,B线程如果在这时调用object对象中的synchronized类型方法则需等待,也就是同步
- synchronized锁重入:当一个线程得到对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的(父子类中同样适用)
- 出现异常,锁自动释放
- 同步不具有继承性:子类中覆盖父类中的同步方法后,需再次在子类方法中添加synchronized关键字
- 1.2、synchronized同步代码块
- 用处:缩小同步范围,提升执行效率。
1、对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
2、同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码 - 应用
- synchronized(this)
- 锁定当前对象,synchronized(this)同步代码块、当前对象同步方法皆呈同步效果
- synchronized(非 this 对象 x)
- 对象x为对象监视器。在多个线程持有“对象监视器”为同一对象的前提下,同一时间只有一个线程可以执行同步代码块内容
- 静态同步synchronized方法与synchronized(Class 对象)
- 对Class类进行持锁
- 知识点
- 由于String常量池的特性,因此在大多数情况下,synchronized代码块都不以String作为锁对象
- 死锁:不同的线程都在等待不可能释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程的“假死”
- 如果多个线程同时持有锁对象,则这些线程之间就是同步的,如果分别获得锁对象,这些线程之间就是异步的
2、volatile关键字
- 用处:修饰成员变量,使变量在多个线程间可见。强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
- 弊端:不支持原子性
- 知识点
- volatile解决的是变量读时的可见性问题,但无法保证原子性(同步性),对于多个线程访问同一个实例变量还是需要加锁同步
- 原子类也可以在没有锁的情况下做到线程安全
- 原子类的方法是原子的,但方法与方法之间的调用却不是原子的,解决这种问题只能用同步
- volatile与synchronized对比
- 关键字volatile是线程同步的轻量级实现,性能高于synchronized关键字,volatile关键字只能修饰变量,而synchronized可以修饰方法以及代码块
- 多线程访问volatile不会出现阻塞,而synchronized会出现阻塞
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步
- volatile解决的是变量在多个线程间的可见性;而synchronized关键字解决的是多个线程之间访问线程资源的同步性
3、Lock
- ReentrantLock类
- 用处:保证实例变量的安全性,实现线程之间的同步互斥,相比synchronized扩展功能更加强大,eg:嗅探锁定,多路分支通知,在使用上也更加灵活
- 常用API
- lock()方法
- 用处:获取锁
- lockInterruptibly()方法
- 用处:如果当前未被中断(被interrupt()标记),则获取锁定,如果已经被中断,则出现异常
- tryLock()方法
- 用处:如果锁定未被其他线程持有,则获取锁定
- tryLock(long timeout, TimeUnit timeUnit)方法
- 用处:如果在给定时间内锁定未被其他线程持有,且当前线程未被中断,则获取该锁定
- unlock()方法
- 用处:释放锁
- constructor(boolean isFair)构造方法
- 用处:创建公平锁,获取锁的顺序按照线程执行顺序来分配
- getHoldCount()方法
- 用处:查询当前线程保持锁定的个数
- getQueueLength()方法
- 用处:返回正在等待此锁定的线程数
- waitQueueLength(Condition condition)方法
- 用处:返回等待与此锁定相关给定条件condition的线程数
- hasQueuedThread(Thread thread)方法
- 用处:查询指定的线程是否正在等待获取此锁定
- hasQueuedThreads()方法
- 用处:查询是否有线程正在等待获取此锁定
- hasWaiters(Condition condition)方法
- 用处:查询是否有线程正在等待与此锁定有关的condition条件
- isFair()方法
- 用处:判断是不是公平锁
- isHeldByCurrentThread()方法
- 用处:判断当前线程是不是保持此锁定
- isLocked()方法
- 用处:查询此锁定是否由任意线程保持
- 知识点
- 多路分支通知:创建多个Condition,对线程进行分组监控。
- 公平锁与非公平锁:公平锁表示获取锁的顺序按照线程执行顺序来分配,即FIFO先进先出顺序。非公平锁就是一种获取锁的抢占机制,是随机获取锁的,先来的不一定先得到锁,这个方式可能造成一些线程一直拿不到锁,结果也就是不公平的了
- ReentrantReadWriteLock类
- 用处:提供读写锁,分隔读写功能,在ReentrantLock的基础上提高效率
- 知识点
- 一个读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。读读共享,写写互斥,读写互斥,写读互斥
三、线程间通信
意义:线程与线程之间不是独立的个体,它们彼此之间可以互相通信和协作。使线程间进行通信后,系统之间的交互性会更强大,在大大地提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控和监督
1、等待/通知机制
-1.1、synchronized关键字
- wait()方法
- 用处:使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
- wait(long)方法
- 用处:使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒,如果超过给定时间,则自动唤醒
- notify()方法
- 用处:随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,直到被再次唤醒
- notifyAll()方法
- 用处:使所有正在等待队列中华等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也可能是随机执行,因为这要取决于JVM虚拟机的实现
- 注意点
- 当方法wait()被执行后,锁被自动释放,但执行完notify()方法。锁却不自动释放,执行完代码块才会释放
- 当线程呈wait()状态时,调用线程对象的interrupt()方法会出现InterruptException异常
-1.2、 Lock锁
- newCondition()方法
- 用处:创建对象监视器,Lock锁中实现线程间通信,也需要该对象
- await()方法
- 用处:使当前执行代码的线程进行等待
- await(long time, TimeUnit timeUnit)方法
- 用处:使当前执行代码的线程进行等待指定时间
- awaitUninterruptibly()方法
- 用处:使当前执行代码的线程进行等待,且等待状态下,被interrupt()标记,不抛出异常
- awaitUntil(long time)方法
- 用处:使当前执行代码的线程进行等待,等到给定时间自动唤醒
- singal()方法
- 用处:一次只随机通知一个线程进行唤醒
- singalAll()方法
- 唤醒当前对象监视器下的所有线程
2、传值通信
-2.1、ThreadLocal
- ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量冲突,实现了线程的数据隔离性
- get()方法
- 用处:以当前ThreadLocal对象为key,将value从当前Thread对象中的ThreadLocalMap中取出。
- set(value)方法
- 用处:以当前ThreadLocal对象为key,将value存储进当前Thread对象中的ThreadLocalMap中。
- 底层原理
- 1、每个Thread维护着一个ThreadLocalMap的引用ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- 2、调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
- 3、调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
- 4、ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
- 注意点
- 一个ThreadLocal中只可以存储一个线程的一个值。因为它是以与线程对象绑定的ThreadLocalMap实现,而key恰好是ThreadLocal对象本身
- ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题
- 我们可以通过继承ThreadLocal类,来实现我们个性化的多线程变量管理工具。eg:设置get()默认返回值
-2.2、InheritableThreadLocal
- 值继承:使用类InheritableThreadLocal可以在子线程中取得父线程继承下来的值
- childValue()方法
- 可以对从父线程中继承的值进行二次处理
- 注意点
- 如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值
- 底层原理
- 1、InheritableThreadLocal继承ThreadLocal并覆盖了childValue()、getMap()、createMap()方法。
- 2、getMap()方法的覆盖,保证拿到的是Thread对象中的inheritableThreadLocal变量
- 3、createMap()方法的覆盖,保证了在第一次set时,创建的是InteritableThreadLocal对象
- 4、继承性实现:主线程中新建子线程,在Thread构造方法中会对主线程的inheritableThreadLocal变量进行检测,配合childValue方法完成两个线程中inheritableThreadLocal变量的复制
-2.3、管道流
- 用处:用于在不同线程间直接传送数据
- 字节流:PipedInputStream
- 输入管道,读取数据
- 字节流:PipedOutputStream
- 输出管道,发送数据
- 字符流:PipedReader
- 字符流:PopedWriter
四、定时器Timer
1、概念:Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。
2、知识点
- TimerTask:Timer类的主要作用是设置计划任务,但封装任务的类是TimerTask类,执行的任务代码要放入到TimerTask的子类中,因为TimerTask是一个抽象类
- Timer在执行定时任务时只会创建一个线程任务,如果存在多个线程,若其中某个线程因为某种原因而导致线程任务执行时间过长,超过了两个任务的间隔时间,会导致下一个任务执行时间滞后
- Timer对调度的支持是基于绝对时间的,而不是相对时间,所以它对系统时间的改变非常敏感。
- Timer线程是不会捕获异常的,如果TimerTask抛出的了未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,他会错误的认为整个Timer线程都会取消。同时,已经被安排单尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为
3、常用API
- schedule(TimerTask task, Date time)方法
- 用处:在指定的日期执行一次某一任务
- schedule(TimerTask task, Date firstTime, long period)方法
- 用处:在指定的日期之后,按指定的间隔周期性地无限循环执行某一任务
- schedule(TimerTask task, long delay)方法
- 用处:以执行时间为参考时间,在此时间基础上,延迟指定的毫秒数后,执行一次TimeTask任务
- schedule(TimerTask task, long delay, long period)方法
- 用处:以执行时间为参考时间,在此时间基础上,延迟指定的毫秒数后,再以某一间隔时间无限次数执行某一任务
- scheduleAtFixedRate(TimerTask task, Date firstTime, long period)方法
- 用处:与schedule的相同,差别是scheduleAtFixedRate方法具有追赶性,如果任务比较复杂,或者由于任何原因(如垃圾回收或其他后台活动)而延迟了某次执行,scheduleAtFixedRate会把已经过去的时间也作为周期执行,将快速连续地出现两次或更多的执行,从而使后续执行能够“追赶上来”
4、底层原理
- Timer类包含另外两个类,TaskQueue、TimerThread。Timer提供对外调度,TaskQueue管理TimerTask,TimerThread是TimerTask的执行线程
public class Timer {
}
/**
* This "helper class" implements the timer's task execution thread, which
* waits for tasks on the timer queue, executions them when they fire,
* reschedules repeating tasks, and removes cancelled tasks and spent
* non-repeating tasks from the queue.
*/
class TimerThread extends Thread {
}
/**
* This class represents a timer task queue: a priority queue of TimerTasks,
* ordered on nextExecutionTime. Each Timer object has one of these, which it
* shares with its TimerThread. Internally this class uses a heap, which
* offers log(n) performance for the add, removeMin and rescheduleMin
* operations, and constant time performance for the getMin operation.
*/
class TaskQueue {
}
- 1、创建Timer对象时,构造中同时创建TimerThread线程并启动
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
- 2、通过schedule方法,任务被添加到TaskQueue中TimerTask数组(默认容量128)中,TaskQueue是一个优先级队列,他是通过nextExecutionTime进行优先级排序的,距离下次执行时间越短优先级越高
// schedule 方法
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
// 被调用
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
//队列新增
queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}
// 队列新增
void add(TimerTask task) {
// Grow backing store if necessary
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length); //扩容
queue[++size] = task;
fixUp(size); // 队列排序
}
//排序方法
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
- 3、TimerThread会通过循环的方式对周期执行、延时执行进行处理。详见@See TimeThread # mainLoop
/**
* The main timer loop. (See class comment.)
*/
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
// 缺点:使用绝对时间,对系统时间依赖过重
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
// 单次执行
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
// 循环执行,设置下一次执行时间
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
}
五、其他
1、SimpleDateFormat非线程安全
- 原因:实例变量非线程安全,造成多线程操作时实例变量的误读
- 解决办法
- 创建多个SimpleDateFormat对象
- 使用ThreadLocal与SimpleDateFormat绑定,利用ThreadLocal的隔离性获取不同的SimpleDateFormat对象。本质上与方案1相同
- 加锁
2、单例模式与多线程
- 立即加载/ “饿汉模式”
/**
* 立即加载/“饿汉模式”
* Created by Eric on 2019/9/6.
*/
public class MyObject {
// 立即 加载 方式== 饿汉 模式
private static MyObject myObject = new MyObject();
private MyObject() {
}
public static MyObject getInstance() {
// 此 代码 版本 为 立即 加载 // 此版 本 代码 的 缺点 是 不能 有其 他 实例 变量 // 因为 getInstance() 方法 没有 同步
return myObject;
}
}
- 延迟加载/ “懒汉模式”
- DCL双检查锁机制
/**
* 立即加载/“懒汉模式”
* Created by Eric on 2019/9/6.
*/
public class MyObject {
private volatile static MyObject myObject;
private MyObject() {
}
// 使用 双 检测 机制 来 解决问题, 既 保证 了 不需要 同步 代码 的 异步 执行 性
// 又 保证 了 单 例 的 效果
public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模拟 在 创建 对象 之前 做 一些 准备 性的 工作
Thread.sleep(3000);
synchronized (MyObject.class) {
if (myObject == null) {
myObject = new MyObject();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
} // 此 版本 的 代码 称为 双重 检查 Double- Check Locking
}
资源:
知识点思维脑图:
链接:百度云盘思维脑图 提取码:2v8h
复制这段内容后打开百度网盘手机App,操作更方便哦