重新学习并发-Java线程

Java线程

  • 摘要
  • 线程
    • 实现线程的方式
      • 使用内核线程实现
      • 使用用户线程
      • Java线程
    • 线程调度
  • 线程状态
  • Daemon线程
  • 启动和终止线程
    • 线程的创建
      • 实现Runnable接口
      • 继承Thread类
      • 实现Callable接口
    • 启动线程
  • Thread类解析
    • 与线程运行状态有关的方法

摘要

线程是操作系统调度的最小单元,在多核环境下实现多线程能够显著提升程序性能。本文会先简单的介绍Java线程基础知识,并从启动一个线程到线程间不同的通信方式。

线程

在现代操作系统中运行一个程序时,会为其创建一个进程。在一个进程里可以创建多个线程,这些线程都拥有各自的计数器和局部变量等属性,并且能够访问共享的内存变量,处理器在这些线程上高速切换实现并发。

线程是比进程更轻量级的调度执行单位,各个线程共享着进程资源(内存地址、文件I/O等),也可以进行独立的调度(线程是CPU调度的基本单位)。

实现线程的方式

使用内核线程实现

内核线程直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操纵线程调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以看成是内核的一个分身,这样处理器就可以同时处理多任务。

操作系统会对内核线程进行一个封装,程序一般通过调用其封装后的接口 — 轻量级进程(Light Weight Process,LWP),轻量级进程和内核线程是1:1的对应关系。轻量级进程的局限就是:线程的创建、同步等都需要进行系统调用,系统调用的代价需要从用户态和内核态中来回切换,代价较高;每个轻量级进程都有对应的内核线程支持,轻量级进程需要消耗一定的内核资源,一个系统所支持的轻量级进程的数量也是有限的。
重新学习并发-Java线程_第1张图片

使用用户线程

如果一个线程只要不是内核线程就可以认为是用户线程。轻量级进程的实现始终是建立在内核之上,属于用户线程,但是其许多操作都要进行系统调用,效率会受到限制;

用户线程的建立、同步、销毁等都是在用户态中实现,不需要借助内核,这种操作是快速且低消耗的;
用户线程的优势就在于不需要内核线程的支持,所有对于线程的操作都是要考虑的,比如线程的创建、切换与调度,阻塞如何处理等问题,在设计用户线程时都是需要解决的,现在采用用户线程的程序越来越少;

Java线程

目前JDK版本中,操作系统支持怎样的线程模型就很大程度上决定了Java虚拟机的线程是怎样映射的,线程的实现是基于操作系统原生线程模型来实现。线程模型的差异对于并发规模和操作成本产生影响,但是对于Java程序的编码和运行过程来说,这些差异都是透明的;

线程调度

现在操作系统基本都是采用时分的形式调度和运行线程,操作系统会分出一个个时间片,线程会分配到若干个时间片,当线程的时间片用完之后就会发生线程调度,并且等到下次分配。线程分配到时间片的多少也就决定了线程使用处理器资源的多少。线程调度是指系统为线程分配处理器使用权的过程,主要分为协同式线程调度和抢占式线程调度;

采用协同式线程调度,线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,主动通知并切换到另一个线程上,好处就是实现简单,线程要把自己的事情干完之后才会进行线程切换,切换操作自己可知,不会产生线程同步的问题;弊端就是线程执行时间不可控,如果一个线程有问题且不通知系统进行线程切换,将会导致线程一直阻塞,相当不稳定;

采用抢占式线程调度,每个线程由系统分配执行时间,线程的切换不由线程本身来决定。线程的执行时间是系统可控的,不会出现一个线程导致进程阻塞的问题;

Java线程调度是系统自动完成的,但是如何给线程多分配或者少分配一些处理器资源?
在Java线程中可以通过设置一个整型成员变量priority来控制优先级来实现,一共设置10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),当两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。但是线程的优先级并不是太靠谱,因为Java线程是通过映射到系统原生线程上来实现的,所以线程的调度最终还是取决于操作系统;许多操作系统提供线程优先级但是并不能与Java线程的优先级一一对应;在不同的JVM以及操作系统上,线程规划有差异,有些操作系统甚至会忽略对于线程优先级的设定。

线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。

线程状态


状态名称 说明
NEW 线程创建后尚未启动
RUNNABLE 运行状态,Java中将就绪和运行状态统称为“运行中”
BLOCKED 阻塞状态,线程阻塞于锁
WAITING 等待状态,线程进入等待状态,等待通知或中断
TIME_WAITING 超时等待状态
TERMINATED 终止状态,表示当前线程已经执行完毕

线程状态转换入如下所示:
重新学习并发-Java线程_第2张图片

Daemon线程

Daemon线程是一种支持型线程,主要用作程序中后台调度以及支持性工作,当一个Java程序中不存在非Daemon线程时,Java虚拟机会退出,可以通过Thread.setDaemon(true)将线程设置为Daemon线程;

