Java多线程编程-基础学习

1. 基本概念

我们先来了解下什么是进程和线程?他们之间的关系是什么?

  • 进程:进程是程序的运行实例(例如:一个运行的Eclipse就是一个进程),进程是程序向操作系统申请资源(如内存空间和文件句柄)的基本单位。
  • 线程:线程是操作系统能够进行运算调度的最小单位。
  • 关系:一个进程可以包含多个线程,同一个进程中的所有线程共享改进程中的资源,如内存空间、文件句柄等。

使用多线程的优势:

  • 在我们多核的系统中,可以充分利用提高CPU利用率。
  • 提高系统的吞吐率,当一个线程因为I/O操作而处于等待时,其它线程仍然可以执行其操作。
  • 提高响应性,对于Web应用程序而言,一个请求的处理慢了并不会影响其它请求的操作。

2. 线程创建与启动

创建线程有如下两种方式:

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

下面的代码,就是以定义Thread类子类的方式创建并启动线程:

public class CreateThreadDemo {
    public static void main(String[] args) {
        //创建线程
        MyThread myThread = new MyThread();
        //启动线程
        myThread.start();
    }
}

// 定义Thread类的子类
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("myThread begin running, threadName:" + Thread.currentThread().getName());
    }
}
实现Runnable接口

下面的代码,就是以实现Runnable接口的方式创建并启动线程:

public class CreateThreadByRunnable {
    public static void main(String[] args) {
        //创建线程
        Thread myThread = new Thread(new MyTask());
        //启动线程
        myThread.start();
    }
}

// 实现Runnable接口
class MyTask implements Runnable{
    @Override
    public void run() {
        System.out.println("myThread begin running, threadName:" + Thread.currentThread().getName());
    }
}

3. 常用方法

下表列出了线程的一些常用方法:

方法 功能 备注
currentThread 返回当前代码的执行线程 同一段代码对Thread.currentThread()调用,返回值可能对应不同的线程
yield 使当前线程主动放弃其对处理的占用,可能导致当前线程被暂停 这个方法是不可靠的,被调用时当前线程可能仍然继续运行
sleep 使当前线程休眠指定时间
start 启动相应线程 一个Thread实例的start方法只能调用一次,多次调用会导致异常抛出
run 用于实现线程的任务处理逻辑 由Java虚拟机直接调用,一般情况下应用程序不应该调用
interrupt 中断线程 将线程的中断标记设置为false
interrupted 判断的是当前线程是否处于中断状态,同时会清除线程的中断状态
isInterrupted 判断调用线程是否处于中断状态 线程可以通过此方法来响应中断
getName 获取线程的名称
join 等待相应线程运行结束 若线程A调用线程B的方法,那么线程A的运行会被暂停,知道线程B运行结束
getId 获取线程的标识符

4. 线程生命周期

一个线程从其创建、启动到其运行结束的整个生命周期可能经历若干状态,如下图所示:
在这里插入图片描述

  • NEW:一个已创建(调用线程start方法)而未启动的线程处于该状态,由于一个线程只能启动一次,所有只可能有一次处于该状态。
  • RUNNABLE:该状态可以看成一个复合状态,包含REAY和RUNNING两个子状态。READY表示可以被线程调度器进行调度从而进入RUNNING状态。一个处于RUNNING状态的线程可以通过Thread.yield()方法变为READY状态。
  • BLOCKED:一个线程发起一个阻塞IO操作,或者申请独占资源,未获取到锁时,会进入阻塞BLOCKED状态。当阻塞IO操作完成或获取到锁后,则状态转为RUNNABLE。
  • WAITING:当一个线程执行了Object.wait()或LockSupport.part(Object)方法后,会进去WAITING状态。此状态必须通过其它线程调用Object.notify()/LockSupport.unpart(Object)方法来进行唤醒,从而再次转换为RUNNABLE状态。
  • TIMED_WAITING:当一个线程调用了Thread.sleep(long),Object.wait(long),LockSupport.parkNanos(long)等方法后,会进入带有时间限制的等待状态TIMED_WAITING,当等待的时间满足后,会自动转换为RUNNABLE状态。
  • TERMINATED:已经执行结束的线程处于TERMINATED状态。由于一个线程只能启动一次,所有只可能有一次处于该状态。

5. 线程中断

什么是中断

断可以看作由一个线程(发起线程)发送给另一个线程(目标线程)的一种指示,该指示用于表示发起线程希望目标线程停止其正在执行的操作。
中断仅仅代表发起线程的一个诉求,而这个诉求能够被满足则取决于目标线程自身,目标线程可能会满足发起线程的诉求,也可能根本不理会发起线程的诉求。

使用方法

看下面这段代码:

public class ThreadInterrupt {
    public static void main(String[] args) {
        // 创建子线程
        Thread myThread = new Thread(new InterruptTask());
        // 启动子线程
        myThread.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 中断子线程
        myThread.interrupt();

    }
}

