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方法的区别在面试中经常被问及,我们整理如下:

waitsleep
使用限制只能在同步synchronized中调用wait方法不需要在同步中调用
作用对象wait方法定义在Object类中,作用于对象本身sleep方法定义在Thread中,作用于当前线程
释放锁资源释放锁不释放锁
唤醒条件其他线程调用对象的notify()或者notifyAll()方法超时或者中断
方法属性wait是实例方法sleep是静态方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值