一、多线程
1.1 线程和进程
操作系统可以同时执行多个任务,每个任务就是一个进程;进程可以同时执行多个任务,每个任务就是一个线程。
进程:当一个程序进入内存运行时,就是一个进程。进程是系统进行资源分配和调度的一个独立单位。
- 具有独立性,进程是系统中独立存在的实体,拥有自己独立的资源,每一个进程都拥有自己独立的内存空间。
- 具有动态性:进程是系统中正在活动的指令集合,程序只是一个静态的指令集
- 具有并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响
线程:线程是进程的执行单位,一个进程至少包含一个线程。
并发和并行:并发指在同一个时间点,有多条指令在处理器上执行;并行指在同一个时间点只能有一条指令执行,但多个进程指令被快速轮换执行。
1.2 线程的创建和启动
1.2.1 继承 Thread 类创建线程类
- 定义 Thread 的子类,并重写该类的 public void run() 方法,该 run() 方法的方法体就代表了线程需要完成的任务。因此 run() 方法称为线程执行体
- 创建 Thread 的子类的实例,也就是创建了线程对象
- 调用线程对象的 start() 方法来启动该线程
- 因为程序每次都要创建一个新的 Thread 的子类的实例,所以 Thread 子类中的实例变量不能被多个 Thread 子类的实例共享
public class ExtendsThread extends Thread {
// 重写 run() 方法,run() 方法的方法体就是线程执行体
@Override
public void run() {
// 当线程类继承Thread类时,可以直接使用this关键字获得当前线程
// getName() 方法返回当前线程的名字
System.out.println(getName() + UUID.randomUUID());
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 2) {
// 创建第一个线程
new ExtendsThread().start();
// 创建第二个线程
new ExtendsThread().start();
}
}
}
}
Thread.currentThread():currentThread() 是 Thread 类的静态方法,该方法总是返回当前正在执行的线程对象
getName():该方法是 Thread 类的实例方法,该方法返回调用该方法的线程名字
setName(String name):为线程设置名字
1.2.2 实现 Runnable 接口创建线程类
- 定义 Runnable 接口的实现类,并重写该接口的 public void run() 方法,run() 方法的方法体就是该线程的线程执行体
- 创建 Runnable 实现类的实例,以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象
- 调用线程对象的 start() 方法来启动该线程
- 创建的 Runnable 实现类对象只是线程的 target ,多个线程可以共享一个 target ,所以多个线程可以共享一个线程类的实例变量
public class IRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + UUID.randomUUID());
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 2) {
// 创建第一个线程
IRunnable iRunnable = new IRunnable();
new Thread(iRunnable,"线程一").start();
// 创建第二个线程
new Thread(iRunnable,"线程二").start();
}
}
}
}
注意: Runnable 实例对象只作为 Thread 对象的 target ,Runnable 实现类里包含的 run() 方法仅作为线程执行体,实际线程对象还是 Thread 实例,只是该 Thread 线程负责执行其 target 的 run() 方法。 通过继承 Thread 类获取当前线程对象只需要用 this 就可以了,但是实现 Runnable 接口的实例对象则只能使用 Thread.currentThread() 方法来获取当前线程对象。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable 接口中只包含了一个抽象的 run() 方法,从 Java8 开始,Runnable 接口使用了 @FunctionalInterface 修饰,也就是说 Runnable 接口是函数式接口,可以使用 Lambda 表达式创建 Runnable 对象。
public class Runable7_8 {
public static void main(String[] args) {
// Java8之前
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before Java8, too much code for too little to do");
}
}).start();
// Java8之后
new Thread(()-> System.out.println("In Java8,Lambda expression rocks!!!")).start();
}
}
1.2.3 使用Callable 和 Future 创建线程
Java5 开始,Java 提供了 Callable 接口,Callable 接口提供了一个 call() 方法可以作为线程的执行体,但是 call() 方法可以有返回值,也可以声明抛出异常。
Callable接口不是 Runnable 接口的子接口,所以不能直接作为 Thread 类的 target,并且 call() 方法有返回值,它是作为线程执行体被调用的。Java5 提供了 Future 接口来代表 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,并且该实现类实现了 Runable 接口,可以作为 Thread 类的 target。
Callable 接口有泛型限制,Callable 接口里的泛型参数类型与 call() 方法返回值类型相同,而且 Callable 接口是函数式接口,可以使用 Lambda 表达式来创建 Callable 对象。
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,且该 call() 方法有返回值,再创建 Callable 实现类的实例
- 使用 FutureTask 类包装 Callable 对象,该 FutureTask 对象封装了 Callable 对象的 call() 方法的返回值
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
- 使用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值
public class ICallable{
public static void main(String[] args) {
// ICallable 实现 Callable 接口,并重写 call() 方法,创建 Callable 对象
// ICallable iCallable = new ICallable();
// 先使用 Lambda 表达式创建 Callable<Integer> 对象
// 使用 FutureTask 来包装 Callable 对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i =0;
for (; i < 2; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
// call() 方法可以有返回值
return i;
});
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+" 循环变量i的值:"+i);
if(i == 1){
// 实质还是以 Callable对象来创建启动线程的
new Thread(task,"有返回值的线程").start();
}
}
try {
// task.get() 获取线程返回值,结果为:子线程的返回值:2
System.out.println("子线程的返回值:"+task.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
1.2.4 三种创建线程方式的比较
通过继承 Thread 类和 实现 Runnable、Callable 接口都可以实现多线程。不过实现 Runnable 和 Callable 接口的方式基本相同,只是 Callable 接口里定义的方法有返回值,并且可以声明抛出异常。
优缺点 | 实现接口创建线程 | 继承Thread类创建线程 |
---|---|---|
优点 | 线程类只是实现了Runnable或Callable接口,还可以继承其它类;多个线程可以共享一个target对象 | 编写简单,如需要访问当前线程,直接使用this即可获得当前线程对象 |
劣势 | 编程稍微复杂,如需访问当前线程对象,必须使用Thread.currentThred() 方法 | 因为已经继承了Thread类,不能再继承其它父类 |
1.3 线程的生命周期
当线程创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead) 五种状态。线程启动后线程状态会多次在运行、阻塞之间切换。
1.3.1 新建和就绪状态
当使用 new 关键字创建了一个线程后,该线程就处于新建状态,此时它和其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值,程序不会执行线程的执行体。
当线程对象调用了 start() 方法之后,该线程就处于就绪状态 ,Java虚拟机会为其创建方法调用栈和程序计数器。这时只是表示线程可以运行了,至于什么时候运行,取决于JVM中线程调度器的调度。
start() 方法 | run() 方法 |
---|---|
调用 start() 方法启动线程,系统会把该 run() 方法当成线程执行体来处理 | 直接调用 run() 方法,run() 方法会立即执行,且在 run() 方法返回之前其他线程无法并发处理,系统把 run() 方法当成一个普通对象处理 |
注意:只能对处于新建状态的线程调用 start() 方法,否则会引发 IllegalThreadStateException 异常
1.3.2 运行和阻塞状态
引发线程阻塞的情况:
- 线程调用 sleep() 方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所只有
- 线程正在等待某个通知(notify)
- 程序调用了线程的 suspend() 方法将该线程挂起。但这个方法容易导致死锁
- 执行 wait() 方法也会导致线程阻塞
解除以上情况导致线程阻塞的情况:
- 调用 sleep() 方法的线程经过了指定时间
- 线程调用的阻塞式IO方法已经返回
- 线程成功地获得了试图取得的同步监视器
- 线程正在等待某个通知时,其他线程发出了一个通知
- 处于挂起状态的线程被调用了 resume() 恢复方法
- 执行 notify() 或 notifyAll() 唤醒执行了 wait() 方法的线程
1.3.3 线程死亡
线程会以以下三种方式结束,结束后就处于死亡状态
- run() 或 call() 方法执行完成,线程正常结束
- 线程抛出一个未捕获的 Error 或 Exception
- 直接调用该线程的 stop() 方法结束线程----容易导致死锁
可以使用 isAlive() 方法测试某个线程是否死亡,当线程处于就绪、运行、阻塞三种状态时,返回值是 true,否则返回值是 false;对死亡或者新建状态的线程再次调用 start() 方法,都会引发 IllegalThreadStateException 异常
1.4 控制线程
1.4.1 join 线程
当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到被 join() 方法加入的线程执行完为止。
join() 方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程,当所有小问题得到处理后,再调用主线程来进一步处理问题。
public class JoinThread extends Thread {
// 构造方法设置线程的名字
public JoinThread(String name) {
super(name);
}
// 重写 run() 方法,定义线程执行体
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 3; i++) {
if (i == 1) {
JoinThread jt = new JoinThread("被join的线程" + i);
jt.start();
// main线程调用了jt的join,必须要等jt线程执行完才会继续向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
当主线程中的循环变量的值为1的时候,启动了并 join 名为 “被join的线程” 的线程,主线程将一直处于阻塞状态,直到名为 “被join的线程” 的线程执行完成。运行结果为: main 0、被join的线程1 0、被join的线程1 1、main 1、main 2
join() 方法由三种重载方式:
- join():等待被 join 的线程执行完成
- join(long millis):等待被 join 的线程的时间最长为 millis 毫秒,如果再 millis 毫秒内被 join 的线程还没执行结束,则在不等待
- join(ling milis,int nanos):等待被 join 的线程的最长时间为 millis 毫秒 + nanos 毫微秒
1.4.2 后台线程
有一种线程,它是在后台运行的,它的任务就是为其他线程提供服务,又称为守护线程或精灵线程。JVM的垃圾回收线程就是典型的后台线程。
它最大的特征就是:如果所有的前台线程都死亡了,后台线程会自动死亡。调用 Thread 的 setDaemon(true) 方法就可以将指定线程设置为后台线程。
public class DaemonThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + i);
}
}
public static void main(String[] args) {
DaemonThread dt = new DaemonThread();
// 将此线程设置为后台线程
dt.setDaemon(true);
dt.start();
for (int i = 0; i < 1; i++) {
System.out.println(Thread.currentThread().getName()+" " + i);
}
// 程序到此,main 线程结束,随即后台线程也应立即结束
}
}
运行结果:main 0、Thread-00、Thread-01、Thread-02
先将 dt 线程设置为后台线程,然后启动此线程,本应此线程应该执行到i=999时才结束,但是因为唯一的前台线程 main 结束了,JVM会主动退出,因而后台线程也被结束了。
前台线程死亡后,JVM会通知后台线程死亡,但它收到指令到做出响应,需要一定时间。并且要将一个线程设置为后台线程必须要在线程启动前设置,也就是在start()方法执行前执行 setDaemon(true) 方法,否则会引发 IllegalThreadStateException 异常。
1.4.3 线程睡眠:sleep
如果要让当前正在执行的线程暂停一段时间,并进入阻塞状态,可以通过调用 Thread 类的静态 sleep() 方法实现。当前线程调用 sleep() 方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有可执行的其他线程。
public class SleepThread {
public static void main(String[] args) throws Exception{
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName()+" " + new Date());
// main Thu Aug 13 16:03:33 CST 2020
// main Thu Aug 13 16:03:38 CST 2020
// 让线程睡眠 5s
Thread.sleep(5000);
}
}
}
1.4.4 线程让步:yield
它也是 Thread 类中提供的一个静态方法。它可以让当前正在执行的线程暂停,但不会阻塞该线程,只是将线程转入就绪状态。
yield() 只是让当前线程暂停一下,让系统的线程调度器重新调度一次,当某个线程调用了 yield() 方法暂停后,只有优先级与之相同或比之更高的处于就绪状态的线程才会获得执行的机会。
public class YieldThread extends Thread {
YieldThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName() + " " + i);
if (i == 1) {
// 当前线程让步
Thread.yield();
}
}
}
public static void main(String[] args) {
YieldThread yt1 = new YieldThread("高优先级");
// 设置 yt1 线程为高优先级
//yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldThread yt2 = new YieldThread("低优先级");
// 设置 yt2 线程为低优先级
//yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
注意:Thread 提供了 setPriority(int new Priority) 方法来设置线程的优先级,优先级高的线程只是可以获得更高的执行机会不是一定。
1.5 线程同步
引自:【多线程】synchronized同步代码块
1.5.1 同步方法和同步代码块(sychronized)
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用 synchronized 方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
public class SynThread {
public void doLongTimeTask(){
try{
System.out.println(Thread.currentThread().getName()+" 线程开始,执行一个较长时间的任务,其内容不需要同步");
Thread.sleep(1000);
synchronized (this){
System.out.println(Thread.currentThread().getName()+" 线程,执行同步代码块,其内容需要同步");
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName()+" 线程执行完毕");
}catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
SynThread synThread = new SynThread();
new Thread(()->{synThread.doLongTimeTask();},"线程一").start();
new Thread(()->{synThread.doLongTimeTask();},"线程二").start();
}
}
当两个并发线程访问同一个对象中的 synchronized(this) 同步代码块时,一段时间内只能有一个线程执行,另一个线程必须等待当前线程执行完这个同步代码块的内容后才能执行该同步代码块。但是此时,另一个线程仍可以访问该对象的非 synchronized(this)同步代码块
1.5.2 将任意对象作为对象监视器
多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。
这说明synchronized同步方法或synchronized(this) 同步代码块分别有两种作用。
1)synchronized同步方法:
- 对其他synchronized同步方法或synchronized(this)同步代码块呈阻塞状态。
- 同一时间只有一个线程可以执行synchronized同步方法中的代码
2)synchronized(this)同步代码块
- 对其他synchronized同步方法或synchronized(this)同步代码块呈阻塞状态。
- 同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码。
在前面的学习中,使用synchronized(this)格式来同步代码块,其实Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数是实例变量及方法的参数,使用格式为synchronized(非this对象)。
SynService类:
public class SynService {
private String userName;
private String password;
private String anyObj = new String();
public void setNameAndPwd(String name,String pwd){
try{
synchronized (anyObj){
System.out.println(Thread.currentThread().getName()+" 线程在 "+System.currentTimeMillis()+" 进入同步代码块");
userName = name;
Thread.sleep(2000);
password = pwd;
System.out.println(Thread.currentThread().getName()+" 线程在 "+System.currentTimeMillis()+" 退出同步代码块");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
ThreadA 和 ThreadB 类
public class ThreadA extends Thread{
private SynService synService;
public ThreadA(SynService synService){
super();
this.synService = synService;
}
@Override
public void run() {
synService.setNameAndPwd("ThreadA","ThreadAAA");
}
}
public class ThreadB extends Thread {
private SynService synService;
public ThreadB(SynService synService) {
super();
this.synService = synService;
}
@Override
public void run() {
synService.setNameAndPwd("ThreadB", "ThreadBBB");
}
}
主方法:
public class AnyObjSynTest {
public static void main(String[] args) {
SynService synService = new SynService();
ThreadA threadA = new ThreadA(synService);
ThreadB threadB = new ThreadB(synService);
threadA.setName("AAA");
threadA.start();
threadB.setName("BBB");
threadB.start();
}
}
最终实现了同步效果,输出结果如下:
锁非this对象具有一定的优点:如果一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响消息;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块的程序与同步方法是异步的。不与其他锁this同步方法争抢this锁,则可以大大提高运行效率。
使用synchronized(非this对象x)同步代码块 进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。
1.5.3 释放同步监视器的锁定
出现以下情况时,会释放同步监视器的锁定
- 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
- 当前线程的同步方法、同步代码块中遇到break、return终止了同步方法、同步代码块的运行,当前线程将会释放同步监视器
- 当前线程的同步方法、同步代码块中出现了未处理的 Error 或 Exception 导致该同步方法、同步代码块异常结束,当前线程将会释放同步监视器
- 当前线程在执行同步方法或同步代码块时,程序执行了同步监视器对象的 wait() 方法,则当前线程暂停,并释放同步监视器
出现以下情况时,不会释放同步监视器的锁定
- 线程在执行同步方法或同步代码块时,程序调用 Threed.sleep()、Threed.yield() 方法来暂停当前线程的执行,该线程不会释放同步监视器
- 线程在执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放同步监视器
1.5.4 同步锁(Lock)
Java5 开始,Java 提供了一种更强大的线程同步机制—通过显示定义同步锁对象来实现同步,这种机制下同步锁由 Lock 对象充当。
通常,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。某些锁可能允许对共享资源并发访问,如:ReadWriteLock(读写锁)、Lock、ReadWriteLock是 Java5 提供的两个根接口,并为 Lock 提供了 ReentrantLock(可重入锁)实现类,为 ReadWriteLock 提供了 ReentrantReadWriteLock实现类。
在实现线程安全的控制中,比较常用的是 ReentrantLock ,使用该 Lock 对象可以显示地加锁、释放锁。
public class MyReentrantLock implements Runnable {
// 向上转型
private Lock lock = new ReentrantLock();
@Override
public void run() {
// 上锁
lock.lock();
try {
for (int i = 0; i < 3; i++) {
System.out.println("当前线程名为:" + Thread.currentThread().getName() + " " + i);
}
} finally {
// 释放锁
lock.unlock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "释放了lock锁");
}
}
public static void main(String[] args) {
MyReentrantLock reentrantLock = new MyReentrantLock();
new Thread(reentrantLock, "A").start();
new Thread(reentrantLock, "B").start();
new Thread(reentrantLock, "C").start();
}
}
运行结果:只有当持有对象监视器的线程执行完之后其它线程获取到对象监视器后才能去执行。但是线程之间是随机的。
1.5.4.1 ReentrantLock
ReentrantLock 锁具有可重入性,一个线程可以对已被枷锁的 ReentrantLock 锁再次加锁,ReentrantLock 对象会 维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock() 加锁后,必须显示调用 unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
1.6 线程通信
1.6.1 传统的线程通信
- wait():执行此方法会导致当前线程阻塞,并且会释放当前线程对该同步监视器的锁定。无参的wait() 方法是一直等待,直到其它线程通知,有参的 wait() 方法是等待指定的时间后自动苏醒
- notify(): 唤醒在此同步监视器上等待的单个线程,如果所有线程都在此同步监视器上等待,则会随机唤醒其中一个线程,只有当前线程放弃对该同步监视器的锁定后,被唤醒的线程才能继续执行。
- **notifyAll():**唤醒在此同步监视器上等待的所有线程,只有当前线程放弃对该同步监视器的锁定后,被唤醒的线程才能继续执行。
使用 wait()、notify()、notifyAll() 实例:引用自:Java中notify和notifyAll的区别 - 何时以及如何使用
- 在这个例子中,如果boolean变量go为false,则三个线程将等待,记住boolean go是一个volatile变量,以便所有线程都能看到它的更新值。
- 最初三个线程A,B,C将等待,因为变量go为假,而一个线程 D 将变为真,并通过调用 notifyAll() 方法通知所有线程,或通过调用notify() 方法通知一个线程。在notify() 调用的情况下,无法保证哪个线程会被唤醒,您可以通过多次运行此Java程序来查看它。
- 在 notifyAll() 的情况下,所有线程都将被唤醒,线程D最先执行完,之后哪个线程被唤醒先获得对象监视器就会先执行完,后续两个线程也是如此。( shouldGo() 方法中 go = false; 注释开启的情况下)
- 在 notifyAll() 的情况下,所有线程都将被唤醒,但是它们将竞争监视器或锁定,并且将首先获得锁定的线程将完成其执行并且重置为false将迫使其他两个线程仍在等待。在该程序结束时,将有两个线程在等待,两个线程包括通知线程完成。程序不会终止,因为其他两个线程仍在等待,并且它们不是守护程序线程。( shouldGo() 方法中 go = false; 注释关闭的情况下)
/**
* @Author:于晨
* @Description:写多线程代码步骤
* * 1: 线程 2:操作(方法) 3:资源类
* * 1:判断 2:干活 3:通知
* @Date:Create in 2020/8/13 19:19
*/
public class WaitSync {
public static void main(String args[]) throws InterruptedException {
WaitThreadSource waitThreadSource = new WaitThreadSource();
new Thread(() -> {
waitThreadSource.waitTask();
}, "AAA").start();
new Thread(() -> {
waitThreadSource.waitTask();
}, "BBB").start();
new Thread(() -> {
waitThreadSource.waitTask();
}, "CCC").start();
// 主线程休眠200s,确保所有将被阻塞的线程都会被阻塞
Thread.sleep(200);
new Thread(() -> {
waitThreadSource.notifyTask();
}, "DDD").start();
}
}
class WaitThreadSource {
// 作为对象监视器
private volatile boolean go = false;
public void waitTask() {
try {
shouldGo();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程正在执行");
}
public void notifyTask() {
go();
System.out.println(Thread.currentThread().getName() + " 线程正在执行");
}
// 此方法将会阻塞线程A、B、C
private synchronized void shouldGo() throws InterruptedException {
while (go != true) {
System.out.println(Thread.currentThread().getName() + " 将会执行 wait()方法被阻塞");
wait();
System.out.println(Thread.currentThread().getName() + " 被唤醒");
}
//go = false;
}
// 此方法将会通知唤醒其它阻塞线程
private synchronized void go() {
while (go == false) {
System.out.println(Thread.currentThread().getName() + " 将会唤醒被阻塞的线程");
go = true;
//notify(); // notify会随机唤醒其中一个线程
notifyAll(); // notifyAll会唤醒所有线程
}
}
}
结果如下:
1.6.2 使用 Condition 控制线程通信
若程序使用 Lock 对象来保证同步,Java 提供了一个 Condition 类来保持协调。Lock 替代了同步方法或同步代码块,Condition 替代了同步监视器的功能。
Condition 实例被绑定在一个 Lock 对象上,要获得特定 Lock 实例的 Condition 实例,调用 Lock 对象的 newCondition() 方法即可。Condition 提供了三个方法:await()---->wait()、signal()---->notify()、signalAll()---->notifyAll()
使用 await()、signal()、signalAll() 实例:
/**
* @Author:于晨
* @Description:写多线程代码步骤:资源类中去执行方法逻辑,在本地只是去调用资源类的方法
* 1: 线程 2:操作(方法) 3:资源类
* 1:判断 2:干活 3:通知
* @Date:Create in 2020/8/13 20:35
*/
public class AwaitLock {
public static void main(String[] args) throws InterruptedException {
ThreadSource threadSource = new ThreadSource();
new Thread(() -> {
threadSource.awaitLock();
}, "AAA").start();
new Thread(() -> {
threadSource.awaitLock();
}, "BBB").start();
new Thread(() -> {
threadSource.awaitLock();
}, "CCC").start();
Thread.sleep(200);
new Thread(() -> {
threadSource.signalLock();
}, "DDD").start();
}
}
class ThreadSource {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void awaitLock() {
// 上锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 线程开始执行");
System.out.println(Thread.currentThread().getName() + " 要进入阻塞状态了");
condition.await();
System.out.println(Thread.currentThread().getName() + " 线程被唤醒");
// 释放所资源
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 执行结束并释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void signalLock() {
// 上锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 线程开始执行");
System.out.println(Thread.currentThread().getName() + " 要唤醒其它线程了");
condition.signal();
// 释放所资源
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 执行结束并释放了锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果:
1.6.3 多线程案例
1.6.3.1 生产者和消费者案例一(使用 Lock 锁)
生产者生产一条消息,消费者消费一条消息, 当消息未被消费时,生产者阻塞,当消息已被消费时,消费者阻塞。这里以两个加减的方法作为案例:
注意,在多线程中判断不能用if,只能用while。if会造成虚假唤醒,在 Java 官方文档中,对 wait() 方法有这样一句描述:
* As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
* 与一个参数版本一样,可能会产生中断和虚假唤醒,并且此方法应始终在循环中使用,也就是不能只判断一次,需要循环判断
若将 while 改为 if,一个生产者一个消费者不会出现虚假唤醒的问题,但是变为更多个,就会出现虚假唤醒问题。
public class ProConThread {
public static void main(String[] args) {
ProConSource proConSource = new ProConSource();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
proConSource.increase();
}
}, "AAA").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
proConSource.decrease();
}
}, "BBB").start();
}
}
class ProConSource {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increase() {
// 上锁
lock.lock();
try {
// 判断
while (number != 0) {
// 生产者阻塞
condition.await();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName() + " " + number);
// 通知唤醒
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrease() {
// 上锁
lock.lock();
try {
// 判断
while (number != 1) {
// 停止消费
condition.await();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName() + " " + number);
// 通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
1.6.3.2 生产者和消费者案例一(使用BlockingQueue阻塞队列)
BlockingQueue 具有一个特征:当生产者线程试图向 BlockingQueue 中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从 BlockingQueue 中取出元素时,如果该队列已空,则该线程被阻塞。
BlockingQueue 继承了 Queue 接口,可以使用 Queue 接口中的方法,归结为:
抛出异常 | 不同返回值 | 阻塞线程 | 指定超时时长 | |
---|---|---|---|---|
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
队头删除元素 | remove() | poll() | take() | poll(time,unit) |
获取、不删除元素 | element() | peek() | 无 | 无 |
常用的队列有三种:
- ArrayBlockingQueue:基于数组实现的 BlockingQueue 队列。
- LinkedBlockingQueue:基于链表实现的 BlockingQueue 队列。
- SynchronousQueue:同步队列。该队列的存、取操作必须交替进行。
public class BlockThread {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
BlockSource blockSource = new BlockSource();
// 创建三个生产者生产消息
new Thread(() -> {
blockSource.producer(queue);
}, "AAA").start();
new Thread(() -> {
blockSource.producer(queue);
}, "BBB").start();
new Thread(() -> {
blockSource.producer(queue);
}, "CCC").start();
// 创建一个消费者消费消息
new Thread(() -> {
blockSource.consumer(queue);
}, "DDD").start();
}
}
class BlockSource {
String ele = "ele";
public void producer(BlockingQueue<String> queue) {
try {
queue.put(ele);
System.out.println(Thread.currentThread().getName() + " 成功生产了一条消息" + ele);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void consumer(BlockingQueue<String> queue) {
while (true) {
try {
String take = queue.take();
System.out.println(Thread.currentThread().getName() + " 成功消费了一条消息" + take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.7 线程中方法比较
方法 | sleep() | yield() | join() | wait() | notify() | notifyAll() |
---|---|---|---|---|---|---|
线程状态 | 阻塞 | 就绪 | 阻塞 | 阻塞 | 调用执行了wait()方法中的某个线程就绪 | 调用执行了wait()方法中的所有线程就绪 |
所属类 | Thread类的一个静态方法 | Thread类的一个静态方法 | Thread类的一个方法 | Object类的一个方法 | Object类的一个本地方法 | Object类的一个本地方法 |
二、线程池
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。线程池技术正是关注如何缩短或调整T1,T3时间的技术。线程池中的线程复用极大节省了系统资源。线程池另一功能是控制并发,因为内部使用了阻塞队列。
2.1 线程池核心类
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
2.1.1 corePoolSize:核心线程数量
当一个新的任务被提交到线程池时,如果当前运行的线程小于 corePoolSize ,就会新建一个线程,即使线程池的其它线程可能处于空闲状态,因此在初始,线程池会迅速将线程池中线程的数量提升至 corePoolSize 。如果当前运行的线程数量大于 corePoolSize 但是小于 maximumPoolSize ,线程池会将这个任务放入队列,直到队列已满再建立新的线程。
2.1.2 maximumPoolSize:最大线程数量
2.1.3 keepAliveTime和unit:过量线程存活时间
当线程池当前的线程数量大于corePoolSize,如果这些线程的空闲时间超过了keepAliveTime(时间单位为unit),那么这些线程将会被终止。使用 allowCoreThreadTimeOut方法可以同样控制小于corePoolSize的部分线程。
2.1.4 threadFactory:创建线程的工厂
Executors提供了默认的DefaultThreadFactory,一般都会使用这个。也可以自己指定一个threadFactory,来修改线程的名称,线程组,优先级,守护线程状态等。
2.1.5 workQueue:任务队列
任务队列一般采用3种模式:
- **SynchronousQueue:**本身只存储一个任务,当有任务提交之后进入阻塞状态直到有线程过来获取任务。newCachedThreadPool采用的这个队列,当有新的任务到来就创建一个线程,maximumPoolSize使用的是Integer的最大值来避免任务被拒绝。
- **LinkedBlockingQueue:**无界队列。newSingleThreadExecutor和newFixedThreadPool均使用该队列。任务能够无限的放入队列之中等待被执行。因为对用永远也不可能填满,所以maximumPoolSize这个参数在这里是无效的。使用这个队列需要注意生产者和消费者的速度差导致的内存溢出问题。
- **ArrayBlockingQueue:**有界队列。可定制性最强的队列。能够有效的保护资源(线程或是内存)。可以选择长队列+少线程数或是短队列+多线程数的组合可以控制对CPU和内存的使用。
2.1.6 handler(RejectedExecutionHandler):拒绝策略
当线程池已经停止或是队列已满,线程数量已经达到maximumPoolSize,那么新提交的任务将会被拒绝。
JDK提供的几种默认的策略:
-
ThreadPoolExecutor.AbortPolicy:直接抛异常RejectedExecutionException。
-
ThreadPoolExecutor.CallerRunsPolicy:直接以同步方式调用任务。
-
ThreadPoolExecutor.DiscardPolicy:不作为。直接抛弃任务。
-
ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中进入队列等待时间最长的任务。
-
需要的话也可以自己去实现RejectedExecutionHandler接口,然后实现方法。
void rejectedExecution(Runnable r, ThreadPoolExecutor e)
2.2 线程池工作流程
- 当前的任务数量小于corePoolSize,会直接创建线程,当任务大于等于corePoolSize,任务将会放入队列。
- 当队列已满,就会新创建线程直到线程数量达到 maximumPoolSize。
- 如果队列已满并且线程数量已经达到了 maximumPoolSize,那么新的任务将会被拒绝。
2.3 线程池分类(工作中一个也不用,自己手写)
2.3.1 newFixedThreadPool
指定工作线程数量的线程池
newFixedThreadPool(int nThreads)
**固定线程池,**核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。
手动创建:
new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),hreadFactory)
2.3.2 newCachedThreadPool
处理大量短时间工作任务的线程池
- 视图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
- 如果线程限制的时间超过阈值,则会被终止并移除缓存;
- 系统长时间闲置的时候,不会消耗什么资源。
newCachedThreadPool()
**固定线程池,**核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE 大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。
手动创建:
new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>())
2.3.3 newSingleThreadExecutor
创建唯一的工作线程来执行任务,如果线程异常结束,会有另外一个线程取代它
newSingleThreadExecutor()
单线程线程池,核心线程数和最大线程数均为1,空闲线程存活0毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行
手动创建:
new ThreadPoolExecutor(1, 1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),threadFactory)
2.3.4 newScheduledThreadPool
调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已
2.3.5 newWorkStealingPool()
内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序
2.4 Fork/Join框架
JDK7中引入的,把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架,使用工作窃取方法。子任务分别放在不同的队列中,为每个队列创建一个单独的线程来执行队列里的任务,为防止一些队列中任务已完成,一些还未完成造成的资源浪费,因此采用工作窃取方法和双端队列,为了减少窃取任务和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务的线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
Work-Stealing算法
2.5 设置线程池大小—蚂蚁金服面试题
想要合理配置线程池线程数的大小,需要分析任务的类型,任务类型不同,线程池大小配置也不同。
配置线程池的大小可根据以下几个维度进行分析来配置合理的线程数:
任务性质可分为:CPU密集型任务,IO密集型任务,混合型任务。
任务的执行时长。
任务是否有依赖——依赖其他系统资源,如数据库连接等。
CPU密集型任务-计算
尽量使用较小的线程池,一般为CPU线程数+1。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
IO密集型任务-磁盘io
可以使用稍大的线程池,一般为2*CPU线程数+1。
因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失
依赖其他资源
如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。
借鉴别人的文章 对线程池大小的估算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
可以得出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。