很多开发者在初学多线程的时候.比较关注同步原语,线程池,future等api.这当然没有错,然而当你想要深入理解一种技术的时候,必须把最基础,最核心的概念弄清楚.不然的话上层知识的堆砌都是无本之源。java多线程是一个很复杂的知识体系和话题。可以写成书籍,市面上也有不少介绍多线程知识的书籍。本篇文章主要讲解与thread相关的基础概念。帮助读者更加深入理解这个基础的api相关的概念。
1. 进程和线程
- 进程是运行着的程序
程序是是指令和数据的集合
-
- 线程是"轻量级"的进程,是计算机可以调度的的最小单元.线程不能单独存在,必须依附于进程,一个进程可以包含多个线程
计算机多线程,多进程模型示意图
- 线程是"轻量级"的进程,是计算机可以调度的的最小单元.线程不能单独存在,必须依附于进程,一个进程可以包含多个线程
1.1 为什么要使用多进程/多线程?
为什么使用多线程/多进程模型,可以说出很多东西,总结起来一句话,那就是为了充分利用现代计算机核心/线程资源,达到提升应用程序计算效率的目的.通俗的讲,就是让程序跑的更快.
1.2 多进程/多线程创建方式及对应Linux C API
- 启动多个进程
- 使用fork函数创建进程
- 使用pthread_create函数创建线程
在windows平台下,谷歌浏览器就是一个典型的多进程应用程序,当然同时也使用有多线程技术
1.3典型多进程模式编程语言
python
虽然python也有线程api,但是由于gil的存在致使python无法真正在同一时刻并行执行任务。因此多线程api就显得比较鸡肋,在python中更多的是使用进程api
1.4典型多线程编程模式语言
JAVA,C#
当然可能还有其它的语言也是多线程模式语言,笔者并不太了解。这里仅仅以笔者用过的语言举例
2.java线程API
2.1 线程的创建
- 创建一个类,实现Runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello,world");
}
}
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable());
}
- 创建一个类,继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello,world");
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
}
- Java8 通过Lambda创建
Thread t1=new Thread(()->{
System.out.println("hello,world");
});
2.11线程创建建议
通过new Thread(Runnable对象,函数名)创建,不要通过new子类创建.
2.12创建线程的高级方法
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize)
- stackSize为0含义
stackSize值为0是默认行为,即使用通过xss设置的值
- stackSize是否可以是任意long范围内的正整数?
可否任意大,最大值受什么影响?
JAVA线程中可以设置任意值,即long范围内的任意值,这可能跟你想的不一样,但是不管如何java程序都是要运行的,在linux上不能超过Linux对进程的限制。如上图通过ulimit -a可以看到stack size值,我使用的是centos 7.6,默认是8M,即不能超过8M。当然这个值是可以调整的
还可以通过在程序启动时通过Xss指定栈大小
虽然程序中可以设置任意小,但是不能通过-Xss设置过小的值,否则会报错
为什么?因为这里是对进程的全局设置,值太小可能会导致程序无法正常运行
我们在启动时通过指定xss将值设置为500M再看看效果
通过命令查看,值确实好像是生效了。
这是否意味着真的设置生效了呢。答案是否定的,前面也说了java程序要运行在操作系统上。不管是windows还是linux都有栈大小限制。
以下是结论:
虽然Flag设置成功了,但是值会被忽略
通过-Xss设置的值过大时会被忽略
详细信息请查看openjdk bug报告
On Linux we do check if -Xss/ThreadStackSize is larger than ulimit value.
If ThreadStackSize exceeds the system limit, the stack size is quietly
reduced to ulimit value
2.2 线程的启动
public static void main(String[] args) {
Thread t1=new Thread(()-> System.out.println("hello,world"));
t1.run();
t1.start();
}
run方法无法启动线程,会像普通函数调用一样同步执行,不会创建线程
2.21线程start是否是线程安全的?
public static void main(String[] args) {
Thread t1=new Thread(()-> System.out.println("hello,world"));
for (int i = 0; i < 3; i++) {
t1.start();
}
}
答案是肯定的,为什么,看源码可以看到start方法是被synchronized修饰的。thread其它相关的很多api也都有synchronized修饰。其实想一想就会明白。如果线程的创建、停止,暂停都无法保证安全,程序的可靠性将受到很大挑战。
2.22调用线程start方法是否会马上执行代码
Java里Thread start方法注释Causes this thread to begin execution;
在英语里面,带to的往往不代表正在进行,而是将要进行。
实际上在linux上,java创建线程是通过调用pthread_create 创建的
pthread_t thread;
int ret = pthread_create(&thread, NULL, &thread_function, NULL);
由于计算机资源的有限性,不可能所有的请求来了linux马上都为你创建一个线程。linux收到请求后将根据自己的调度策略在合适的时间为请求者创建线程。
2.23 一个java进程可以启动多少个线程,受哪些因素影响?
通过前面介绍可以知道,计算机为用户进程分配的资源都是有限的,并且计算机本身的资源也是有限的。因此不可能无限创建。但是线程创建的个数受哪些因素影响呢?
Linux用户进程最大栈尺寸限制
这个前面已经介绍过了。程序启动的时候可以以通过xss设置栈大小。创建thread对象时也可以指定栈大小。由于最大栈尺寸的限制,线程占用的栈空间越大,可创建的线程数量就越少
Linux最大进程数限制
通过pid_max可以查看linux最大进程数量是多少
看到这里很多同学可能会懵逼。我创建的是线程,为什么会受到linux最大进程数量的限制呢?其实了解了linux线程pid分配机制后就会明白了。比如一个进程的id是10.进程启动后创建的10个线程,那么这些线程的id就会顺着进程id递增分配。分别是11,12,13,14…也就是说,创建一个线程就会消耗一个linux进程pid 由于pid总量是有限制的,因此总的线程数不能超过linux pid max限制
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Thread t=new Thread(()->{
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
我们的demo程序创建的10个线程。通过jps看到程序运行的进程id是25747
我们通过top -H -p查看java demo程序里的子线程。可以看到线程的id是顺着25747依次再分配的
2.24线程的阻塞以及如何获取线程执行结果
@Data
public class Student {
private String name;
}
public static void main(String[] args) throws Exception{
Student student=new Student();
Thread t=new Thread(()->{
//do some work
student.setName("Baidu");
});
t.start();
t.join();
System.out.println(student.getName());
}
以上我们通过thread.join阻塞当前线程直到子线程运行完成,后面就可以输出结果了
2.25封装自己的Future
public interface MyCallable<T> {
T call();
}
public class MyFuture<T> extends Thread{
T result=null;
private MyCallable<T> task;
public MyFuture(MyCallable<T> task){
this.task = task;
}
@Override
public void run() {
//do someWork
result= task.call();
}
public T myGet(){
try {
this.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
}
public static void main(String[] args) throws Exception{
MyFuture<String> myFuture=new MyFuture<>(()->"hello,world");
myFuture.start();
String s1 = myFuture.myGet();
}
前面我们也说过,在设计上尽量不要继承Thread,下面展示一个改进版
public class MyFutureV2<T> implements Runnable{
T result=null;
private MyCallable<T> task;
public MyFutureV2(MyCallable<T> task){
this.task=task;
}
@SneakyThrows
@Override
public void run() {
result= task.call();
synchronized (this){
notify();
}
}
@SneakyThrows
public T myGet() {
synchronized (this){
wait();
}
return result;
}
}
public static void main(String[] args) throws Exception{
MyFutureV2<String> myFuture=new MyFutureV2<>(()->"hello,world");
Thread t=new Thread(myFuture);
t.start();
String s = myFuture.myGet();
}
不继承thread对象,我们就没有join方法可以使用了。这里我们使用的是对象的wait和notify机制。
我们封装很多高级的api都离不开基础知识,只有理解并掌握他们了才能灵活运用。
2.3线程的终止
2.31为什么线程的stop方法被弃用了
Java Thread stop方法的注释:Forces the thread to stop executing.翻译成人话就是强制终止当前运行的线程,太过暴力
thread stop导致线程不安全示例
public class StopThread extends Thread{
private int i=0;
private int j=0;
@Override
public void run(){
synchronized (this) {//增加同步锁,确保线程安全
++i;
try {
//休眠5秒,模拟耗时操作
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
++j;
}
}
/**
* 打印i和j
*/
public void print(){
System.out.println("i="+i+" j="+j);
}
}
public static void main(String[] args) throws Exception{
StopThread thread=new StopThread();
thread.start();
try {
//休眠1秒,确保i变量自增成功
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
thread.stop();
while(thread.isAlive()){//确保线程已经终止
}
//输出结果
thread.print();
}
我们强制线上程序,最终导致程序执行状态不正确,这是很严重的问题!
正在做的事情要让它做完,不然后果很严重。
2.32interrupt方法无法中断线程
public class MyThreadInterruptable implements Runnable {
@Override
public void run() {
while (true){
System.out.println("正在运行中");
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThreadInterruptable myThread = new MyThreadInterruptable();
Thread thread = new Thread(myThread);
thread.start();
// 为了使效果更明显,使当前线程休眠一秒
Thread.sleep(1000);
// 中断线程
thread.interrupt();
}
interrupt方法只是发出中断信号,本身并不能终止线程执行大家切不可望文生义
2.33线程终止正确姿势1
@Override
public void run() {
while (true){
// 判断线程是否被中断
if(Thread.interrupted()){
break;
}
System.out.println("正在运行中");
}
}
public static void main(String[] args) throws InterruptedException {
MyThreadInterruptable myThread = new MyThreadInterruptable ();
Thread thread = new Thread(myThread);
thread.start();
// 为了使效果更明显,使当前线程休眠一秒
Thread.sleep(1000);
// 中断线程
thread.interrupt();
}
public class MyThreadInterruptable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("sleep is interrupted");
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception{
MyThreadInterruptable myThread = new MyThreadInterruptable ();
Thread thread = new Thread(myThread);
thread.start();
// 中断线程
thread.interrupt();
}
第一种是写个死循环查看interrupt状态是否改变。第二种是catch住InterruptedException异常。thread.interrupt()会抛出中断信号
> 网上有不少示例都是一大抄。使用while死循环来监听状态改变,这并不是一件有趣的事。为了响应中断写个死循环,拜拜浪费cpu资源。
多线程技术应用
3.1 idea线程调试thread断点模式
public static void main(String[] args) throws Exception{
for (int i = 0; i < 3; i++) {
Thread t=new Thread(()->{
while (true){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t"+i);
t.start();
}
SpringApplication.run(ConcurrentdemoApplication.class, args);
}
什么意思呢。有时候我们调度多线程程序的时候,断点老是补干扰,一次执行还没完,断点又来了。使用thread模式就可以解决这种苦恼,专注于调度单个线程内的执行逻辑。
3.2快速定位失去响应代码
有时候我们的程序在启动的时候一直卡着不动,日志也不输出了,到底问题出在了哪里呢?
@RestController()
@RequestMapping("/slow")
public class SlowController {
@RequestMapping("/slowTest")
public void slowTest(){
try {
Thread.sleep(1500000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void afterPropertiesSet() throws Exception {
URL url = new URL("http://localhost:7001/slow/slowTest");
URLConnection urlConnection = url.openConnection();
HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
InputStream inputStream = httpUrlConnection.getInputStream();
}
以上模拟一个耗时很长的接口,我们在另一个程序启动的时候调用这个接口
程序好像卡住了
调试模式下点击暂停应用按钮,程序断在当前正在执行的行
切到调试面板,找到当前应用程序内执行的方法
最终通过堆栈分析,我们定位到失去响应的代码
3.3 CPU负载过高分析示例
public class HeavWorkTask implements Runnable {
private int index;
public HeavWorkTask(int i) {
this.index = i;
}
@Override
public void run() {
while (true) {
if (index != 0) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws Exception{
for (int i = 0; i < 10; i++) {
Thread t=new Thread(new HeavWorkTask(i));
t.start();
}
SpringApplication.run(ConcurrentdemoApplication.class, args);
}
我们先通过jps拿到java进程id,然后通过 pidstat -t -p 来查看分析哪个线程占用cpu资源过高。我们看到thread_0 cpu占用达到了101。
下面我们看一下java堆栈信自的内容是什么样子的
可以看到最前面是线程名称。我们可以通过cpu过高线程thread-0名称来过滤
也可以通过拿到线程的id然后转成十六进制根据tid一栏来搜索。根据id结果更精确。但是需要注意的是这里的id 是线程的nativeid,并不是java程序给线程分配的id。
3.4.2根据线程Native Id过滤堆栈信息
将线程Native Id转为十六进制显示形式,CPU占用高的线程ID18094对应的十六进制为46ae