java并发编程:基础篇

2021年第一个安排,搞一波java并发编程的知识。大概会分几篇来展开,本次首先梳理一下线程的基础知识。

1 什么是线程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
在这里插入图片描述

程序计数器:是一个内存区域,用来记录线程当前要执行的指令地址,方便线程重新获得CPU之后,定位上次执行的地址。这个计数器只有在执行java代码的时候才有值,执行的是native方法,则计数器为undefined地址。

:用于存储该线程的局部变量,以及线程的调用栈帧。

:进程中最大的一块内存,被进程中的所有线程共享,进程创建时分配,存放一些对象实例;

方法区:用来存放JVM加载的类,常量及静态变量等信息,也被线程共享。

2 线程创建

java中有三种创建方式:继承Thread类并重写run方法;实现Runnable接口的run方法,使用FutureTask方式。他们各有特点,可以根据具体场景选择合适的创建方式。

2.1 使用继承Thread类的方式

创建线程对象并用start()方法启动线程。

public class ThreadDemo {
    public static class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("by extends Thread");
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

好处:

在run方法里获取当前线程,直接使用this就好,不需要使用Thread.currentThread()方法;

方便传参,直接使用构造方法,set方法。

坏处:

  • 任务没有返回值;
  • java不支持多线程,继承Thread类之后,无法再继承其他类;
  • 任务与线程代码没有分离,当多个线程执行一样的任务时,需要多份任务代码,而Runnable没有这个限制。

2.2 实现Runnable接口

将任务剥离出来,实现Runnable接口,新建Thread对象的时候传入任务实例。

public static class RunnableTask implements Runnable{

    @Override
    public void run() {
        System.out.println("by implements Runnable");
    }
}
public static void main(String[] args) {
    RunnableTask runnableTask = new RunnableTask();
    new Thread(runnableTask).start();
    new Thread(runnableTask).start();
}

如上代码所示,Thread提供了接受Runnable参数的构造方法,利用该方法,可以将任务单独抽取出来,且RunnableTask也可以继承其他类,实现其他接口,避免了使用继承方式的弊端。

缺点:

只能使用主线程里声明为final的常量;(但是可以给不同的任务实例,赋予不同的参数)

任务没有返回值。

2.3 FutureTask方式

Runnable解决了任务与线程区分开的问题,但是还是存在没有返回值的问题。

public static class CallerTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "by FutureTask";
    }
}

