线程的基本概念

本文介绍了线程的基本概念,包括进程与线程的关系,以及JAVA中线程的抽象和状态。通过示例展示了如何创建并启动线程,探讨了常用线程方法,如sleep、start、interrupt和join。最后,讨论了多线程环境下可能出现的竞态条件和内存可见性问题,强调并发编程中的挑战和注意事项。
摘要由CSDN通过智能技术生成

按照规划,从本篇开始我们开启『并发』系列内容的总结,从本篇的线程开始,到线程池,到几种并发集合源码的分析,我们一点点来,希望你也有耐心,因为并发这块知识是你职业生涯始终绕不过的坎,任何一个项目都或多或少的要涉及一些并发的处理。

这一系列文章只能算是对并发这块基本理论知识的一个总结与介绍,想要成为并发高手,必然是需要通过大规模并发访问的线上场景应用,或许以后我有了相关经验了,再给你们做一点分享吧。

基本的进程线程概念

进程和线程算是操作系统内两个很基本、很重要的概念了,进程是操作系统中进行保护和资源分配的基本单位,操作系统分配资源以进程为基本单位。而线程是进程的组成部分,它代表了一条顺序的执行流。

系统中的进程线程模型是这样的:

image

 

进程从操作系统获得基本的内存空间,所有的线程共享着进程的内存地址空间。当然,每个线程也会拥有自己私有的内存地址范围,其他线程不能访问。

由于所有的线程共享进程的内存地址空间,所以线程间的通信就容易的多,通过共享进程级全局变量即可实现。

同时,在没有引入多线程概念之前,所谓的『并发』是发生在进程之间的,每一次的进程上下文切换都将导致系统调度算法的运行,以及各种 CPU 上下文的信息保存,非常耗时。而线程级并发没有系统调度这一步骤,进程分配到 CPU 使用时间,并给其内部的各个线程使用。

在分时系统中,进程中的每个线程都拥有一个时间片,时间片结束时保存 CPU 及寄存器中的线程上下文并交出 CPU,完成一次线程间切换。当然,当进程的 CPU 时间使用结束时,所有的线程必然被阻塞。

JAVA 对线程概念的抽象

JAVA API 中用 Thread 这个类抽象化描述线程,线程有几种状态:

  • NEW:线程刚被创建
  • RUNNABLE:线程处于可执行状态
  • BLOCKED、WAITING:线程被阻塞,具体区别后面说
  • TERMINATED:线程执行结束,被终止

其中 RUNNABLE 表示的是线程可执行,但不代表线程一定在获取 CPU 执行中,可能由于时间片使用结束而等待系统的重新调度。BLOCKED、WAITING 都是由于线程执行过程中缺少某些条件而暂时阻塞,一旦它们等待的条件满足时,它们将回到 RUNNABLE 状态重新竞争 CPU。

此外,Thread 类中还有一些属性用于描述一个线程对象:

  • private long tid:线程的序号
  • private volatile char name[]:线程的名称
  • private int priority:线程的优先级
  • private boolean daemon = false:是否是守护线程
  • private Runnable target:该线程需要执行的方法

其中,tid 是一个自增的字段,每创建一个新线程,这个 id 都会自增一。优先级取值范围,从一到十,数值越大,优先级越高,默认值为五。

Runnable 是一个接口,它抽象化了一个线程的执行流,定义如下:

public interface Runnable {

    public abstract void run();
}
复制代码

通过重写 run 方法,你也就指明了你的线程在得到 CPU 之后执行指令的起点。我们一般会在构造 Thread 实例的时候传入这个参数。

创建并启动一个线程

创建一个线程基本上有两种方式,一是通过传入 Runnable 实现类,二是直接重写 Thread 类的 run 方法。我们详细看看:

1、自定义 Runnable 实现

public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("hello world");
    }
}
复制代码
public static void main(String[] args) {
    Thread thread = new Thread(new MyThread());
    thread.start();
    System.out.println("i am main Thread");
}
复制代码

运行结果:

i am main Thread
hello world
复制代码

其实 Thread 这个类也是继承 Runnable 接口的,并且提供了默认的 run 方法实现:

@Override
public void run() {
    if (target != null) {
       target.run();
    }
}
复制代码

target 我们说过了,是一个 Runnable 类型的字段,Thread 构造函数会初始化这个 target 字段。所以当线程启动时,调用的 run 方法就会是我们自己实现的实现类的 run 方法。

所以,自然会有第二种创建方式。

2、继承 Thread 类

既然线程启动时会去调用 run 方法,那么我们只要重写 Thread 类的 run 方法也是可以定义出我们的线程类的。

public class MyThreadT extends Thread{
    @Override
    public void run(){
        System.out.println("hello world");
    }
}
复制代码
Thread thread = new MyThreadT();
thread.start();
复制代码

效果是一样的。

几个常用的方法

