多线程--基础

Java多线程

从本篇开始,笔者开始了一个新的专题,来说说Java多线程

在讲解Java多线程之前,我们来了解下进程和线程的概念!!!!

进程

进程的概念,是60年代初首先由麻省理工学院的MULTICS系统和IBM公司的CTSS/360系统引入的。

对于操作系统来说,进程是最核心的概念,操作系统实现并发的基础。进程是一个动态的过程,存在生命周期,可以申请和拥有系统资源,是一个程序的执行过程,是一个活动的实体。

程序作为一种软件资料长期存在,指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是有一定生命期的,程序在处理机上的一次执行过程,它是一个动态的概念,也就是说:程序是永久的,进程是暂时的。

简单的理解:进程是正在运行程序的实例。

我们知道,进程是一个实体,它拥有自己的内存空间,包含了文本区域(代码)、数据区域(变量信息)和堆栈信息(调用指令)。此外,程序是没有生命周期的,只有当处理器赋予程序生命时,它便成为了一个活动的实体,即成为了一个进程。

在不同操作系统下,进程的图形化展示:

windows下开启的线程
linux下开启的进程

线程

线程,也被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。

当我们运行一个程序时,系统会为我们创建一个进程,在实际运行过程中,进程会创建一个个线程,以来实现程序不同的功能。

通常在一个进程中会包含若干个线程,它们可以利用进程所拥有的资源,但是其本身并不拥有系统资源。

在我们常见的操作系统中,进程是资源分配的基本单位,而把线程是独立运行和独立调度的基本单位。直白点,就是说操作系统给进程分配系统内存、CPU等核心资源,而进程来实现程序种的功能。

由于线程比进程更小,不占用系统资源,对线程的调度所付出的开销要小得多,所以能更高效的提高系统中多个程序间并发程度,从而显著提高系统资源的利用率和吞吐量。

多线程

在一个进程中,同时运行多个线程来完成不同的工作,就称为多线程。

多线程的存在,是为了同时完成多项任务,提高资源使用效率。

在Java中,一个Java程序的启动,意味着虚拟机这个进程的启动,当我们执行一个main()方法时,实际上启动了一个叫做main-thread的线程,这个线程就来实现我们所需要的功能、逻辑。

接下来,我们就来介绍下在Java中,多线程的实现。

线程创建

在Java中,创建创建有两种方法:

继承 Thread 类创建线程;

实现 Runnable 接口类创建线程;

(1)继承Thread类

public class ThreadTest1 extends Thread{

    @Override
    public void run() {
        System.out.println("新启线程为:"+Thread.currentThread().getName());
    }