public static void main(String[] args) {
    FutureTask<String> futureTask =new FutureTask<>(new CallerTask());
    new Thread(futureTask).start();
    try{
        String result = futureTask.get();
        System.out.println(result);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这里使用了FutureTask的传CallTask类型的构造方法,实例化一个futureTask之后,使用Thread传入FutureTask的构造方法,创建一个实例。

最后再通过futureTask.get()获得返回结果。

3 线程的状态切换

3.1 线程的状态

在jdk中,定义了线程的六种状态:

  • NEW :一个已经创建的线程,但是还没有调用start方法启动;
  • RUNNABLE :正在运行或者正在等待CPU资源;
  • BLOCKED : 阻塞状态,当线程准备进入synchronized同步块或同步方法的时候,需要申请一个监视器锁而进行的等待,会使线程进入BLOCKED状态。
  • WAITING : 调用了**Object.wait()或者Thread.join()或者LockSupport.park()**进入该状态。处于该状态下的线程在等待另一个线程执行一些其余action来将其唤醒。
  • TIMED_WAITING : 该状态和上一个状态一样,不过其等待的时间是明确的。
  • TERMINATED : 线程执行结束,run方法执行结束表示线程处于消亡状态。

其原版注释如下:

 public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

线程状态切换如下图所示:

在这里插入图片描述

3.2 状态切换的函数

3.2.1 通知与等待

  1. Object.wait():

当一个线程调用了共享变量的wait()方法,那么该调用的线程会被挂起;

直到:

  • 其他对象调用了该共享变量的notify()或者notifyAll()方法;
  • 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

以上两种情况发生一种,线程进入运行状态。

如果调用wait()的线程,事先没有获得到该对象的监视器锁,那么会抛IllegalMonitorStateException异常。

通常获得监视器锁的方法是使用synchronized关键字声明代码段,或者方法。

synchronized(共享变量){
	// do something
}

synchronized void test(int a){
    // do something
}

值得注意到是,线程从挂起到运行可能不经过上述的情况,定义为虚假唤醒,所以为了防止这种意外的发生造成程序错误,在拿到监视器之后的第一件事是检查该线程被唤醒的条件是否满足,不满足则进入挂起状态:

synchronized(obj){
	while(条件不满足){
        obj.wait();
    }
	// do something
}

此外,wait()方法还有一些重载的方法:

  • wait(long timeout): 设置了超时返回的界限,如果参数是0,等同于wait();
  • wait(long timeout, int nanos): 调用的还是wait(long timeout),在nanos>0的时候,timeout+1,传入wait(long timeout)。
public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }
  1. notify():

主要作用就是,一个线程调用了共享对象的notify方法之后,会唤醒一个在该共享变量上调用wait()系列方法被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个是随机的。

notifyAll():不同点在于会唤醒所有因该共享变量挂起的线程。

3.2.2 等待线程终止

join()方法,由Thread类提供,无参数,无返回值。

主要的应用场景是,需要等待多个事情完成之后,再继续处理。

public static void main(){
    ...
    threadOne.start();
    threadTwo.start();
    
    // 等待子线程完成
    threadOne.join();
    threadTwo.join();
    // do something
}

如上例子,主线程在开启两个子线程之后,调用了threadOne.join(),进入阻塞状态,等待threadOne完成之后,threadOne.join()就会返回,然后继续调用threadTwo.join(),等待第二子线程的完成。实现对多个线程的等待。

值得注意的是:当线程A,调用了线程B的join方法,A进入阻塞状态之后,其他线程C调用了线程A的interrupt()方法中断了线程A时,A会抛出InterruptedException异常而返回。(也就是说,这种情况下,是不可以中断的)

3.2.3 让线程睡眠

Thread类中提供了一个静态方法void sleep(long millis),当一个执行中的线程调用了Thread的sleep方法,调用的线程会暂时让出指定时间的时间执行权,这段时间不参与CPU调度,但是拥有的监视器资源不释放。指定时间后,线程重新进入就绪状态,等待CPU资源。

如果在睡眠期间,其他线程调用了该线程的interrupt方法,中断了该线程,则会在调用sleep()的地方抛出InterruptedException异常。

3.2.4 让出CPU执行权

Thread类中提供了一个静态方法void yield()。

当一个线程调用了yield方法,相当于通知调度器,我已经完成工作,可以让出CPU,进行下一轮调度。

所以调用的效果就是让出CPU使用权,处于就绪状态,不再等待CPU的时间片使用完。

3.2.5 线程中断

线程中断,一种线程间的协作模式,可以设置线程的中断标志,不会直接终止线程的执行,而是由被中断的线程根据中断状态自行处理。

  • void interrupt()方法:中断线程,当线程A运行时,B调用了A的interrupt()方法,来设置线程A的中断标志位为true,并立即返回。此时A没有中断,只是被修改了标志位,会继续执行。需要注意的是,当A因为wait系列的函数,join或sleep方法被阻塞挂起时,此时B再调用A的interrupt()方法就会抛出InterruptedException异常而返回。
  • boolean isInterrupted()方法:检测当前线程是否被中断。
  • boolean Interrupted()方法:检测当前线程是否被中断,与isInterrupted()不同的是,如果发现当前线程被中断,会清楚中断标志。

线程使用Interrupted()退出:

try{
    while(!Thread.currentThread().isInterrupted && other){
    	// do 
	}
} catch (InterruptedException e){
    // 线程在阻塞状态,执行interrupt()
}
finally{
    // 释放资源
}


其实就是相当于,当需要线程之间进行中断的时候,被中断的线程要在逻辑上加上对中断标志的判断,以及相关处理。

此时,其他线程调用了该线程的interrupt方法,修改了标志位,该线程就会根据标志位进行相关操作。

3.3 死锁

死锁产生的四个条件:

  • 互斥条件:资源同时只能由一个线程占用;
  • 请求并持有条件:一个线程已经拥有一个资源,还要申请新的资源;
  • 不可剥夺条件:线程在获取资源之后,在使用完之前不能被其他线程抢走;
  • 循环等待条件:存在线程之间互相等待的环形链。

避免死锁就是破坏这四个条件,在java中主要的解决方式: 一种是用synchronized,一种是用Lock显式锁实现。

本文参考:《java并发编程之美》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值