高并发编程基础-02-线程基础知识说明

世上唯一不能复制的是时间,唯一不能重演的是人生,唯一不劳而获的是年龄。该怎么走,过什么样的生活,全凭自己的选择和努力。人生很贵,请别浪费!与智者为伍,与良善者同行。高并发编程基础-02-线程基础知识说明,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

引言

现在几乎所有操作系统都支持多任务执行,其中每个任务被视为一个进程。在每个进程内部,至少有一个线程在运行,线程也被称为轻量级进程。

线程可以看作是程序执行的一条路径,每个线程都有自己的局部变量表、程序计数器(指向当前正在执行的指令)以及各自的生命周期。现代操作系统通常支持同时运行多个线程。例如,在启动Java虚拟机(JVM)时,操作系统会创建一个新的进程(即JVM进程),并在该进程中生成多个派生或创建的线程。

线程生命周期

新建New

就绪Runnable

运行Running

阻塞Blocked

就绪Runnable

运行Running

终止Terminated

等待Waiting

唤醒Notify

超时等待Timed Waiting

唤醒Notify

在JDK17版本的JVM线程的生命周期共7个状态,可以在java.lang.Thread.State枚举类看到,具体如下:

  1. 新建状态(New):当一个Thread类或其子类的对象被创建时,该线程处于新建状态。此时它尚未启动,即没有开始执行run()方法。
  2. 就绪状态(Runnable):当线程对象调用了start()方法之后,该线程进入就绪状态。此时该线程已经有了执行资格,只等待CPU的调度,也就是分配时间片。
  3. 运行状态(Running):当就绪状态的线程获得了CPU时间片,开始执行run()方法时,该线程进入运行状态。
  4. 阻塞状态(Blocked):在某些情况下,线程可能会暂时失去对CPU的控制权,暂停执行。这时线程进入阻塞状态。例如,线程调用了sleep()方法、I/O操作、等待synchronized锁等都会使线程进入阻塞状态。
  5. 等待状态(Waiting):当线程执行wait()、join()、park()等方法之后,线程进入等待状态。此时线程不会占用CPU资源,也不会释放持有的锁,需要其他线程的唤醒才能继续执行。
  6. 超时等待状态(Timed Waiting):与等待状态类似,但是可以设置等待的时间。当线程调用了带有时间参数的sleep()、wait()、join()方法或者LockSupport.parkNanos()、LockSupport.parkUntil()方法时,线程进入超时等待状态。
  7. 终止状态(Terminated):线程执行完run()方法后,或者出现异常而结束时,线程进入终止状态。此时线程已经彻底结束,不会再回到其他状态。

这些线程状态在Java中非常重要,理解它们的含义和转换规则有助于我们编写高效、正确的多线程程序。

线程的生命周期是从新建状态开始,通过调用 start() 方法进入可运行状态,然后可能进入阻塞、等待或者被中断,最后进入终止状态。JVM 管理线程状态的转换,可以通过 Thread 类的状态相关方法来查询当前线程的状态。

Running状态的转换

  • 直接进入 TERMINATED 状态,比如用JDK已经不推荐使用的stop法或者判断某个逻辑标识。
  • 进入 BLOCKED 状态,比如调用了 sleep 或者 wait 方法而加入了 waitSet 中。
  • 进行某个阻塞的 IO 操作,比如因网络数据的读写而进入了 BLOCKED 状态获取某个锁资源,从而加人到该锁的阻塞队列中而进入了 BLOCKED 状态。
  • 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE 状态。
  • 线程主动调用 yield 方法,放弃 CPU 执行权,进入 RUNNABLE 状态。

Blocked状态的转换

  • 直接进人 TERMINATED 状态,比如调用JDK已经不推荐使用的 stop 方法或者意外死亡(JVM Crash)。
  • 线程阻塞的操作结束,比如读取了想要的数据字节进入到 RUNNABLE 状态。
  • 线程完成了指定时间的休眠,进入到了 RUNNABLE 状态。
  • Wait 中的线程被其他线程 notify/notifyall 唤醒,进入RUNNABLE状态。
  • 线程获取到了某个锁资源,进人 RUNNABLE 状态。
  • 线程在阻塞过程中被打断,比如其他线程调用了 interrupt 方法,进入RUNNABLE状态。

