你真的懂Thread吗?深入理解java多线程基础知识之thread核心概念精讲

很多开发者在初学多线程的时候.比较关注同步原语,线程池,future等api.这当然没有错,然而当你想要深入理解一种技术的时候,必须把最基础,最核心的概念弄清楚.不然的话上层知识的堆砌都是无本之源。java多线程是一个很复杂的知识体系和话题。可以写成书籍,市面上也有不少介绍多线程知识的书籍。本篇文章主要讲解与thread相关的基础概念。帮助读者更加深入理解这个基础的api相关的概念。

1. 进程和线程

  • 进程是运行着的程序

程序是是指令和数据的集合

    • 线程是"轻量级"的进程,是计算机可以调度的的最小单元.线程不能单独存在,必须依附于进程,一个进程可以包含多个线程
      计算机多线程,多进程模型示意图
      多线程/进程模型示意

1.1 为什么要使用多进程/多线程?

为什么使用多线程/多进程模型,可以说出很多东西,总结起来一句话,那就是为了充分利用现代计算机核心/线程资源,达到提升应用程序计算效率的目的.通俗的讲,就是让程序跑的更快.

1.2 多进程/多线程创建方式及对应Linux C API

  • 启动多个进程
  • 使用fork函数创建进程
  • 使用pthread_create函数创建线程

在windows平台下,谷歌浏览器就是一个典型的多进程应用程序,当然同时也使用有多线程技术
多进程应用chrome

1.3典型多进程模式编程语言

python
虽然python也有线程api,但是由于gil的存在致使python无法真正在同一时刻并行执行任务。因此多线程api就显得比较鸡肋,在python中更多的是使用进程api

1.4典型多线程编程模式语言

JAVA,C#

当然可能还有其它的语言也是多线程模式语言,笔者并不太了解。这里仅仅以笔者用过的语言举例

2.java线程API

2.1 线程的创建

  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());
}



  1. 创建一个类,继承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();
}
  1. 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范围内的正整数?
    可否任意大,最大值受什么影响?
    ulimit

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状态是否改变。第二种是catchInterruptedException异常。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

在这里插入图片描述

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

国通快递驿站

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值