在后台默默地完成一些系统性的服务,比如垃圾回收线程等,与之对应的就是用户线程,用户线程就是系统的工作线程,它会先完成这个程序应该要完成的业务操作,如果用户线程全部结束,也意味着这个程序实际上无事可做,设置守护线程必须在start()之前设置;

public final void setDaemon(boolean on){
	checkAccess();
	if(isAlive()){
		throw new IllegalThreadStateException();
	}
	daemon = on;
}

启动和终止线程

线程的创建

线程的创建共有三种方法:

  • 实现Runnable接口;
  • 实现Callable接口;
  • 继承Thread类

实现Runnable接口

需要实现run方法,通过Thread调用start()方法来启动线程,start()方法会新建一个线程并让这个线程执行run()方法:

public class MyRunnable implements Runnable{
	public void run(){
		System.out.println("Hello World!");
	}
	public static void main(String[] args){
		MyRunnable myRunnable = new MyRunnable();
		Thread thread = new Thread(myRunnable);
		thead.start();
	}
}

继承Thread类

需要实现run()方法,因为Thread类也实现了Runnable接口;
当调用start()方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的run()方法

public class MyThread extends Thread{
	public static void run(){
		Systm.out.println("Hello World!");
	}
	public static void main(String[] args){
		Thread thread = new MyThread();
		thread.start();
	}
}

实现Callable接口

与Runnable接口相比,Callable可以有返回值,返回值通过FutureTask进行封装;

public class MyCallable implements Callable<Integer> {

    public Integer call(){
        return 123;
    }

    public static void main(String[] args) throws ExecutionException,InterruptedException{
        MyCallable mc = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

实现接口VS继承Thread类

  • Java不支持多继承,因此继承了Thread类就无法继承其他类,但是可以实现多个接口;
  • 类可能只要求可执行即可,继承整个Thread类开销过大;

启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程,线程start()方法的含义是:当前线程同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

启动一个线程前最好为该线程设置线程名称,以便在使用jstack分析程序或者进行问题排查时,方便处理bug;

Thread类解析

与线程运行状态有关的方法

1、start()
start()方法用来启动一个线程,当调用该方法时,相应的线程就会进入就绪状态,该线程中的run()方法会在某个时机被调用;

2、run()
run()方法不需要用户来调用,当通过start()方法启动一个线程之后,一旦线程获得了CPU的执行时间,便进入run()方法体中去执行具体的任务;

3.sleep()
在指定的毫秒数内让线程睡眠,并且交出CPU去执行其他的任务,当线程睡眠结束之后,不一定会立即执行线程,因为此时的CPU可能在执行其他的任务,调用sleep()方法相当于让线程进入阻塞状态;

  • sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象;
  • 使用sleep方法,阻塞的线程被中断时抛出InterruptedException异常
  • public static native void sleep(long millis) throws InterruptedException;

4、yield()
调用该方法只会让当前线程交出CPU资源,让CPU去执行其他的线程,但是yield不能控制具体的交出CPU的时间;

  • yield只会让具有相同优先级的线程具有获取CPU执行时间的机会;
  • 调用yield()方法不会让线程进入阻塞状态,而是让线程重回就绪状态,只需要等待重新得到CPU的执行;
  • 不会释放锁;

5、join()
在main线程中调用thread.join方法,则main线程会等待thread线程执行完毕或者等待一定的时间;

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);等待时间为0,意味着永远等待,直到线程被唤醒;
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

join方法会调用wait方法让宿主线程进入阻塞状态,并且释放线程占有的锁,并交出CPU执行权限,结合join方法的声明,有以下三条:

  • join方法会让线程交出cpu执行权限;
  • join方法会让线程释放对一个对象持有的锁;
  • 如果调用join方法,必须捕获InterruptedException异常或者将该异常向上层抛出;

6、interrupt()
中断可以理解为线程的一个标识位属性,表示运行中的线程能否被其他线程内进行中断操作。可以把中断表示其他线程对该线程打了一个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作;

interrupt即中断的意思,单独调用interrupt方法可以使得处于阻塞的线程抛出一个异常,可以用来中断一个处于阻塞状态的线程;线程中断只是给线程发送一个通知,告知目标线程有人希望你退出;

线程通过检查自身是否被中断来进行响应,线程可以通过方法isInterrupted()方法来判断是否被中断,也可以调用静态方法Thread.interrupted()方法对当前线程的中断标识位进行复位;

public void Thread.interrupt() //通知目标线程中断,设置中断标志位
public boolean Thread.isInterrupted()//判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态

直接调用interrupt方法不能中断正在运行中的程序;一般会在MyThread类中增加一个volatile属性isStop来标识是否结束while循环,然后在while循环中判断isStop的值

你可能感兴趣的:(重新学习Java并发编程)