// 实现Runnable接口
class InterruptTask implements Runnable{
    @Override
    public void run() {
        // while条件是检测当前线程是否被中断
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("thread is running...");
        }
    }
}
  • 主线程通过调用子线程的interrupt()方法,将子线程的中断标记设置为true。
  • 子线程通过调用Thread.currentThread().isInterrupted()来获取自身的中断标记值,若检测到被中断,则可以作响应的处理。
中断响应

Java标准库中许多阻塞方法对中断的响应方式都是抛出InterruptedException异常。
我们需要注意的是,抛出异常后,中断标志位会被清空(线程的中断标志位会由true重置为false)。我们看下面这个例子:

public class ThreadInterrupt {
    public static void main(String[] args) {
        // 创建子线程
        Thread myThread = new Thread(new InterruptTask());
        // 启动子线程
        myThread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 中断子线程
        myThread.interrupt();

    }
}

// 实现Runnable接口
class InterruptTask implements Runnable{
    @Override
    public void run() {
        try {
            // 子线程挂起3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        	System.out.println("子线程中断标记:" + Thread.currentThread().isInterrupted());
            e.printStackTrace();
        }
    }
}

程序运行后结果:

子线程中断标记:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.demo.InterruptTask.run(ThreadInterrupt.java:26)
	at java.lang.Thread.run(Thread.java:745)

我们看到子线程在挂起的过程中被主线程中断,抛出了InterruptedException异常,且中断标记被重置为false。
由于中断标记被重置了,如果我们处理不当,可能会带来一些问题,看下面这个例子:

public class ThreadInterrupt {
    public static void main(String[] args) {
        // 创建子线程
        Thread myThread = new Thread(new InterruptTask());
        // 启动子线程
        myThread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 中断子线程
        myThread.interrupt();

    }
}

// 实现Runnable接口
class InterruptTask implements Runnable{
    @Override
    public void run() {
        // 业务方法挂起期间被中断
        service();
        if(Thread.currentThread().isInterrupted()){
            System.out.println("子线程被中断了");
        }else{
            System.out.println("子线程未被中断");
        }
    }

    // 子线程执行的业务方法
    public void service(){
        try {
            // 这边模拟子线程在执行的过程中挂起
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            // 这边捕获到异常什么也不做
        }
    }
}

上面的例子演示了,当我们捕获到InterruptedException 异常后(中断标记被重置为false),我们什么都没做,这边导致调用方无法正确判断中断标记,所以导致错误的逻辑。我们可以通过如下方法处理:

	// 子线程执行的业务方法
    public void service(){
        try {
            // 这边模拟子线程在执行的过程中挂起
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            // 保留线程中断标记
            Thread.currentThread().interrupt();
        }
    }

当捕获到InterruptedException异常后,通过调用Thread.currentThread().interrupt()方法保留中断标记,这样调用者就可以知道当前线程被中断了。

6. 等待(wait)/通知(notify)

在Java中,Object.wait及Object.nofity可用于实现和通知。Object.wait的作用是使其执行线程被暂停(生命周期状态变为WAITING),该方法可用来实现等待;Object.notify的作用是唤醒一个被暂停的线程,调用该方法可实现通知。我们看下使用方法:

public class ThreadWaitNofity {
    public static void main(String[] args) {
        Object o = new Object();
        // 新建一个等待线程
        Thread waitThread = new Thread(() -> {
            // 注意:Object.wait方法只能在synchronized块中执行
            synchronized (o){
                try {
                    System.out.println("线程进入了等待,开始等待时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                    o.wait();
                    System.out.println("线程被唤醒了,被唤醒时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 恢复线程中断标记
                    Thread.currentThread().interrupt();
                }
            }
        });

        // 新建一个通知线程
        Thread notifyThread = new Thread(() -> {
            try {
                // 延迟3秒钟
                Thread.sleep(3000);
                // 注意:Object.notify方法只能在synchronized块中执行
                synchronized (o){
                    o.notify();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 启动等待线程
        waitThread.start();
        // 启动唤醒线程
        notifyThread.start();
    }
}

运行结果如下:

线程进入了等待,开始等待时间:2020-03-11 14:49:14
线程被唤醒了,被唤醒时间:2020-03-11 14:49:17

关于wait方法与sleep方法的区别在面试中经常被问及,我们整理如下:

wait sleep
使用限制 只能在同步synchronized中调用wait方法 不需要在同步中调用
作用对象 wait方法定义在Object类中,作用于对象本身 sleep方法定义在Thread中,作用于当前线程
释放锁资源 释放锁 不释放锁
唤醒条件 其他线程调用对象的notify()或者notifyAll()方法 超时或者中断
方法属性 wait是实例方法 sleep是静态方法
发布了7 篇原创文章 · 获赞 3 · 访问量 2101

猜你喜欢

转载自blog.csdn.net/ym572170/article/details/104790598