并发编程之一分布式与高并发之并发编程的发展

本文深入探讨了Java中的并发编程,包括线程的创建、运行原理以及线程状态。介绍了如何通过多线程提升CPU利用率,以及并发与并行的区别。详细阐述了线程的生命周期、状态,如NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING和TERMINATED。同时,讨论了线程的中断机制,强调了interrupt方法和InterruptedException在处理阻塞操作中的角色。最后,通过实际的线程状态分析工具jstack展示了如何排查CPU占用过高和死锁问题。

分布式与高并发之并发编程的发展

目标(了解线程)

  • 了解并发和并发的价值
  • 了解线程
  • java中的线程的实现
  • 多线程的基本原理
  • 线程的启动和停止

并发

  • 高并发
    当前系统能够同时承载的并发数
    一般通过两个值来衡量当前系统的并发数
    TPS:每秒处理的事务的数量
    QPS:每秒处理的查询的数量
  • 如何支撑高并发?
    核心点:硬件资源
    CPU,核心数:代表了当前程序同时并行的任务数量
    内存,用于IO性能的提高,储存热点数据等等
    磁盘,用高效读写的磁盘提升性能
    网卡,万兆千兆的网卡提升数据传输速度
  • 何合理的利用资源呢?
    如一个软件需要用到
    CPU -->线程资源 8核,同时可以运行8个线程
    IO --> 数据库的交互 --> 数据刷到磁盘, 优化点:内存/缓存/异步刷盘策略…;数据库分库分表,分布式缓存、分布式消息中间件
    单节点遇到瓶颈,采用多个计算机组成一个分布式计算机
    最终的收益:都是为了QPS、TPS的提升 qps 1000 --> 10000
    最终的目的:配合程序一起提升系统的系统

多线程技术

  • 什么是线程
    java程序 -> .java源文件(磁盘) ->JVM(编译) .class -> main方法运行这个程序(加载到程内存中) -->产生一个进程
    CPU来执行这个进程中的指令
    假设进程中,有一个从磁盘加载一个文件保存到数据库的操作
    涉及到CPU的运算效率,和磁盘的IO效率 CPU的效率 > 磁盘的效率 CPU会产生阻塞
    CPU是计算机中的核心资源(CPU阻塞会造成CPU资源的浪费)
    如何提升CPU的利用率?
    思考:当某个进程因为IO阻塞时,能否切换到其他的进程来执行呢?
    多道程序设计:多进程,让多个进程同时加载到内存,进程之间是相互隔离不影响。
    分时系统:利用CPU的时间片的切换,当某个进程阻塞时可以切换到其他进程执行

线程 --> 即轻量级进程
一个进程中 -> 可以存在N个线程
1.加载磁盘 -> 将耗时的操作分配给线程执行
2.保存到数据库
3…
拓展概念:
并行:同一个时刻有多少个线程可以同时运行
并发:这个系统能够最高支撑处理的访问是多少

线程的特征

  • 异步 (用户注册 --> 异步发送email、短信)
  • 并行(依赖CPU核数)

Java中如何实现线程

  • 继承Thread类
  • 实现Runable接口
  • Callable/Future
public class ThreadDemo implements Runnable{
   @Override
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println("当前线程会被执行的代码");
   }

   public static void main(String[] args) {
       new Thread(new ThreadDemo()).start(); //不需要等待这个程序的处理结果
       new Thread(new SmsSenderTask()).start(); //不需要等待这个程序的处理结果,比如发送短信直接创建一个SmsSenderTask类就可以了
       //为什么用start方法不用run方法,因为start方法是JVM层面的,等待CPU的调度就可以了
       //start方法执行后会回调run方法
       //run方法就只是一个实例方法的调用不能算是多线程
       System.out.println("Main方法的输出结果");
   }
}

多线程的基本原理 (java中开启线程的执行过程)

线程的start方法,实际上底层做了很多事情,具体的实现简图如下,画得不一定工整,但是能够表达大
概意思就行。
OS调度算法有很多,比如先来先服务调度算法(FIFO)、最短优先(就是对短作业的优先调度)、时
间片轮转调度等。

在这里插入图片描述

线程的生命周期

线程从开始 --> 结束
start()启动一个线程
当前线程中的执行逻辑执行完毕后自动销毁,run()结束
线程的其他状态 --> 下方代码示例

