关于深入理解Java线程

关于深入理解Java线程

一、线程基础

1、进程和线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的 。
  • 当一个程序被运行,从磁盘加载此程序的代码到内存时,就开启了一个进程。
  • 进程可视为程序的一个实例。大部分程序可同时运行多个实例进程(如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(如网易云音乐、360 安全卫士等)。
  • 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。

线程

  • 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行 。
  • 线程有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。

线程在CPU中表现为指令序列,一行指令序列对应一个线程

在这里插入图片描述
JDK自带的工具jconsole.exe可以查看线程(C:\Program Files\Java\jdk1.8.0_301\bin)

以启动Tomcat为例,会开启对应进程,包含以下线程:
main:主方法线程
RMI TCP Accept-0:需要处理对外的请求,因此会有连接发送过来(请求),此线程处理对应的socket
ajp-nio-8009-ClientPoller-0:nio模型,会转交给两个poller处理(默认处理器的最小值,一般为2个)
ajp-nio-8009-ClientPoller-1:poller处理写事件(响应)
在这里插入图片描述

进程与线程的区别

  1. 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  2. 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  3. 进程间通信较为复杂
    a. 同一台计算机的进程通信称为IPC(Inter-process communication)
    b. 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  4. 线程通信相对简单,因为它们共享进程内的内存,例如多个线程可以访问同一个共享变量
  5. 线程更轻量,线程上下文切换成本一般上比进程上下文切换低

进程间通信方式

  1. 管道(pipe)及有名管道(named pipe):

管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

  1. 信号(signal):

信号是在软件层次上对中断机制的一种模拟,是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果一致。

  1. 消息队列(message queue):

消息队列是消息的链接表,克服了上述两种通信方式中信号量有限的缺点,具有写权限的进程可按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可从消息队列中读取信息。

  1. 共享内存(shared memory):

为最有用的进程间通信方式。多个进程可访问同一块内存空间,不同进程可及时看到对方进程中对共享内存中数据的更新。此方式需依靠某种同步操作,如互斥锁和信号量等。

  1. 信号量(semaphore):

主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。

  1. 套接字(socket):

一种更为一般的进程间通信机制,可用于网络中不同机器之间的进程间通信,应用非常广泛。

2、线程的同步互斥

线程同步:

线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥:

对于共享的进程系统资源,在各单个线程访问时存在的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可看成是一种特殊的线程同步。

四种线程同步互斥的控制方法

  1. 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。

在一段时间内只允许一个线程访问的资源则为临界资源。

  1. 互斥量:用来协调共同对一个共享资源的单独访问。
  2. 信号量:用来控制一个具有有限数量用户资源。
  3. 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

3、上下文切换(Context switch)

上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。

进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程(即创建线程的进程)共享一个地址空间和其他资源。
上下文是CPU寄存器和程序计数器在任何时间点的内容。
寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。
程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。

在这里插入图片描述
上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:

  1. 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
  2. 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
  3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)来恢复进程。

在这里插入图片描述

上下文切换只能在内核模式下发生

内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。

其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。

内核模式(Kernel Mode)

在内核模式下,执行代码可以完全且不受限制地访问底层硬件。可执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的,会使整个电脑瘫痪。

用户模式(User Mode)

在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。在计算机上运行的大多数代码将在用户模式下执行。

在这里插入图片描述

CPU保护模式

x86 CPU提供了四个保护环(protection rings):0、1、2、3。通常只使用0环(内核)和3环(用户)。

在这里插入图片描述

应用程序切换到内核模式的情况:
  1. 系统调用。
  2. 异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
  3. 设备中断。在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件,若原来在用户态,则会切换到内核态。

上下文切换是多任务操作系统的一个基本特性

在多任务操作系统中,多个进程似乎同时在一个CPU上执行,彼此之间互不干扰。这种并发的错觉是通过快速连续发生的上下文切换(每秒数十次或数百次)来实现的。这些上下文切换发生的原因是进程自愿放弃它们在CPU中的时间,或者是调度器在进程耗尽其CPU时间片时进行切换的结果。

上下文切换通常是计算密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本,实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个重点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一是它的上下文切换和模式切换成本极低。

通过命令查看CPU上下文切换情况

linux系统可以通过命令统计CPU上下文切换数据

#可以看到整个操作系统每1秒CPU上下文切换的统计
vmstat 1

在这里插入图片描述
cs列为CPU上下文切换的统计。

只是CPU上下文切换不等价于线程切换,很多操作会造成CPU上下文切换:

  • 线程、进程切换
  • 系统调用
  • 中断

查看某一个线程\进程的上下文切换

使用pidstat命令