关于线程的操作,Thread 类中也给我们提供了一些方法,有些方法还是比较常用的。

1、sleep

public static native void sleep(long millis)
复制代码

这是一个本地方法,用于阻塞当前线程指定毫秒时长。

2、start

public synchronized void start()
复制代码

这个方法可能很多人会疑惑,为什么我通过重写 Runnable 的 run 方法指定了线程的工作,但却是通过 start 方法来启动线程的?

那是因为,启动一个线程不仅仅是给定一个指令开始入口即可,操作系统还需要在进程的共享内存空间中划分一部分作为线程的私有资源,创建程序计数器,栈等资源,最终才会去调用 run 方法。

3、interrupt

public void interrupt()
复制代码

这个方法用于中断当前线程,当然线程的不同状态应对中断的方式也是不同的,这一点我们后面再说。

4、join

public final synchronized void join(long millis)
复制代码

这个方法一般在其他线程中进行调用,指明当前线程需要阻塞在当前位置,等待目标线程所有指令全部执行完毕。例如:

Thread thread = new MyThreadT();
thread.start();

thread.join();

System.out.println("i am the main thread");
复制代码

正常情况下,主函数的打印语句会在 MyThreadT 线程 run 方法执行前执行,而 join 语句则指明 main 线程必须阻塞直到 MyThreadT 执行结束。

多线程带来的一些问题

多线程的优点我们不说了,现在来看看多线程,也就是并发下会有哪些内存问题。

1、竞态条件

这是一类问题,当多个线程同时访问并修改同一个对象,该对象最终的值往往不如预期。例如:

image

 

我们创建了 100 个线程,每个线程启动时随机 sleep 一会,然后为 count 加一,按照一般的顺序执行流,count 的值会是 100。

但是我告诉你,无论你运行多少遍,结果都不尽相同,等于 100 的概率非常低。这就是并发,原因也很简单,count++ 这个操作它不是一条指令可以做的。

它分为三个步骤,读取 count 的值,自增一,写回变量 count 中。多线程之间互相不知道彼此,都在执行这三个步骤,所以某个线程当前读到的数据值可能早已不是最新的了,结果自然不尽如期望。

但,这就是并发。

2、内存可见性

内存可见性是指,某些情况下,线程对于一些资源变量的修改并不会立马刷新到内存中,而是暂时存放在缓存,寄存器中。

这导致的最直接的问题就是,对共享变量的修改,另一个线程看不到。

image

 

这段代码很简单,主线程和我们的 ThreadTwo 共享一个全局变量 flag,后者一直监听这个变量值的变化情况,而我们在主线程中修改了这个变量的值,由于内存可见性问题,主线程中的修改并不会立马映射到内存,暂时存在缓存或寄存器中,这就导致 ThreadTwo 无法知晓 flag 值的变化而一直在做循环。

总结一下,进程作为系统分配资源的基本单元,而线程是进程的一部分,共享着进程中的资源,并且线程还是系统调度的最小执行流。在实时系统中,每个线程获得时间片调用 CPU,多线程并发式使用 CPU,每一次上下文切换都对应着「运行现场」的保存与恢复,这也是一个相对耗时的操作


 

进程和线程是操作系统中的两个基本概念,它们都是用来执行程序的执行单元,但在一些方面有着明显的区别。 1. 进程(Process): - 进程是程序在执行过程中的一个实例。 - 每个进程都有自己的独立内存空间,包括代码段、据段和堆栈段。 - 进程之间相互独立,拥有各自的资源,通信需要通过进程间通信(IPC)机制。 - 进程拥有自己的进程控制块(PCB),用于描述进程的状态、资源和调度信息。 2. 线程(Thread): - 线程是进程中的一个执行单元。 - 多个线程可以共享同一个进程的内存空间,包括代码段、据段和堆栈段。 - 线程之间共享进程的资源,如打开的文件、信号处理等。 - 线程线程控制块(TCB)来描述,每个线程有自己的栈和寄存器上下文。 区别: 1. 资源占用:每个进程都有独立的内存空间和系统资源,而线程共享进程的资源。 2. 创建销毁开销:创建或销毁进程比线程开销大,因为进程需要分配独立的内存空间和系统资源,而线程只需要创建线程控制块。 3. 切换开销:进程切换的开销较大,需要保存和恢复整个进程的上下文,而线程切换只需要保存和恢复线程的上下文。 4. 通信和同步:进程间通信需要使用进程间通信机制,如管道、消息队列等。线程间通信和同步相对容易,可以使用共享内存、信号量、互斥量等机制。 总结: 进程和线程都是用于执行程序的执行单元,但进程是资源分配的基本单位,线程是CPU调度的基本单位。多线程比多进程更轻量级,线程之间的切换开销更小,但进程之间相互独立,安全性更高。在实际应用中,需要根据具体需求选择使用进程还是线程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值