Terminated状态的形成

  • 线程运行正常结束,结束生命周期。
  • 线程运行出错意外结束。
  • JVM Crash,导致所有的线程都结束。

线程的创建

创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式。

  • 重写Thread的run方法。
  • 实现 Runnable 接口的run方法,并且将Runnable 实例用作构造Thread 的参数。
public class MyThread extends Thread {
    public void run() {
        // 定义线程执行的任务
        System.out.println("This is a new thread.");
    }
    
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 启动线程
    }
}

public class MyRunnable implements Runnable {
    public void run() {
        // 定义线程执行的任务
        System.out.println("This is a new thread.");
    }
    
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable); // 将实现了 Runnable 接口的对象作为参数传递给 Thread 类的构造方法
        thread.start(); // 启动线程
    }
}

线程中的策略模式

无论是 Runnable 接口的 run() 方法,还是 Thread 类本身的 run() 方法,都遵循了将线程的控制逻辑与业务逻辑分离的原则,以实现职责分明、功能单一的设计思想。这种设计方式与 GoF(Gang of Four)设计模式中的策略模式有相似之处。

在策略模式中,将可变的算法封装成独立的策略类,并通过接口或抽象类与调用者进行解耦。调用者可以根据需要选择不同的策略来完成特定的任务。类似地,Java 中的线程创建方式也将线程的执行逻辑封装在一个单独的类(实现 Runnable 接口或继承 Thread 类)中,通过调用 start() 方法来启动线程。

使用这种设计模式,可以使线程控制逻辑与业务逻辑分离,提高代码的可维护性和可扩展性。例如,可以根据不同的业务需求,定义不同的 Runnable 实现类或 Thread 子类,并在启动线程时选择合适的线程对象,从而实现不同的业务逻辑。

总结来说,Java 中线程的创建方式与策略设计模式相似,都体现了将控制逻辑与具体业务逻辑分离的设计原则,以实现代码的灵活性和可扩展性。

线程中的Runnable复用

重写 Thread 类的 run() 方法和实现 Runnable 接口的 run() 方法有一个关键的不同点。Thread 类的 run() 方法是无法共享的,也就是说,一个线程的 run() 方法不能被另一个线程当作自己的执行单元。相比之下,使用 Runnable 接口可以实现线程执行单元的共享。通过传递同一个实现了 Runnable 接口的对象给多个 Thread 实例,可以使多个线程共享同一个执行单元,从而提高代码的复用性和可维护性。

public class MyRunnable implements Runnable {
    public void run() {
        // 定义线程执行的任务
        System.out.println("This is a new thread.");
    }
    
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable); 
        thread.start(); // 启动线程
        Thread thread2 = new Thread(myRunnable); 
        thread2.start(); // 启动线程
    }
}

例子

创建线程

package engineer.concurrent.battle.onebasic;

import java.util.concurrent.TimeUnit;

public class TryConcurrent {

    public static void main(String[] args) {
        new Thread(TryConcurrent::writeCode).start();
        listenMusic();
    }

    private static void listenMusic() {
        for(;;){
            System.out.println("music is good");
            sleep(1);
        }
    }


    private static void writeCode() {
        for(;;){
            System.out.println("write code and work hard");
            sleep(1);
        }
    }

    private static void sleep(int i) {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

多线程排队模拟

/**
 * 叫号机排队模拟,通过多线程并发
 */
public class TicketWindow extends Thread {
    private final String name;
    private final static int MAX = 100;
    private static AtomicInteger index = new AtomicInteger(1);

    public TicketWindow(String name) {
        this.name = name;
    }

    public void run() {
        while (index.get() <= MAX) {
            System.out.println(name + "柜台正在排队,排队号码为:" + index);
            index.getAndIncrement();
        }
    }

    public static void main(String[] args) {
        new TicketWindow("一号窗口").start();
        new TicketWindow("二号窗口").start();
        new TicketWindow("三号窗口").start();
        new TicketWindow("四号窗口").start();
    }
}

参考

  • 《Java高并发编程详解:多线程与架构设计》
  • Java Thread Doc

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/221968.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!