常用的参数:
-u 默认参数,显示各个进程的 CPU 统计信息
-r 显示各个进程的内存使用情况
-d 显示各个进程的 IO 使用
-w 显示各个进程的上下文切换
-p PID 指定 PID

# 显示进程5598每一秒的切换情况
pidstat ‐w ‐p 5598 1

在这里插入图片描述
cswch表示主动切换,nvcswch表示被动切换。从统计数据中看到,该进程每秒主动切换次数达到将近500次,因此代码中存在大量的 睡眠\唤醒 操作。

从进程的状态信息中查看

通过命令 cat /proc/5598/status 查看进程的状态信息

voluntary_ctxt_switches: 40469351
nonvoluntary_ctxt_switches: 2268

上述两项即为该进程从启动到当前时间,所有的上下文切换情况

4、操作系统层面的线程生命周期

操作系统层面的线程生命周期为5种状态
初始状态、可运行状态、运行状态、休眠状态、终止状态。
在这里插入图片描述

  1. 初始状态:

线程已经被创建,但还不允许分配CPU执行。此状态属于编程语言特有的,但此处的被创建,仅为编程语言层面的被创建,而在操作系统层面,真正的线程还未被创建。

  1. 可运行状态:

线程可分配CPU执行。此状态下,真正的操作系统线程已被成功创建,所以可分配CPU执行。

  1. 可运行状态与运行状态:

当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态。

  1. 休眠状态:

运行状态的线程若调用一个阻塞的API(如以阻塞方式读文件)或等待某个事件(如条件变量),此时线程的状态就会转换为休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。

  1. 终止状态:

线程执行完或出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态意味着线程的生命周期彻底结束。

这五种状态在不同编程语言里会有简化合并。例如,C 语言的POSIX Threads规范,就把初始状态和可运行状态合并;Java语言里则把可运行状态和运行状态合并,这两个状态在操作系统调度层面有用,而JVM层面不关心这两个状态,因为JVM把线程调度交给操作系统处理了。

5、查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

linux

  • ps - fe 查看所有进程
  • ps - fT - p 查看某个进程( PID )的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top - H - p 查看某个进程( PID )的所有线程

Java

  • jps 命令查看所有Java进程
  • jstack 查看某个Java进程( PID )的所有线程状态
  • jconsole 查看某个Java进程中线程的运行情况(图形界面)

Linux系统中线程实现方式

  • LinuxThreads:linux/glibc包在2.3.2之前只实现了LinuxThreads
  • NPTL(Native POSIX Thread Library)
// 可以通过以下命令查看系统是使用哪种线程实现
getconf GNU_LIBPTHREAD_VERSION

在这里插入图片描述

二、Java线程详解

1、Java线程的实现方式

方式1:使用 Thread类或继承Thread类

// 创建线程对象
Thread t = new Thread() {
	public void run() {
		// 要执行的任务
	}
};
// 启动线程
t.start();

方式2:实现 Runnable 接口配合Thread

把 线程 和 任务(要执行的代码)分开

  • Thread:线程
  • Runnable:可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
    public void run(){
		// 要执行的任务
	}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

方式3:使用有返回值的 Callable

class CallableTask implements Callable<Integer> {
	@Override
	public Integer call() throws Exception {
		return new Random().nextInt();
	}
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());

方式4:使用 lambda

new Thread(()> System.out.println(Thread.currentThread().getName())).start();

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法

2、Java线程实现原理

Java线程实现的本质都是调用start方法,而start方法的底层会通过启动Java的线程,使得JVM和操作系统也创建出对应的线程,其中涉及到用户态切换到内核态,经过一系列的操作,最后才调用run方法。

若直接调用run方法,只是普通对象的方法调用,并不是一个真正的线程。

