并发编程一、多线程的意义和使用
一、相关概念
1. 影响服务器吞吐量的因素
硬件方面:CPU、内存、磁盘、网络
软件层面:硬件资源是前提,软件层面的配置是为了最大化的利用硬件资源,如配置 线程数量、JVM内存分配大小、网络通信机制(BIO/NIO/AIO)、磁盘IO
从计算机本身的角度来考虑的话,一个请求的访问到处理最终到返回,性能瓶颈只会是:CPU、文件IO、网络IO、内存、等因素。而一台计算机中这些纬度是有性能瓶颈的 ,如果某个资源消耗过多,通常会造成系统的响应速度较慢,所以增加一台机器使得数据库的IO和CPU资源独占一台机器从而增加性能。
各个资源的消耗原因
CPU/IO/内存
1. CPU,主要是上下文的切换,因为每个CPU核心在同一时刻只能执行一个线程,而CPU的调度有几种方式,比如抢占式和轮询等,以抢占式为例,每个线程会分配一定的执行时间,当达到执行时间、线程中有IO阻塞或者有高优先级的线程要执行时。CPU会切换执行其他线程。而在切换的过程中,需要存储当前线程的执行状态并恢复要执行的线程状态,这个过程就是上下文切换。比如IO、锁等待等场景下也会触发上下文切换,当上下文切换过多时会造成内核占用比较多的CPU。
2. 文件IO,比如频繁的日志写入,磁盘本身的处理速度较慢、都会造成IO性能问题
3. 网络IO,带宽不够
4. 内存,包括内存溢出、内存泄漏、内存不足
实际上不管是应用层的调优也好,还是硬件的升级也好。其实无非就是这几个因素的调整。
2. 进程与线程
对比 | 进程 | 线程 |
---|---|---|
定义 | 进程是程序运行的一个实体的运行过程,是系统进行资源分配和调配的一个独立单位 | 线程是进程运行和执行的最小调度单位 |
包含关系 | 一个进程内有1到N个线程 | 线程是进程的一部分,也被称为轻量级的进程 |
系统开销 | 创建撤销切换开销大,资源要重新分配和收回 | 仅保存少量寄存器的内容,开销小,在进程的地址空间执行代码 |
拥有资产 | 资源拥有的基本单位 | 基本上不占资源,仅有不可少的资源(程序计数器,一组寄存器和栈) |
调度 | 资源分配的基本单位 | 独立调度分配的单位 |
安全性 | 进程间相互独立,互不影响 | 线程共享一个进程下面的资源,可以互相通信和影响 |
地址空间 | 系统赋予的独立的内存地址空间 | 由相关堆栈寄存器和和线程控制表TCB组成,寄存器可被用来存储线程内的局部变量 |
3. 并发和并行
并行:指两个或多个事件在同一时刻发生
并发:指两个或多个事件在同一时间间隔内发生。以单核CPU的电脑为例,如果它是以多线程运行的,CPU是通过不断分配时间片的方式来实现线程切换,只是线程之间切换的速度太快我们难以察觉。
二、线程的应用
1. 线程创建
在Java中,有多种方式来实现多线程。继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现带返回结果的多线程。
继承Thread类、
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
实现Runnable接口、
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
使用ExecutorService、Callable、Future实现带返回结果的多线程
import java.util.concurrent.*;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
String str = Thread.currentThread().getName() + " is running";
return str;
}
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<String> future = pool.submit(new MyCallable());
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
三、线程的生命周期
1. 代码演示线程的几种状态
首先分别创建几种情况的线程,然后使用jps
、jstack
命令分别查看该进程pid下的线程的状态。
4个不同的线程:Thread_Status_01
:休眠一定时间;Thread_Status_02
: 等待;Thread_Status_03
和Thread_Status_04
: 两个线程同时抢占一个对象锁,所以其中会有一个线程一定没有抢到锁;
import java.util.concurrent.TimeUnit;
public class ThreadStatus {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Thread_Status_01").start();
new Thread(() -> {
while (true) {
synchronized (ThreadStatus.class) {
try {
ThreadStatus.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Thread_Status_02").start();
BlockedDemo blockedDemo = new BlockedDemo();
new Thread(() -> {
blockedDemo.test();
}, "Thread_Status_03").start();
new Thread(() -> {
blockedDemo.test();
}, "Thread_Status_04").start();
}
}
class BlockedDemo {
public void test() {
synchronized (this) {
while (true) {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
命令行或Intellij Idea中右键Open In Terminal
在IDE中打开命令行,输入jps
查看demo对应的进程pid,可以看到ThreadStatus所对应进程pid为15516
,
之后,使用JVM工具jstack
来查看该pid下的信息,找出定义的4个线程及其对应状态;windows下也可以使用jvisualvm
命令来打开Java VisualVM
可视化工具找到对应pid的线程信息,导出线程 Dump
来查看线程信息
E:\..\..\..\bigshen-threads-boot\src\main\java\com\bigshen\threads>jstack 15516
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):
"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x00000000030e3800 nid=0x3ee8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread_Status_04" #15 prio=5 os_prio=0 tid=0x000000001fbfa000 nid=0x3f8c waiting for monitor entry [0x00000000204ff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.bigshen.threads.BlockedDemo.test(ThreadStatus.java:61)
- waiting to lock <0x000000076bc86e90> (a com.bigshen.threads.BlockedDemo)
at com.bigshen.threads.ThreadStatus.lambda$main$2(ThreadStatus.java:44)
at com.bigshen.threads.ThreadStatus$$Lambda$3/769287236.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread_Status_03" #14 prio=5 os_prio=0 tid=0x000000001fbef000 nid=0x168c waiting on condition [0x00000000203fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.bigshen.threads.BlockedDemo.test(ThreadStatus.java:61)
- locked <0x000000076bc86e90> (a com.bigshen.threads.BlockedDemo)
at com.bigshen.threads.ThreadStatus.lambda$main$1(ThreadStatus.java:40)
at com.bigshen.threads.ThreadStatus$$Lambda$2/2046562095.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread_Status_02" #13 prio=5 os_prio=0 tid=0x000000001fbee800 nid=0x38d0 in Object.wait() [0x00000000202ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b823090> (a java.lang.Class for com.bigshen.threads.ThreadStatus)
at java.lang.Object.wait(Object.java:502)
at com.bigshen.threads.ThreadStatus.lambda$main$0(ThreadStatus.java:28)
- locked <0x000000076b823090> (a java.lang.Class for com.bigshen.threads.ThreadStatus)
at com.bigshen.threads.ThreadStatus$$Lambda$1/214126413.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread_Status_01" #12 prio=5 os_prio=0 tid=0x000000001ee5f800 nid=0x3dfc waiting on condition [0x000000001f9ff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.bigshen.threads.ThreadStatus$1.run(ThreadStatus.java:15)
at java.lang.Thread.run(Thread.java:745)
"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001edab800 nid=0xd68 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
....
可以看到:线程Thread_Status_01
为TIMED_WAITING
超时等待状态、线程Thread_Status_02
为WAITING
等待状态、
线程Thread_Status_03
为TIMED_WAITING
超时等待状态,表明线程3已结执行了sleep方法,获得了锁、
线程Thread_Status_04
为BLOCKED
阻塞状态,因为线程4和线程3再抢夺同一个对象锁,线程3获得了锁,线程4阻塞。
2. 线程的生命周期
Java线程既然能够创建,势必也能被销毁,所以线程是存在生命周期的。
线程一共有6种状态新建(NEW)
、运行(RUNNABLE)
、超时等待(TIMED_WAITING)
、等待(WAITING)
、阻塞(BLOCKED)
、终止(TERMINATED)
3. 线程的启动
线程的启动并不是直接调用run()
方法,而是调用start()
方法,源码中可以看到真正调用的是native的start0()
方法,底层为C语言的逻辑实现。
private native void start0();
4. 线程的终止
线程的终止也并不是简单的调用stop()
命令,首先它是一个过期的方法,根本原因是stop方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。
要去优雅的中断一个线程,在线程中提供了一个interrupt()
方法。底层调用的是native的interrupt0()
方法,同样为C语言实现。
private native void interrupt0();
下面以三个demo来演示线程的终止。
样例1:主线程创建线程Thread_end_demo
,该线程内部根据线程是否是终止状态来决定是否继续执行;然后主线程在休眠100毫秒后终止Thread_end_demo
线程。
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class ThreadEndDemo implements Runnable {
@Override
public void run() {
// while (true) {
while (!Thread.currentThread().isInterrupted()) {
// do something
System.out.println(Thread.currentThread().getName() + " ---- " + Thread.currentThread().isInterrupted() + " ----- " + new Date());
}
}
public static void main(String[] args) {
Thread thread = new Thread(new ThreadEndDemo(), "Thread_end_demo");
thread.start();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("set child thread interrupt at" + new Date());
thread.interrupt();
}
}
运行结果
...
Thread_end_demo ---- false ----- Mon May 18 10:41:46 CST 2019
Thread_end_demo ---- false ----- Mon May 18 10:41:46 CST 2019
Thread_end_demo ---- false ----- Mon May 18 10:41:46 CST 2019
Thread_end_demo ---- false ----- Mon May 18 10:41:46 CST 2019
Thread_end_demo ---- false ----- Mon May 18 10:41:46 CST 2019
set child thread interrupt atMon May 18 10:41:46 CST 2019
Thread_end_demo ---- false ----- Mon May 18 10:41:46 CST 2019
可以看到在主线程调用子线程的interrupt()方法进行终止后,子线程成功终止。
样例2:
演示当主线程终止子线程时,子线程如果是休眠状态能否成功终止?
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class ThreadEndDemo2 implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " throw InterruptedException : " + e + "; at " + new Date());
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " ---------- " + Thread.currentThread().isInterrupted() + " ---- " + new Date());
}
}
public static void main(String[] args) {
Thread thread = new Thread(new ThreadEndDemo2(), "Thread_end_demo");
thread.start();
System.out.println("main thread start sleep 3s at " + new Date());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread wakeup, set child thread interrupt at" + new Date());
thread.interrupt();
}
}
···运行结果
main thread start sleep 3s at Mon May 18 10:59:07 CST 2019
main thread wakeup, set child thread interrupt atMon May 18 10:59:10 CST 2019
Thread_end_demo throw InterruptedException : java.lang.InterruptedException: sleep interrupted; at Mon May 18 10:59:10 CST 2019
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.bigshen.threads.end.ThreadEndDemo2.run(ThreadEndDemo2.java:14)
at java.lang.Thread.run(Thread.java:745)
Thread_end_demo ---------- false ---- Mon May 18 10:59:10 CST 2019
Thread_end_demo ---------- false ---- Mon May 18 10:59:20 CST 2019
Thread_end_demo ---------- false ---- Mon May 18 10:59:30 CST 2019
Thread_end_demo ---------- false ---- Mon May 18 10:59:40 CST 2019
Thread_end_demo ---------- false ---- Mon May 18 10:59:50 CST 2019
Thread_end_demo ---------- false ---- Mon May 18 11:00:00 CST 2019
Thread_end_demo ---------- false ---- Mon May 18 11:00:10 CST 2019..
...
可以看到,当线程Thread_end_demo
是在休眠状态下时,如果主线程尝试将其终止,子线程会收到该终止指令并抛出 InterruptedException,但并不会真正将线程终止,而且子线程的isInterrupted()
状态也被重置为了false。由此可以推断抛出的InterruptedException
会触发线程状态的复位,即将interrupt状态重新改为false。这里其实从hotspot底层jvm源码中可以看出,不再继续展开。
其实这里设计的意思已经很明显了:如果一个线程处于休眠状态时被终止,因为该线程可能在休眠后还有其余业务逻辑需要执行,这个需要交由子线程自己去判断。所以系统并不会直接终止该线程,而是会将该线程唤醒并抛出InterruptedException
异常,交由子线程自己来判断是立刻终止、处理完一些业务后终止、还是不终止。
样例3: 当线程处于休眠状态下时进行终止
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class ThreadEndDemo2 implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
// 抛出InterruptedException异常,底层实现会触发线程 interrupt 状态复位
System.out.println(Thread.currentThread().getName() + " -------- " + Thread.currentThread().isInterrupted() + "; throw InterruptedException : " + e + "; at " + new Date());
// e.printStackTrace();
//TODO do something
// 终止线程
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " ---------- " + Thread.currentThread().isInterrupted() + " ---- " + new Date());
}
}
public static void main(String[] args) {
Thread thread = new Thread(new ThreadEndDemo2(), "Thread_end_demo");
thread.start();
System.out.println("main thread start sleep 3s at " + new Date());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread wakeup, set child thread interrupt at" + new Date());
thread.interrupt();
}
}
... 运行结果
main thread start sleep 3s at Mon May 18 11:19:53 CST 2019
main thread wakeup, set child thread interrupt atMon May 18 11:19:56 CST 2019
Thread_end_demo -------- false; throw InterruptedException : java.lang.InterruptedException: sleep interrupted; at Mon May 18 11:19:56 CST 2019
Thread_end_demo ---------- true ---- Mon May 18 11:19:56 CST 2019
Process finished with exit code 0
此例子在InterruptedException
异常中将线程重新设置为终止状态。成功将线程终止。