并发编程之线程详解

版权声明:转载请注明地址,谢谢配合 https://blog.csdn.net/pulong0748/article/details/88061530

认识线程

一.:线程、进程、子例程、协程的区别

线程:执行线程是可由调度器独立管理的最小程序指令序列,调度器通常是操作系统的一部分。

进程:进程是在计算机中运行的程序的实例。它与任务有意义,与任务一样,进程是一个运行程序,通过该程序关联特定数据集,以便可以跟踪进程。由多个用户共享的应用程序通常在每个用户的某个执行阶段具有一个进程。

线程与进程:线程和进程的实现在操作系统之间有所不同,但在大多数情况下,线程是进程的一个组件多个线程可以存在于一个进程中并发执行和共享资源(如内存)而不同的进程不共享这些资源。特别是,进程的线程共享其可执行代码及其值在任何给定时间动态分配变量和非线程局部 全局变量。如图:

1546172659542

子例程:一般地认为,子例程是某个主程序的一部分代码该代码执行特定的任务并且与主程序中的其他代码相对独立。子例程又被称为子程序过程方法函数等。在主程序中可以调用子例程来执行。函数,是一种子程序,利用函数名称,可以接收回传值。

协程:协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。一个程序可以包含多个协程,可以对比与一个进程包含多个线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。


二:线程的生命周期

线程的生命周期一共分为以下6种状态:

  1. New
  2. Runnable
  3. Blocked
  4. Waiting
  5. Timed Waiting
  6. Terminated

如图:

1546171276771

  1. New:创建新线程,当处于此状态的时候,表示线程代码还未运行。

  2. Runnable:准备运行的线程被移动到可运行状态。在这种状态下,线程可能实际上正在运行或者它可能在任何时刻都准备好运行。线程调度程序负责为线程提供运行时间。多线程程序为每个单独的线程分配固定的时间量。每个线程都会运行一段时间,然后暂停并放弃CPU到另一个线程,这样其他线程就有机会运行。发生这种情况时,所有准备运行的等待CPU调度的线程当前运行的线程的线程都处于runnable状态。

  3. Blocked/waiting

    当线程暂时处于Blocked/waiting状态时,它处于以下状态之一:

    • 阻塞
    • 等待

    线程在对象上调用wait()join()方法时进入waiting状态。在进入waiting状态之前,线程释放它所持有的对象的锁。它将保持waiting状态,直到任何其他线程在同一对象上调用notify()notifyAll()

    一旦另一个线程在同一个对象上调用notify()notifyAll(),将通知一个或所有等待锁定该对象的线程。所有通知的线程都不会立即获得对象锁定。一旦当前线程释放锁定,它们将优先获得对象锁定。直到他们将处于Blocked状态。

    简单点来说,如果线程正在等待来自其他线程的通知,则该线程将处于waiting状态。如果线程正在等待其他线程释放它想要的锁,则该线程将处于Blocked状态。

1546173011988

waiting和Blocked总结如下:

waiting Blocked
当调用wait()join()方法时,线程将保持waiting状态,直到任何其他线程调用notify()notifyAll() 当其他线程通知但尚未获得对象锁定时,线程将处于Blocked状态。
waiting线程正在等待来自其他线程的通知。 Blocked线程正在等待其他线程释放锁。
waiting线程可以被中断。 Blocked线程无法中断。
  1. Timed Waiting:线程在调用具有超时参数的方 法时处于定时等待状态。线程处于此状态,直到超时完成或收到通知为止。例如,当线程调用sleep或条件等待时,它将被移动到定时等待状态。

  2. Terminated:由于以下原因之一,线程终止:

    • 发生了错误事件,比如error或者未处理Exception.

    处于已终止状态的线程不再消耗任何CPU周期。


三:线程的基本操作

1.新建线程

java定义了两种创建线程的方式:

  • 通过实现Runnable接口。
  • 通过扩展Thread类。

1.继承Thread类

通过继承Thread类并创建该类实例的新类创建线程的另一种方法。扩展类必须覆盖**run()**方法,该方法是新线程的入口点。

public class Mythread extends Thread {
    @Override
    public void run() {
        System.out.println("我的线程开始运行。。。");
    }