线程创建和启动的流程:

  1. 使用new Thread()创建一个线程,再调用start()进行Java层面的线程启动;
  2. 通过调用本地方法start0(),来调用JVM中的JVM_StartThread方法进行线程创建和启动;
  3. 调用new JavaThread(&thread_entry, sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create_thread方法进行线程创建;
  4. 新创建的线程状态为Initialized,调用了sync的wait方法进行等待,等到被唤醒才继续执行thread的run方法;
  5. 调用Thread::start(native_thread)进行线程启动,此时将线程状态置为RUNNABLE,接着再调用os:start_thread(thread),根据不同的操作系统选择不同的线程启动方式;
  6. 线程启动之后状态置为RUNNABLE,并唤醒第4步中等待的线程,再接着执行thread的run方法;
  7. JavaThread::run()会回调第1步new Thread中复写的run方法。

Java线程属于内核级线程

JDK1.2:基于操作系统原生线程模型来实现。

Sun JDK:Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。

内核级线程(Kernel Level Thread ,KLT)

依赖于内核,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。

用户级线程(User Level Thread,ULT)

操作系统内核不知道应用线程的存在:
在这里插入图片描述

协程(Coroutines)

一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理的,是完全由程序所控制(即在用户态执行),具有对内核来说不可见的特性。

这样的好处是极大地提升性能,不会像线程切换那样消耗资源。
在这里插入图片描述
子程序(函数)在所有语言中都是层级调用。如A调用B,B在执行过程中又调用C,C执行完毕返回,B执行完毕返回,最后A执行完毕。

协程的调用和子程序不同。协程在子程序内部是可中断的,中断后去执行别的子程序,在适当的时候再返回来接着执行。

def A():
    print '1'
    print '2'
    print '3'
def B():
    print 'x'
    print 'y'
    print 'z'

假设由协程执行,在执行A的过程中,可以随时中断,转去执行B,B也可能在执行过程中中断再去执行A,因此结果可能是:1 2 x y 3 z。

协程的特点是一个线程执行,和多线程比,协程的优势:

  1. 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
  2. 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。
  3. 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需判断状态即可,所以执行效率比多线程高很多。

注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景。

3、Java线程的调度机制

系统为线程分配处理器使用权的过程,主要调度方式分两种:协同式线程调度、抢占式线程调度。

协同式线程调度

线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。

最大优点是实现简单,且切换操作对线程自己是可知的,无线程同步问题。

缺点是线程执行时间不可控制,若一个线程产生问题,可能引起永久阻塞。

抢占式线程调度

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

Java线程调度即为抢占式调度

希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不绝对,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

public class SellTicketDemo implements Runnable {
    /**
     * 车票
     */
    private int ticket;

    public SellTicketDemo() {
        this.ticket = 1000;
    }

    @Override
    public void run() {
        while (ticket > 0) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        //线程进入暂时的休眠
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //获取当前正在执行的程序的名称,打印余票
                    System.out.println(Thread.currentThread().getName() 
                                       + ":正在执行操作,余票:" + ticket--);
                }
            }
            Thread.yield();
        }
    }

    public static void main(String[] args) {
        SellTicketDemo demo = new SellTicketDemo();

        Thread thread1 = new Thread(demo, "thread1");
        Thread thread2 = new Thread(demo, "thread2");
        Thread thread3 = new Thread(demo, "thread3");
        Thread thread4 = new Thread(demo, "thread4");
        //priority优先级默认为5,最低1,最高10
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);
        thread3.setPriority(Thread.MIN_PRIORITY);
        thread4.setPriority(Thread.MIN_PRIORITY);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

4、Java线程的生命周期

Java语言中线程共有6种状态

  1. NEW(初始化状态):

    只是普通对象

  2. RUNNABLE(可运行状态+运行状态):

    调用start(),变为就绪状态

  3. BLOCKED(阻塞状态):

    休眠状态之阻塞,针对synchronized:
    ①其中一条线程获取到锁,另一条线程就进入阻塞状态;
    ②当线程调用wait(),即释放锁,阻塞状态的线程被唤醒再去参与获取锁又失败时,又会进入阻塞状态

  4. WAITING(无时限等待):

    休眠状态之无时间限制的等待(调用wait()、join()、park())

  5. TIMED_WAITING(有时限等待):

    休眠状态之有时间限制的等待(加上超时时间 (long) )

  6. TERMINATED(终止状态)

    在操作系统层面,Java 线程中的BLOCKED、WAITING、TIMED_WAITING是一种状态,即前文提到的休眠状态。即若Java线程处于这三种状态之一,则此线程永远没有CPU的使用权。

测试:

public class ThreadStateTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread();
        System.out.println("线程状态 >>> " + thread.getState());
        thread.start();
        System.out.println("线程状态 >>> " + thread.getState());
        Thread.sleep(100);
        System.out.println("线程状态 >>> " + thread.getState());
    }
}

运行结果:

线程状态 >>> NEW
线程状态 >>> RUNNABLE
线程状态 >>> TERMINATED

线程启动没做其他操作,睡眠100ms直接终止

若启动线程之后进行了其他任务,则不会立即终止:

public class ThreadStateTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                LockSupport.park();
            }
        });
        System.out.println("线程状态 >>> " + thread.getState());
        thread.start();
        System.out.println("线程状态 >>> " + thread.getState());
        Thread.sleep(100);
        System.out.println("线程状态 >>> " + thread.getState());
    }
}

运行结果:

线程状态 >>> NEW
线程状态 >>> RUNNABLE
线程状态 >>> WAITING