// 线程的运行状态
public class ThreadStatusDemo {

    public static void main(String[] args) {
        new Thread(()->{
            while(true){
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"TIME_WAITING").start();

        new Thread(()->{
            while(true){
                synchronized (ThreadStatusDemo.class){
                    try {
                        ThreadStatusDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"WAITING");

        new Thread(new BlockDemo(),"BLOCKED-DEMO-01").start();
        new Thread(new BlockDemo(),"BLOCKED-DEMO-02").start();
    }

    static class BlockDemo extends Thread{
        @Override
        public void run() {
            synchronized (BlockDemo.class){
                while(true){
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
打印:
1.命令:jps
16484 ThreadStatusDemo
8756 Jps
15212 Launcher
16012 Launcher
8892
2.命令:jstack 16484
"BLOCKED-DEMO-02" #19 prio=5 os_prio=0 tid=0x000002caa99e7000 nid=0x3b38 waiting for monitor entry [0x000000dc24efe000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.threaddemo.ThreadStatusDemo$BlockDemo.run(ThreadStatusDemo.java:40)
        - waiting to lock <0x000000076bbf11d0> (a java.lang.Class for com.example.threaddemo.ThreadStatusDemo$BlockDemo)
        at java.lang.Thread.run(Thread.java:748)

"BLOCKED-DEMO-01" #17 prio=5 os_prio=0 tid=0x000002caa99e5800 nid=0x14b8 waiting on condition [0x000000dc24dff000]
   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.example.threaddemo.ThreadStatusDemo$BlockDemo.run(ThreadStatusDemo.java:40)
        - locked <0x000000076bbf11d0> (a java.lang.Class for com.example.threaddemo.ThreadStatusDemo$BlockDemo)
        at java.lang.Thread.run(Thread.java:748)

//线程的运行状态
运行上述示例,打开终端命令,输入"jps"(显示当前所有Java进程pid);
根据获取到的pid, 通过jstack pid ,可以打印指定Java进程ID的堆栈信息
通过堆栈信息,可以看到线程的运行状态

线程的状态

//线程的运行状态
通过上面这段代码可以看到,线程在运行过程中,会存在几种不同的状态,一般来说,在Java中,线程
的状态一共是6种状态,分别是

NEW:初始状态,线程被构建,但是还没有调用start方法
RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞
也分为几种情况
	Ø 等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列
	Ø 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么
	jvm会把当前的线程放入到锁池中
	Ø 其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,JVM会把
	当前线程设置为阻塞状态,当sleep结束、join线程终止、io处理完毕则线程恢复
WAITING: 等待状态
TIME_WAITING:超时等待状态,超时以后自动返回
TERMINATED:终止状态,表示当前线程执行完毕

在这里插入图片描述

线程的终止

我们知道Thread提供了线程的一些操作方法,比如stop、suspend等,这些方法可以终止一个线程或者
挂起一个线程,但是这些方法都不建议大家使用。原因比较简单,
举个例子,假设一个线程中,有多个任务在执行,此时,如果调用stop方法去强行中断,那么这个时候
相当于是发送一个指令告诉操作系统把这个线程结束掉,但是操作系统的这个结束动作完成不代表线程
中的任务执行完成,很可能出现线程的任务执行了一半被强制中断,最终导致数据产生问题。这种行为
类似于在linux系统中执行 kill -9类似,它是一种不安全的操作。
那么除了这种方法之外,还有什么方式可以实现线程的终止呢?要了解这个问题,我们首先需要知道,
一个线程什么情况下算是终止了。

我们分析一下下面这段代码,通过start()启动一个线程之后,本质上就是执行这个线程的run方法。
那么如果这个线程在run方法执行完之前,一直处于运行状态,直到run方法中的指令执行完毕,那么这
个线程就会被销毁。
// 普通线程的执行
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("MyThread run()");
    }
}
//测试类
public class ThreadTest {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start();
    }
}
在正常情况下,这个线程是不需要人为干预去结束的。如果要强制结束,只能走stop这个方法。
那在哪些情况下,线程的中断需要外部干预呢?
1.线程中存在无限循环执行,比如while(true)循环
2.线程中存在一些阻塞的操作,比如sleep、wait、join等。
  • 存在循环的线程
假设存在如下场景,在run方法中,存在一个while循环,因为这个循环的存在使得这个run方法一直无
法运行结束,这种情况下,如何终止呢?
// 存在循环的线程
public class MyThread extends Thread{    
    @Override
    public void run() {
        while(true){
            System.out.println("MyThread.run()");
        }
    }
}
//测试类
public class ThreadTest {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start();
    }
}
按照我们开发的思维来说,首先要解决的就是,while(true)这个循环,必须要有一个结束条件,其次是
要在其他地方能够修改这个结束条件让该线程感知到变化。假设我们把while(true)改成while(flag),这
个flag可以作为共享变量被外部修改,修改之后使得循环条件无法被满足,从而退出循环并且结束线
程。
这段逻辑其实非常简单,其实就是给了线程一个退出的条件,如果没有这个条件,那么线程将会一直运
行。
实际上,在Java提供了一个 interrupt 方法,这个方法就是实现线程中断操作的,它的作用和上面讲的
这个案例的作用一样。

interrupt方法

当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行
了,至于什么时候中断,取决于当前线程自己。
线程通过检查资深是否被中断来进行相应,可以通过isInterrupted()来判断是否被中断。
// 先看一个简单的例子说明
public class InterruptExample implements Runnable{

    public static void main(String[] args) {
        Thread thread1 = new Thread(new InterruptExample());
        thread1.interrupt();//发送中断信号
    }
    @Override
    public void run() {
        //todo 业务逻辑···········
        //对外提供一个信号,通过这个信号来判断这个线程是否可以结束

        //当run()方法无法自动结束的情况下,去中断线程
        //没有以下情况是不需要被中断的!
        //1.while(true){}
        //2.阻塞
        try {
            InterruptExample.class.wait();//阻塞
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //3.
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
// 慢慢来看interrupt是如何使用的
public class InterruptExample implements Runnable{

    //可以在外部使用标记的方式来作为中断的一种方法思路
    static boolean flag = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(new InterruptExample());
//        thread1.interrupt();//发送中断信号
        flag = false;
    }
    @Override
    public void run() {
        //todo 业务逻辑···········
        //如果让线程友好的结束,只有当前run方法中的程序知道的时候才可以
        //由当前线程本身来决定中断
        while(flag){

        }
    }
}
//线程中提供了一个方法来获取中断的状态
public class InterruptExample implements Runnable{

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptExample());
        thread1.start();
        //先执行5S再去触发中断
        Thread.sleep(5000);
        thread1.interrupt();//发送中断信号
    }

    @Override
    public void run() {
        //todo 业务逻辑···········
        //如果让线程友好的结束,只有当前run方法中的程序知道的时候才可以
        //由当前线程本身来决定中断
        //Thread.currentThread().isInterrupted()获取中断的状态
        while(!Thread.currentThread().isInterrupted()){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                //java中通过铺货InterruptedException 异常来告诉开发者这里可能会发生中断
                //中断后如何处理,这里的选择在于开发者
                //如果你想让其中断 添加下方这行代码进行中断
                Thread.currentThread().interrupt();//进行中断
            }
            System.out.println(Thread.currentThread().getName()+"--");
        }
    }
}
打印:
Thread-0--
Thread-0--
Thread-0--
Thread-0--
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.example.threaddemo.InterruptExample.run(InterruptExample.java:26)
	at java.lang.Thread.run(Thread.java:748)
Thread-0--
Thread-0--

这种方式和上边的定义 static boolean flag = true; 是一样的只是提供了一个更方便的方法
我们发现抛出异常后线程并没有终止仍然继续执行打印,这里涉及到一个复位的概念
我们平时在线程中使用的sleep、wait、join等操作,它都会抛出一个InterruptedException异常,为什么会抛出异常,是因为它在阻塞期间,必须要能够响应被其他线程发起中断请求之后的一个响应,而这个响应是通过InterruptedException来体现的。
interrupt()有两个功能
1.唤醒处于阻塞状态下的线程
2.修改中断的标记  由false --> true
在catch 住InterruptedException 异常这里
1.java中通过铺货InterruptedException 异常来告诉开发者这里可能会发生中断
2.中断后如何处理,这里的选择在于开发者
3.InterruptedException抛出来之后会恢复中断状态。(复位)
在这个异常中如果不做任何处理的话,我们是无法去中断线程的,因为当前的异常只是响应了外部对于这个线程的中断命令,同时,线程的中断状态也会复位
假如你希望他中断
直接在catch中添加
Thread.currentThread().interrupt();//进行中断
所以,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发
生,至于接下来怎么处理取决于线程本身,比如
1.直接捕获异常不做任何处理
2.将异常往外抛出
3.停止当前线程,并打印异常信息

isInterrupted是native修饰的方法 Hotspot源码中可以看到
官方源码下载:链接: http://hg.openjdk.java.net.

void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();

  if (!osthread->interrupted()) {
    osthread->set_interrupted(true);  //更新中断标记
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    OrderAccess::fence();
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark(); //唤醒被阻塞的方法

  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;

}

排查问题 --Thread Dump日志分析

  • CPU占用率很高,响应很慢
  • CPU占用率不高,但响应很慢
  • 线程出现死锁的情况
// 演示代码
@RestController
public class ThreadController {

    @GetMapping("/loop")
    public String dumpWhile(){
        new Thread(new WhileThread()).start();
        return "ok";
    }

    @GetMapping("/dead")
    public String dumpDeadLock(){
        Thread a = new ThreadRunA();
        Thread b = new ThreadRunB();
        a.start();
        b.start();
        return "ok";
    }
}
class WhileThread implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("Thread");
        }
    }
}
// 运行
nohup java -jar -Dserver.port=8088 thread-demo-0.0.1-SNAPSHOT.jar > all.log &

CPU占用率不高,但响应很慢

// 死锁
通过 curl http://127.0.0.1:8088/dead 演示死锁的场景

查看死锁问题的操作步骤如下:

  • 通过 jps 命令,查看java进程的pid
  • 通过`jstack 查看线程日志

如果存在死锁情况,Thread Dump日志里面肯定会给出Found one Java-level deadlock:信息。只要
找到这个信息就可以立马定位到问题并且去解决。

Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000026070c88 (object 0x00000007163b7d78, a
java.lang.Integer),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00000000260735c8 (object 0x0000000716649aa8, a
java.lang.Integer),
which is held by "Thread-1"

CPU占用率很高,响应很慢

有的时候我们会发现CPU占用率很高,系统日志也看不出问题,那么这种情况下,我们需要去看一下运
行中的线程有没有异常。

// 死循环
执行 curl http://127.0.0.1:8088/loop 这个方法,会出现一个线程死循环的情况。
  • 通过 top -c 动态显示进程及占用资源的排行榜,从而找到占用CPU最高的进程PID,得到的
    PID=80972
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
80972 root 20 0 7773456 296124 12904 S 100.2 1.8 0:38.83 java
  • 然后再定位到对应的线程, top -H -p 80972 查找到该进程中最消耗CPU的线程,得到
    PID=81122
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
81122 root 20 0 7773456 258504 12932 R 99.8 1.6 5:56.34 java
80972 root 20 0 7773456 258504 12932 S 0.0 1.6 0:00.00 java
  • 通过 printf “0x%x\n” 81122 命令,把对应的线程PID转化为16进制
[root@localhost test]# printf "0x%x\n" 81122
0x13ce2
  • 截止执行这个命令 jstack 80972 | grep -A 20 0x13ce2 查看线程Dump日志,其中-A 20表示
    展示20行, 80972表示进程ID, 0x13ce2表示线程ID
[root@localhost test]# jstack 80972 | grep -A 20 0x13ce2
"Thread-3" #30 daemon prio=5 os_prio=0 tid=0x00007f84500ce000 nid=0x13ce2
runnable [0x00007f84a78f7000]
java.lang.Thread.State: RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)
at java.io.FileOutputStream.write(FileOutputStream.java:326)
at
java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
- locked <0x00000006c812f1b0> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:482)
- locked <0x00000006c812f190> (a java.io.PrintStream)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
- locked <0x00000006c812f2d0> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.newLine(PrintStream.java:546)
- eliminated <0x00000006c812f190> (a java.io.PrintStream)
at java.io.PrintStream.println(PrintStream.java:807)
- locked <0x00000006c812f190> (a java.io.PrintStream)
at com.example.threaddemo.WhileThread.run(ThreadController.java:33)
at java.lang.Thread.run(Thread.java:748)

从上述内容可以看出,是WhileThread.run方法中,执行的逻辑导致CPU占用过高。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值