    static class MyTheadDemo{
        public static void main(String[] args) {
            Mythread mythread = new Mythread();
            mythread.start();
        }
    }

运行结果:

我的线程开始运行。。。

在这种情况下,我们必须覆盖run(),然后使用**start()**方法来运行线程。此外,当创建MyThread类对象时,还将调用Thread类构造函数。

 public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

假如线程直接使用run()方法,而不是start方法会如何?

 static class MyTheadDemo{
        public static void main(String[] args) {
            Mythread mythread = new Mythread();
            mythread.run();
        }
    }

运行结果:

我的线程开始运行。。。

同样是会打印这句话,但是区别是:这样做,线程将不会被分配一个新的调用堆栈,它将开始在当前调用堆栈中运行,即线程的调用堆栈。因此多线程不会存在。

1546165984671

  • start()方法:
    • 用于启动线程,调用start()方法表明线程处于就绪状态,等待cpu调度,去执行run()方法。
  • run()方法:
    • 存在于main线程中,只是被当做一个函数而已。

当然这一点是可以验证的,代码如下:

public class Mythread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("我的线程是:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    static class MyTheadDemo{
        public static void main(String[] args) {
            Mythread mt = new Mythread();
            mt.run();
            //mt.start();
            System.out.println("main线程的线程:"  + Thread.currentThread().getName());
        }
    }
}

运行结果:

执行run()方法

我的线程是:main
main线程的线程:main


执行start()方法

main线程的线程:main
我的线程是:Thread-0

执行run方法之后才会继续执行接下来的代码,而执行start方法,(因为我让线程睡了5秒),所以会先执行main线程中的方法。

如果开启两次线程如何?

如果开启两次则会抛出异常

Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at codecarver.chapter1.MyRunable$MyThread2Demo.main(MyRunable.java:12)

当线程处于运行状态,并且尝试再次启动它,或者任何方法尝试使用**start()**方法再次调用该线程时,将抛出异常。


2.实现Runnable接口

创建线程的最简单方法是创建一个实现runnable接口的类。在实现runnable接口之后,该类需要实现**run()**方法,该方法是一种形式

public void run()
  • run()方法将并发线程引入您的程序。当run()方法终止时,该线程将结束
  • 必须指定线程将在run()方法中执行的代码。
  • run()方法可以调用其他方法,可以像使用任何其他常规方法一样使用其他类和声明变量。
public class MyRunable implements Runnable {
    public void run() {
        System.out.println("我的线程");
    }
    static class   MyThread2Demo{
        public static void main(String[] args) {
            MyRunable myRunable = new MyRunable();
            Thread t = new Thread(myRunable);
            t.start();
        }
    }
}

要调用**run()方法,需要使用start()**方法。在调用start()时,会向线程提供一个新堆栈,并调用run()方法将新线程引入该程序。

**注意:**如果要在类中实现Runnable接口,则需要显式创建Thread类对象,并且需要将Runnable接口实现的类对象作为其构造函数中的参数传递。

注意:启动一个线程前最好为这个线程设置线程名称,这样在使用jstack分析程序或者进行问题排查的时候,能比较精确的找到哪个线程。


2.线程中断

中断可以理解为线程的一个标识位属性,表示一个运行中的线程是否被其他线程进行了中断操作。

但是线程中断不会使得当前线程立即退出,而是通知当前线程,其他线程希望你退出,而什么时候退出则由当前线程自己决定。

常见API:

1. Thread.interrupt() // 中断线程 
2. Thread.isInterrupted() // 判断线程是否中断
3. Thread.interrupted() // 判断线程是否中断并且清除中断状态 *
  1. Thread.interrupt()是一个实例方法,用来设置中断标志位(通知目标线程中断)。
  2. Thread.isInterrupted()也是实例方法,通过检查中断标志位来判断当前线程是否被中断。
  3. Thread.interrupted()是静态方法,用来判断线程的中断状态并清除当前的中断标志位状态

代码示例:

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("中断。。。");
                }
            }
        };
        t1.start();
        //当前执行t1线程的线程睡2秒
        Thread.sleep(2_000);
        //中断t1
        t1.interrupt();
    }
}

上面的代码会一直打印出“中断。。。”,也就印证了上面*interrupt()*方法只是设置一个中断标志位,并不会做中断处理。如果希望我们检测到中断标志之后,退出被中断的线程,可以这么做:

public class InterruptDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("我还没被中断呢");
                   if(Thread.currentThread().isInterrupted()){
                       System.out.println("我被中断了");
                       break;
                   }
                }
            }
        };
        t1.start();
        Thread.sleep(2_000);
        //中断t1
        t1.interrupt();
    }
}

上面代码可以发现,执行了大约2秒的"我还没中断呢"之后,线程检测到中断标志位,然后打印出我被中断了,跳出了循环。


3.线程终止

1.为什么要废弃stop

stop方法是被JDK废弃的方法,用来立即终止线程执行。然而一个正在运行的线程,它有自己的堆栈,也许也持有锁,如果终止它,会导致释放所有的监视器,这是极其不安全的做法。比如写线程正在写入文件还没完成,就被你stop了,那么它会丢掉它的锁,那么读线程进来后,读取的信息将会是错的。这将导致不可预知的后果。我们先来看一个stop案例:

模拟了一个简单的stop案例,有两个变量i和j,要进行自增操作,打算在执行中途stop此线程,我们来看看会打印出什么?

public class StopThread extends Thread{
    private int i = 0;
    private int j = 0;

    @Override
    public void run() {
        synchronized (StopThread.class){
            ++i;
            try {
                // 模拟做加法要耗时5秒
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ++j;
        }
    }

    public void print(){
        // 打印出i和j
        System.out.println("线程:" + Thread.currentThread().getName() + ":i=" + i + "j=" + j);
    }
}
public class Client {
    public static void main(String[] args) throws InterruptedException {
        StopThread t1 = new StopThread();
        t1.start();
        // 睡2秒,表示执行了一部分
        // 睡6秒的话,数据是正常的
        Thread.sleep(2000);
        // 终止线程
        t1.stop();
        //输出结果
        t1.print();
    }
}

结果打印为:

线程:main:i=1j=0

我们在开启了t1线程之后,让main线程睡了2秒,然而执行i++,将会耗时5秒,所以一旦过了两秒,main线程抢占过来直接终止线程,导致j没有自增,也就是j=0,当我们让main线程睡眠的时间超过5秒,那么会得到正确的结果然而这并不是我们期盼的结果,因为线程执行的时间可能因为网络状态,读写速度等无法控制,如果是在真实的业务场景中,我们并不能发现问题所在,因为它不报任何错,所产生的数据不一致问题将很让人头疼。我们希望的是能够自己控制线程何时退出。


2.自定义标记变量终止线程

public class StopThread extends Thread{
    private int i = 0;
    private int j = 0;
    // 保证数据的可见性
    volatile boolean stopme = false;
    public void stopMe(){
        stopme = true;
    }

    @Override
    public void run() {
        while (true){
            if(stopme){
                System.out.println("通过标记变量退出");
                break;
            }
            synchronized (StopThread.class){
                ++i;
                try {
                    // 模拟做加法要耗时5秒
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ++j;
            }
        }
    }
    public void print(){
        // 打印出i和j
        System.out.println("线程:" + Thread.currentThread().getName() + ":i=" + i + "j=" + j);
    }
}
public class Client {
    public static void main(String[] args) throws InterruptedException {
        StopThread t1 = new StopThread();
        t1.start();
        // main线程睡2秒,子线程执行了一部分
        Thread.sleep(2000);
        t1.stopme = true;
        //确保线程已经终止
        while(t1.isAlive()){
        }
        //输出结果
        t1.print();
    }
}

打印结果:

通过标记变量退出
线程:main:i=1j=1

通过定义标记变量stopme,来控制线程是否退出。通过控制main线程睡两秒,然后执行t1线程,但是t1在执行自增i的时候又会睡5秒,所以只会执行到++i就被main线程抢占继续执行t1.stopme=true,然后接着去执行剩下的++j代码。接下来看看JDK提供的线程中断方式来终止线程。


3.线程中断方式终止线程

在java中是没有办法可以快速的可靠的停止线程的。因此我们可以通过Interruption 机制来停止线程。原理很简单,传递一个信号,即中断它,然后由它自行决定在合适的时机停止

注意:一旦你启用某个线程,没有什么可以(安全的)阻止它,除了它自己本身。可以通过询问或中断线程来终止自己。

简单的理解中断停止线程就两步:

  1. 向线程发送打断信号
  2. 设计线程来中断自己

了解一下中断线程的三个方法:

