本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,lock 的使用,定时器,单例模式,以及线程状态与线程组。
java 多线程
基础知识
-
创建线程的两种方式:1.继承 Thread 类,2.实现 Runnable 接口。具体两者的联系可以参考我之前的博文《java 基础巩固笔记(5)-多线程之传统多线程》
-
一些基本API:isAlive(),sleep(),getId(),yield()等。
-
isAlive()
测试线程是否处于活动状态 -
sleep()
让“正在执行的线程”休眠 -
getId()
取得线程唯一标识 -
yield()
放弃当前的 CPU 资源 -
弃用的API:
stop()
,suspend()
,resume()
等,已经弃用了,因为可能产生数据不同步等问题。 -
停止线程的几种方式:
-
使用退出标识,使线程正常退出,即 run 方法完成。
-
使用 interrupt 方法中断线程
-
线程的优先级:继承性,规则性,随机性
-
线程的优先级具有继承性. 如,线程 A 启动线程 B,则 B 和 A 优先级一样
-
线程的优先级具有规则性. CPU 尽量倾向于把资源优先级高的线程
-
线程的优先级具有随机性. 优先级不等同于执行顺序,二者关系不确定
-
java 中的两种线程:用户线程和守护(Daemon)线程。
-
守护线程:进程中不存在非守护线程时,守护线程自动销毁。典型例子如:垃圾回收线程。
比较和辨析
-
某个线程与当前线程:当前线程则是指正在运行的那个线程,可由
currentThread()
方法返回值确定。例如,直接在main方法里调用run方法,和调用线程的start方法,打印出的当前线程结果是不同的。 -
interrupted()
和isInterrupted()
-
interrupted()
是类的静态方法,测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能。 -
isInterrupted()
是类的实例方法,测试Thread对象是否已经是中断状态,但不清楚状态标志。 -
sleep()
和wait()
区别: -
sleep()是 Thread 类的 static(静态)的方法;wait() 方法是 Object 类里的方法
-
sleep()睡眠时,保持对象锁,仍然占有该锁;wait()睡眠时,释放对象锁
-
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级;wait() 使用 notify 或者 notifyAlll 或者指定睡眠时间来唤醒当前等待池中的线程
-
wait()必须放在 synchronized block 中,否则会在 runtime 时扔出
java.lang.IllegalMonitorStateException
异常
方法 | 是否释放锁 | 备注 |
---|---|---|
wait | 是 | wait和notify/notifyAll是成对出现的, 必须在synchronize块中被调用 |
sleep | 否 | 可使低优先级的线程获得执行机会 |
yield | 否 | yield方法使当前线程让出 CPU 占有权, 但让出的时间是不可设定的 |
对象及变量的并发访问
-
synchronized
关键字 -
调用用关键字 synchronized 声明的方法是排队运行的。但假如线程 A 持有某对象的锁,那线程 B 异步调用非 synchronized 类型的方法不受限制。
-
synchronized 锁重入:一个线程得到对象锁后,再次请求此对象锁时是可以得到该对象的锁的。同时,子类可通过“可重入锁”调用父类的同步方法。
-
同步不具有继承性。
-
synchronized 使用的“对象监视器”是一个,即必须是同一个对象
-
synchronized 同步方法和 synchronized 同步代码块。
-
对其他 synchronized 同步方法或代码块调用呈阻塞状态。
-
同一时间只有一个线程可执行 synchronized 方法/代码块中的代码
-
synchronized(非 this 对象 x),将 x 对象作为“对象监视器”
-
当多个线程同时执行
synchronized(x){}
同步代码块时呈同步效果 -
当其他线程执行 x 对象中 synchronizd 同步方法时呈同步效果
-
当其他线程执行 x 对象方法里的 synchronized(this) 代码块时呈同步效果
-
静态同步 synchronized 方法与 synchronized(class) 代码块:对当前对应的 class 类进行持锁。
线程的私有堆栈图
-
volatile 关键字:主要作用是使变量在多个线程间可见。加 volatile 关键字可强制性从公共堆栈进行取值,而不是从线程私有数据栈中取得变量的值
-
在方法中 while 循环中设置状态位(不加 volatile 关键字),在外面把状态位置位并不可行,循环不会停止,比如 JVM 在 -server 模式。
-
原因:是私有堆栈中的值和公共堆栈中的值不同步
-
volatile 增加了实例变量在多个线程间的可见性,但不支持原子性
-
原子类:一个原子类型就是一个原子操作可用的类型,可在没有锁的情况下做到线程安全。但原子类也不是完全安全,虽然原子操作是安全的,可方法间的调用却不是原子的,需要用同步。
读取公共内存图
辨析和零散补充
-
synchronized 静态方法与非静态方法:synchronized 关键字加 static 静态方法上是给 Class 类上锁,可以对类的所有实例对象起作用;synchronized 关键字加到非 static 静态方法上是给对象上锁,对该对象起作用。这两个锁不是同一个锁。
-
synchronized 和 volatile 比较
-
1)关键字 volatile 是线程同步的轻量级实现,性能比 synchronized 好,且 volatile 只能修饰变量,synchronized 可修饰方法和代码块。
-
2)多线程访问 volatile 不会发生阻塞,synchronized 会出现阻塞
-
3)volatile 能保证数据可见性,不保证原子性;synchronized 可以保证原子性,也可以间接保证可见性,因为 synchronized 会将私有内存和公共内存中的数据做同步。
-
4)volatile 解决的是变量在多个线程间的可见性,synchronized 解决的是多个线程访问资源的同步性。
-
String 常量池特性,故大多数情况下,synchronized 代码块都不适用 String 作为锁对象。
-
多线程死锁。使用JDK自带工具,jps 命令+jstack命令监测是否有死锁。
-
内置类与静态内置类。
-
锁对象的的改变。
-
一个线程出现异常时,其所持有的锁会自动释放。
变量在内存中的工作过程图
线程间通信
-
等待/通知机制:
wait()
和notify()
/notifyAll()
。wait 使线程停止运行,notify 使停止的线程继续运行。 -
在调用 notify()之前,线程必须获得该对象的对象级别锁;
-
执行完 notify()方法后,不会马上释放锁,要直到退出 synchronized 代码块,当前线程才会释放锁。
-
notify()一次只随机通知一个线程进行唤醒
-
在调用 wait()之前,线程必须获得该对象的对象级别锁;
-
执行 wait()方法后,当前线程立即释放锁;
-
从 wait()返回前,线程与其他线程竞争重新获得锁
-
当线程呈 wait()状态时,调用线程的 interrup()方法会出现 InterrupedException 异常
-
wait(long)
是等待某一时间内是否有线程对锁进行唤醒,超时则自动唤醒。 -
wait()
:将当前执行代码的线程进行等待,置入”预执行队列”。 -
notify()
:通知可能等待该对象的对象锁的其他线程。随机挑选一个呈wait状态的线程,使它等待获取该对象的对象锁。 -
notifyAll()
和notify()
差不多,只不过是使所有正在等待队中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。 -
每个锁对象有两个队列:就绪队列和阻塞队列。
-
就绪队列:存储将要获得锁的线程
-
阻塞队列:存储被阻塞的的线程
-
生产者/消费者模式
-
假死的主要原因:有可能连续唤醒同类。notify 唤醒的不一定是异类,也许是同类,如“生产者”唤醒“生产者”。
-
解决假死:将 notify()改为 notifyAll()
-
wait条件改变,可能出现异常,需要将if改成while
-
“假死”:线程进入 WAITING 等待状态,呈假死状态的进程中所有线程都呈 WAITING 状态。
-
通过管道进行线程间通信:一个线程发送数据到输出管道,另一个线程从输入管道读数据。
-
字节流:
PipedInputStream
和PipedOutputStream
-
字符流:
PipedReader
和PipedWriter
-
join()
:等待线程对象销毁,具有使线程排队运行的作用。 -
join()与interrupt()方法彼此遇到会出现异常。
-
join(long)
可设定等待的时间 -
join
与synchronized
的区别:join在内部使用wait()方法进行等待;synchronized使用的是“对象监视器”原理作为同步 -
join(long)
与sleep(long)
的区别:join(long)内部使用 wait(long)实现,所以 join(long)具有释放锁的特点;Thread.sleep(long)不释放锁。 -
ThreadLocal
类:每个线程绑定自己的值 -
覆写该类的
initialValue()
方法可以使变量初始化,从而解决 get()返回 null 的问题 -
InheritableThreadLocal
类可在子线程中取得父线程继承下来的值。
Lock 的使用
-
ReentrantLock
类:实现线程之间的同步互斥,比 synchronized 更灵活 -
lock()
,调用了的线程就持有了“对象监视器”,效果和 synchronized 一样 -
使用
Condition
实现等待/通知:比 wait()和 notify()/notyfyAll() 更灵活,比如可实现多路通知。 -
调用 condition.await()前须先调用 lock.lock()获得同步监视器
Object 与 Condition 方法对比
Object | Condition |
---|---|
wait() | await() |
wait(long timeout) | await(long time,TimeUnit unit) |
notify() | signal() |
notifyAll() | signalAll() |
一些 API
方法 | 说明 |
---|---|
int getHoldCount() |
查询当前线程保持此锁定的个数,即调用lock()方法的次数 |
int getQueueLength() |
返回正在等待获取此锁定的线程估计数 |
int getWaitQueueLength(Condition condition) |
返回等待与此锁定相关的给定条件Conditon的线程估计数 |
boolean hasQueueThread(Thread thread) |
查询指定的线程是否正在等待获取此锁定 |
boolean hasQueueThreads() |
查询是否有线程正在等待获取此锁定 |
boolean hasWaiters(Condition) |
查询是否有线程正在等待与此锁定有关的condition条件 |
boolean isFair() |
判断是不是公平锁 |
boolean isHeldByCurrentThread() |
查询当前线程是否保持此锁定 |
boolean isLocked() |
查询此锁定是否由任意线程保持 |
void lockInterruptibly() |
如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常 |
boolean tryLock() |
仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定 |
boolean tryLock(long timeout,TimeUnit unit) |
如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定 |
-
公平锁与非公平锁
-
公平锁表示线程获取锁的顺序是按照加锁的顺序来分配的,即FIFO先进先出。
-
非公平锁是一种获取锁的抢占机制,随机获得锁。
-
ReentrantReadWriteLock
类 -
读读共享
-
写写互斥
-
读写互斥
-
写读互斥
定时器
常用 API
方法 | 说明 |
---|---|
schedule(TimerTask task, Date time) | 在指定的日期执行某一次任务 |
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | 在指定的日期之后按指定的间隔周期,无限循环的执行某一任务 |
schedule(TimerTask task, long delay) | 以执行此方法的当前时间为参考时间,在此时间基础上延迟指定的毫秒数后执行一次TimerTask任务 |
schedule(TimerTask task, long delay, long period) | 以执行此方法的当前时间为参考时间,在此时间基础上延迟指定的毫秒数,再以某一间隔时间无限次数地执行某一TimerTask任务 |
-
schedule
和scheduleAtFixedRate
的区别:schedule 不具有追赶执行性;scheduleAtFixedRate 具有追赶执行性
单例模式与多线程
-
立即加载/“饿汉模式”:调用方法前,实例已经被创建了。通过静态属性new实例化实现的
-
延迟加载/“懒汉模式”:调用 get()方法时实例才被创建。最常见的实现办法是在get()方法中进行new实例化
-
声明synchronized关键字,但运行效率非常低下
-
同步代码块,效率也低
-
针对某些重要代码(实例化语句)单独同步,效率提升,但会出问题
-
使用DCL双检查锁
-
使用enum枚举数据类型实现单例模式
-
缺点:多线程环境中,会出问题
-
解决方法
拾遗补增
方法与状态关系示意图
-
线程的状态:
Thread.State
枚举类,参考官网APIEnum Thread.State -
线程组:线程组中可以有线程对象,也可以有线程组,组中还可以有线程。可批量管理线程或线程组对象。
-
SimpleDateFormat
非线程安全,解决办法有: -
创建多个SimpleDateFormat类的实例
-
使用ThreadLocal类
-
线程组出现异常的处理
-
setUncaughtExceptionHandler()
给指定线程对象设置异常处理器 -
setDefaultUncaughtExceptionHandler()
对所有线程对象设置异常处理器