    public static void main(String[] agrs){
        ThreadTest1 threadTest11 = new ThreadTest1();
        ThreadTest1 threadTest12 = new ThreadTest1();
        threadTest11.start();
        threadTest12.start();
        System.out.println("main线程为:"+Thread.currentThread().getName());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试结果如下:

main线程为:main

新启线程为:Thread-0

新启线程为:Thread-1

继承Thread类,需要重写Thread中的run()方法,在run()方法中实现具体逻辑。在main()方法中,创建线程对象,调用start()方法来启动线程,之后会执行run()方法中的逻辑。此外,我们还可以通过调用Thread的getName()方法,来获取到线程的名称。

(2)实现Runnable接口

public class ThreadTest2 implements Runnable{

    @Override
    public void run() {
        System.out.println("新启线程为:"+Thread.currentThread().toString());
    }

    public static void main(String[] agrs){
        for(int x=0;x<10;x++){
            new Thread(new ThreadTest2()).start();
        }
        System.out.println("main线程为:"+Thread.currentThread().toString());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试结果如下:

main线程为:Thread[main,5,main]
新启线程为:Thread[Thread-0,5,main]
新启线程为:Thread[Thread-1,5,main]
新启线程为:Thread[Thread-2,5,main]
新启线程为:Thread[Thread-3,5,main]
新启线程为:Thread[Thread-4,5,main]
新启线程为:Thread[Thread-6,5,main]
新启线程为:Thread[Thread-9,5,main]
新启线程为:Thread[Thread-5,5,main]
新启线程为:Thread[Thread-8,5,main]
新启线程为:Thread[Thread-7,5,main]

实现Runnable接口,需要实现接口中的run()方法。与继承Thread不同的是,在创建线程对象时需要借助Thread的构造方法,再调用start()方法来完成启动线程。

在run()方法中,我们调用了Thread的toString()方法,该方法返回结果包括:线程的名称,线程的优先级,线程组的名称;

通常,我们都是使用实现Runnable接口的方式来完成线程的创建和启动。对于继承Thread来说,该方式实现起来编码更简单,在run()方法内部即可调用Thread类的方法;而实现Runnable接口方式,则极大避免了Java单继承的局限。

从上面的两个例子中可以看出,无论是继承Thread、还是实现Runnable接口的方式,本质上来说都离不开Thread类。

下面,我们来具体看下Thread类中有哪些主要方法:

线程方法

方法 方法描述
public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法
public void run() 虚拟机执行线程调用的方法
public final void setName(String name) 改变线程名称
public final void setPriority(int priority) 更改线程的优先级
public final void setDaemon(boolean on) 将该线程标记为守护线程
public final void join() 当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行
public void interrupt() 中断线程,给在执行的线程一个中断信号,并不是停止线程的运行
public final boolean isAlive() 测试线程是否处于活动状态
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程(也可能是自己)
public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行
public static Thread currentThread() 返回对当前正在执行的线程对象的引用

(1)调整线程优先级--setPriority(int priority)

public class ThreadTest3 implements Runnable{
    @Override
    public void run() {
        System.out.println("新启线程的优先级为:"+Thread.currentThread().getPriority());
    }

    public static void main(String[] agrs){
        //设置main的线程优先级:
        Thread.currentThread().setPriority(10);
        System.out.println("设置main线程的优先级为:"+Thread.currentThread().getPriority());
        for(int x=0;x<10;x++){
            Thread thread = new Thread(new ThreadTest3());
            if(x%2==0){
                thread.setPriority(7);
            }
            thread.start();
        }
        System.out.println("main线程为:"+Thread.currentThread().toString());
    }
}

在上面的例子中,我们通过setPriority(int priority)来设置线程的优先级,优先级高的线程优先执行。

Java线程的优先级取值范围是1~~10,Thread类中有下面三个静态常量:

static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10
          
static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1
          
static int NORM_PRIORITY:分配给线程的默认优先级,取值为5

在Java线程中,每个线程都有默认的优先级,默认为5.

此外,Java线程的优先级还有继承关系,例如上面的例子中,我们首先设置了main线程的优先级,当我们在main中启动别的线程时,如果没有对新启动的线程指定优先级,那么新启动的线程继承main线程的优先级。

(2)线程睡眠--sleep(long millisec)

public class ThreadTest4 implements Runnable{
    @Override
    public void run() {
        System.out.println("新启线程:"+Thread.currentThread().toString());
        try {
            Thread.sleep(100);
            System.out.println("新启线程停止500毫秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] agrs){
        for(int x=0;x<10;x++){
            Thread thread = new Thread(new ThreadTest4());
            thread.start();
        }
        System.out.println("main线程为:"+Thread.currentThread().toString());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使正在运行的Java线程转到阻塞状态,millisec参数设定的是线程的睡眠时间,以毫秒为单位,sleep(long millisec)是静态方法,只能控制当前正在运行的线程。

当Java线程在睡眠结束后,便会转为就绪(Runnable)状态。

(关于线程的状态,在下一小节介绍)

值得注意的是,一个线程执行了sleep操作,如果这个线程获取到锁,那么sleep并不会让出锁。

前面说了,一个线程在睡眠结束后,便会转为就绪状态,并不会立刻执行,需要等待CPU的调度,那么sleep中指定的时间就是线程休眠的最短时间。

(3)父线程等待子线程结束之后再运行--join()

public class ThreadTest5 implements Runnable{
    @Override
    public void run() {
        System.out.println("新启线程:"+Thread.currentThread().toString());
    }

    public static void main(String[] agrs){
        List<Thread> list = new ArrayList<Thread>();
        for(int x=0;x<10;x++){
            Thread thread = new Thread(new ThreadTest5());
            list.add(thread);
            thread.start();
        }
        for(Thread thread:list){
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("main继续做事,main线程为:"+Thread.currentThread().toString());
    }
}

等待线程执行完成后,再继续执行,这就是join存在的意义。当我们在线程A中,调用了线程B的join()方法,那么线程A会停下,被阻塞,但是不会释放锁(这一点跟sleep一样),等待线程B执行完成,此时线程A又恢复到了就绪状态(不是立即执行,这一点跟sleep也一样)。

我们锁了,join()会阻塞线程的执行,那么为什么呢?我们来看看源码:

//无参数的 join():
public final void join() throws InterruptedException {
    join(0);
}

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");
    }
    //如果等于0,则一直执行while循环,循环体中调用wait()方法
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        //如果不为0,则计算阻塞的时间:
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

通过源码,我们发现,join内部其实是由于wait()方法实现。当我们调用无参数的join()方法时,线程会一直执行while循环,探测实现是否还存活,存活就wait(0),就这样一直在while循环中做判断,当线程执行结束后,isAlive()返回false,while循环结束。

(4)暂停当前正在执行的线程,并执行其他线程--yield()

public class ThreadTest8 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("新启动线程:" + i);
            Thread.yield();
        }
    }

    public static void main(String[] agrs){
        new Thread(new ThreadTest8()).start();
        System.out.println("新线程启动了");
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:" + i);
            Thread.yield();
        }
    }
}

暂停当前正在执行的线程,执行其他线程。对于其他线程,这里面包含了两种含义。

Thread.yield()执行后,允许其他线程获得运行机会。因此,使用yield()让多个线程之间能适当的轮转执行。

但是,测试结果来看无法完全保证Thread.yield()的目的,执行Thread.yield()的线程有可能被线程调度程序再次选中,也就是说自己被认为了其他线程。

值得注意的是,Thread.yield()是将线程从运行状态转为了就绪状态,并没有阻塞线程。

(5)中断线程--interrupt()

public class ThreadTest6 implements Runnable{
    @Override
    public void run() {
        while(true){
            if(Thread.currentThread().isInterrupted()){
                System.out.println("我被中断了");
            }else{
                System.out.println("我一直在运行");
            }
        }
    }

    public static void main(String[] agrs){
        Thread thread = new Thread(new ThreadTest6());
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

interrupt()方法通过修改了被调用线程的中断状态来告知那个线程, 告诉它自己已经被中断了,这里指的中断是一个中断信号,不是说将被调用的线程给干掉了,不要理解错了。

我们可以通过调用线程的isInterrupted()方法,来获取线程的中断状态,通过此状态来判断线程中的执行逻辑。

对于非阻塞线程来说(例如上面的例子),调用interrupt()方法后,只是修改了线程的中断状态,isInterrupted()返回true。

但是对于阻塞线程来说,就不同了。

回想下,当我们在程序中调用Thread.sleep()、Object.wait()、Thread.join()时,会抛出一个叫InterruptedException的异常,看这个异常的命名,是不是跟现在我们所讲的interrupt()方法类似。

没错,对于阻塞线程来说,当我们执行interrupt()方法后,被阻塞的线程会抛出InterruptedException异常,并且将线程中断状态置为true。至于,对异常的处理就因业务需求而已了。

public class ThreadTest6 implements Runnable{
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("终于明白sleep会抛出异常了捕获异常了");
            }
        }
    }

    public static void main(String[] agrs){
        Thread thread = new Thread(new ThreadTest6());
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

(6)设置守护线程--setDaemon()

public class ThreadTest7 implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("守护线程中,我会执行吗?");
        }
    }

    public static void main(String[] agrs){
        Thread thread = new Thread(new ThreadTest7());
        thread.setDaemon(true);
        thread.start();
        System.out.println("守护线程启动");
    }
}

在Java中,线程分为两大类型:用户线程和守护线程。

通过Thread.setDaemon(false)设置为用户线程,默认不调用此方法而创建的线程也是用户线程;

通过Thread.setDaemon(true)设置为守护线程。

setDaemon()方法必须在start()方法之前设置,否则会抛出IllegalThreadStateException异常。

守护线程和用户线程有何不同?

当我们将一个线程设置为守护线程,如果主线程执行结束,那么守护线程也会跟随主线程一起结束。

而用户线程却不同,如果主线程执行结束,但是用户线程还在执行,那么程序就不会停止。

最典型的守护线程,就是JVM虚拟机中的垃圾回收器。

上面的例子中,新启动的线程被设置成了守护线程,当main线程结束时,守护线程也随之结束。但是,守护线程中的finally代码块并不会执行。

线程生命周期

线程是一个动态执行的过程,它有一个从出生到死亡的过程。在Thread类中,Java提供了线程的一生中会经过哪些状态。

(1)NEW:出生,new Thread()

(2)RUNNABLE:运行,start()、run()

(3)BLOCKED:阻塞,等待锁synchronized block

(4)WAITING:无限等待,join()、wait()

(5)TIMED_WAITING:定时等待,sleep(x)、wait(x)、join(x)

(6)TERMINATED:终结,线程执行完毕

列个表格,具体说下。需要注意的是,上面6种状态与网上搜出来的很多文章并不一致(网上多一些状态进行了归类),请以此为准,因为这些状态是Thread明确列出的。

方法 简要说明
NEW 线程的初始状态,也就是我们在代码中new Thread()后的状态,还没有调用start()方法
RUNNABLE 运行状态,这一点与网上的很多文章不一样,在Thread类中,运行状态包含了就绪和运行,也就是调用了start()和实际执行run()
BLOCKED 阻塞状态,线程在进入同步代码块之前,发现已经有线程获取到了锁,所以本线程阻塞等待锁的释放
WAITING 等待状态,等待其他线程做一事情,例如当我们的一个线程被执行了Object.wait(),那么该线程实际在等待其他线程触发Object.notify、Object.notifyAll()
TIMED_WAITING 定时等待,与WAITING不同的是,此种状态在达到一定时间便可返回运行状态,而不需要依赖其他线程处理,例如:Thread.sleep(x)、Object.wait(x)
TERMINATED 终结状态,也就是说线程执行完毕了

下面,我们通过图片再具体了解下,状态之间的流转:

image

需要注意的是,图中从其他状态变为运行状态时,其实是恢复成了就绪状态,还需要等待CPU的调度,才能真正的执行。

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,456评论 3 53
  • 为何星空这么璀璨 夜下,凉风吹过? 为何夕阳那么辉煌 黑夜将近? 为何路那么长 走的人只一个? 为何看似那么近 实...
    黎埠阅读 50评论 0 2
  • 每天这个时候一块黑巧。 套路,视之为无形。看破不说破。
    无隐阅读 66评论 0 0