  1. Thread.interrupt() // 中断线程
  2. Thread.isInterrupted() // 判断线程是否中断
  3. *Thread.interrupted() // 判断线程是否中断并且清除中断状态 *
  1. Thread.interrupt()是一个实例方法,用来设置中断标志位(通知目标线程中断)。
  2. Thread.isInterrupted()也是实例方法,通过检查中断标志位来判断当前线程是否被中断。
  3. Thread.interrupted()是静态方法,用来判断线程的中断状态并清除当前的中断标志位状态

代码如下:

public class StopThread extends Thread{
    private int i = 0;
    private int j = 0;

    @Override
    public void run() {
        while (true){
            if(Thread.currentThread().isInterrupted()){
                System.out.println("通过中断退出");
                break;
            }
            synchronized (StopThread.class){
                ++i;
                try {
                    // 模拟做加法要耗时50秒
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 设置中断状态
                    // 这里我们没有break,而是进行了再次设置中断标志位来进行后续的++j操作
                    Thread.currentThread().interrupt();
                }
                ++j;
            }
        }
    }
    public void print(){
        // 打印出i和j
        System.out.println("线程:" + Thread.currentThread().getName() + ":i=" + i + "j=" + j);
    }
}
public class Client {
    public static void main(String[] args) throws InterruptedException {
        StopThread t1 = new StopThread();
        t1.start();
        // main线程睡2秒,t1线程执行了一部分
        Thread.sleep(2000);
        t1.interrupt();
        //确保线程已经终止
        while(t1.isAlive()){
        }
        //输出结果
        t1.print();
    }
}

执行结果:

通过中断退出
java.lang.InterruptedException: sleep interrupted
线程:main:i=1j=1
​ at java.lang.Thread.sleep(Native Method)
​ at codecarver.charpter3.style3.StopThread.run(StopThread.java:18)

首先我们通过让main线程睡眠两秒,之后执行线程中的方法,++i耗时要5秒,之后main线程会设置中断标志位,通过 while(t1.isAlive()){}让t1线程执行完5秒后抢占到线程,从而捕获到中断异常,再次设置中断标志位为true,执行++j操作,然后if(Thread.currentThread().isInterrupted()){}判断后退出。

这里要在异常中继续设置中断标志位是因为Thread.sleep会清除标志位,如果不重新设置中断标志,那么就无法捕获异常,整个线程也会一直陷在死循环中,无法退出。


4.自己创建方法终止线程

有些情景下,我们需要让线程在给定时间内完成他要做的事情,如果超时,那么将需要终止线程。

public class ThreadService {

    private Thread executeThread;

    private boolean finished = false;

    public void execute(Runnable task) {
        executeThread = new Thread() {
            @Override
            public void run() {
                // 子线程runner是一个守护线程
                // 将其设置为守护线程,如果不join的话,那么excuteThread一旦run
                // 那么守护线程的生命周期立马会结束掉,所以在守护线程上进行join
                // 直到守护线程执行完毕再去执行excuteThread的业务逻辑
                Thread runner = new Thread(task);
                runner.setDaemon(true);

                runner.start();
                try {
                    runner.join();
                    // 守护线程执行完毕之后,设置flag为true
                    // 所以如果守护线程的生命周期很长,那么可以手动shutdown执行线程
                    finished = true;
                } catch (InterruptedException e) {
                }
            }
        };

        executeThread.start();
    }

    // 只给执行线程执行mills时间。
    public void shutdown(long mills) {
        long currentTime = System.currentTimeMillis();
        while (!finished) {
            if ((System.currentTimeMillis() - currentTime) >= mills) {
                System.out.println("任务超时,需要结束他!");
                // 通过打断来中断线程 executeThread结束,那么子线程runner也必须结束
                executeThread.interrupt();
                break;
            }


            // 既没有超时,也没有结束
            try {
                executeThread.sleep(1);
            } catch (InterruptedException e) {
                // 执行线程被打断
                System.out.println("执行线程被打断!");
                break;
            }
        }

        finished = false;
    }
}
public class Client {

    public static void main(String[] args) {
        ThreadService service = new ThreadService();
        long start = System.currentTimeMillis();
        service.execute(() -> {
            //
            while (true) {
                // 模拟一个很耗时的资源,理论只给他10秒,就会shutdown
            }
           /* try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        });
        service.shutdown(10000);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

猜你喜欢

转载自blog.csdn.net/pulong0748/article/details/88061530