在这里插入图片描述

从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)

在这里插入图片描述

从OSThread的角度,JVM还定义了一些线程状态给外部使用,如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)

在这里插入图片描述

5、Thread常用方法

sleep方法

  • 调用sleep会让当前线程从Running进入TIMED_WAITING状态,不会释放对象锁
  • 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入参数为0时,和yield相同

yield方法

  • yield会释放CPU资源,让当前线程从Running进入Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程
  • 具体的实现依赖于操作系统的任务调度器

join方法

  • 等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。
public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t begin");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t finished");
            }
        });
        long start = System.currentTimeMillis();
        t.start();
        //主线程等待线程t执行完成
        t.join();

        System.out.println("执行时间:" + (System.currentTimeMillis() - start));
        System.out.println("Main finished");
    }
}

stop方法

  • stop方法已被jdk废弃,由于stop方法太过暴力,强行把执行到一半的线程终止,并释放锁,容易造成数据不一致。
public class ThreadStopDemo {
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    try {
                        Thread.sleep(60000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        });
        thread.start();
        Thread.sleep(2000);
        //停止thread,并释放锁
        thread.stop();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "等待获取锁");
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                }
            }
        }).start();
    }
}

5、Java线程的中断机制

  • Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。
  • 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。
  • 被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。

API的使用

  • interrupt(): 将线程的中断标志位设置为true,不会停止线程
  • isInterrupted():判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
public class ThreadInterruptTest {
    static int i = 0;

    public static void main(String[] args) {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    i++;
                    System.out.println(i);
                    //Thread.interrupted():清除中断标志位
                    //Thread.currentThread().isInterrupted():不会清除中断标志位
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("==========");
                    }
                    if (i == 10) {
                        break;
                    }
                }
            }
        });
        t1.start();
        //不会停止线程t1,只会设置一个中断标志位 flag=true
        t1.interrupt();
    }
}

利用中断机制优雅地停止线程

while(!Thead.currentThread().isInterrupted() && more work to do) {
	do more work
}
public class StopThread implements Runnable {

    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
        }
        System.out.println("线程停止:stop thread");
    };

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
}

注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况

sleep期间可感受到中断

修改上面的代码,线程执行任务期间有休眠需求:

@Override
public void run() {
    int count = 0;
    while (!Thread.currentThread().isInterrupted() && count < 1000) {
        System.out.println("count = " + count++);
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("线程停止:stop thread");
};

在这里插入图片描述
处于休眠中的线程被中断,线程是可感受到中断信号,且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置成false。这样就会导致while条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000这个条件时退出。若不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。

try {
    Thread.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
    //重新设置线程中断状态为true
    Thread.currentThread().interrupt();
}

sleep可以被中断,抛出中断异常:sleep interrupted, 清除中断标志位

wait可以被中断,抛出中断异常:InterruptedException, 清除中断标志位

6、Java线程间通信

volatile

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。

public class VolatileDemo {
    public static volatile boolean flag = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (flag) {
                        System.out.println("turn on");
                        flag = false;
                    }
                }
            }
        }).start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (!flag) {
                        System.out.println("turn off");
                        flag = true;
                    }
                }
            }
        }).start();
    }
}

等待唤醒(等待通知)机制

等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待,直到被唤醒。

public class WaitDemo {
    public static Object lock = new Object();
    public static boolean flag = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    while (flag) {
                        try {
                            System.out.println("wait start …………");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("wait end …………");
                }
            }
        }).start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag) {
                    synchronized (lock) {
                        if (flag) {
                            lock.notify();
                            System.out.println("notify …………");
                            flag = false;
                        }
                    }
                }
            }
        }).start();
    }
}

LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。

public class LockSupportTest {
    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();

        System.out.println("唤醒parkThread");
        LockSupport.unpark(parkThread);
    }
    
    static class ParkThread implements Runnable{

        @Override
        public void run() {
            System.out.println("ParkThread开始执行");
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }
}

管道输入输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处:

主要用于线程之间的数据传输,而传输的媒介为内存。

管道输入/输出流主要包括了如下4种具体实现:

PipedOutputStream、PipedInputStream、PipedReader和PipedWriter;
前两种面向字节,后两种面向字符。

public class Piped {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        //将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);

        Thread printThread = new Thread(new Print(in), "PrintThread");

        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != 1) {
                out.write(receive);
            }
        }finally {
            out.close();
        }
    }

    static class Print implements Runnable {
        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.println((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Thread.join

join可理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的优点是能够保证线程的执行顺序,但若调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但本质上仍为串行,最后join的实现其实是基于等待通知机制的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值