Java并发学习(一):进程和线程

好好学习,天天向上


本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航

前言

俗话说得好“一人拾柴火不旺,众人拾柴火焰高”,一个人的力量毕竟是有限的,想要把一件事情做好,免不了需要一帮人齐心协力。同样的道理,一个复杂程序里面不会只有一个线程在工作,必然是很多个线程在一起工作。那么,这篇文章作为Java并发学习系列的第一篇,就来聊一聊Java并发的基础知识:进程和线程。

进程和线程概念

进程

什么是进程呢?进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。简单的说进程就是一个正在运行的程序,打开任务管理器,可以看到有很多的进程。

比如写文章用的Typora,写程序用的IDEA等,这些都是进程。

线程

那么线程又是什么呢?线程是操作系统能够进行运算调度的最小单位。每个进程里面都包含了一个或多个线程,比如我正在运行的IDEA里面就包含了58个线程。

举个栗子:平时大家应该会用视频软件看视频吧,那么播放视频,下载视频,上传视频,弹幕功能等,这些都是一个个单独线程,这些线程之间可以互相协作,也可以互不干扰的运行。

线程和CPU之间的关系

线程作为操作系统的最小调度单位,同一时间只能运行在一个CPU核心上。现在的操作系统都是分时操作系统,一个线程不会一直占用一个CPU核心,而是等待操作系统的任务调度器分配CPU时间片给一个线程,这个线程才能运行,否则只能在一边等着。就像网吧一样,每台机子就相当于一个CPU核心,上网的人就是线程。每个人不管时间多少都是时间限制的,下机后就给另外的人上,没有机子的人就在一边等着别人下机。不过网吧上网时间是由你自行决定的,而线程的运行时间是操作系统分配的。不过一个CPU时间片特别短,所以你感觉所有的线程都是一起运行的,其实是存在线程切换的。

并行和并发

既然要学习Java并发,那么首先得知道并行和并发的概念吧。

并行

  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

举个栗子:中午了,大家都在食堂打饭,现在有两个窗口,前面排了两队人,每个打饭阿姨同一时间只要应付一个同学,这就是并行

并发

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力

还是打饭的例子,现在只有一个阿姨在打饭,但前面还是排了两队人,这样的话,阿姨一次要处理两个同学的打饭请求,这就是并发

Java线程

多线程的好处

前面介绍过,线程是操作系统进行调度的最小单位,一个线程同一时间只能运行在CPU的一个核心上,我们都知道,现在的CPU都是多核的。假如我们的程序只有一个线程,那么只会用到CPU的一个核心,那我们搞多核CPU不就浪费了吗,所以就需要多线程去提高程序的运行效率。打个比方,现在有个软件开发的任务,需要前端,后端,美工,但现在只有一个人身兼多职,其它几个人在边上喝茶,就相当于单线程运行,开发效率肯定不高。要是这几个人分工明确,齐心合作,开发效率自然就上去了。这就是多线程带来的好处。

创建和运行线程

1. 继承Thread ,重写run() 方法

第一种创建线程的方式是新建一个类继承自 Thread ,然后重写父类的 run() 方法,并在里面编写代码即可:

class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("创建了线程");
    }
}

启动线程的方式很简单,只要创建MyThread类的实例,然后调用start()方法就行了:

new MyThread().start();

其实也可以简单一点,直接使用匿名类的方式,原理都是一样的:

new Thread() {
    @Override
    public void run() {
        System.out.println("创建了线程");
    }
}.start();

2. 实现Runnable接口

第一种方式的耦合性比较高,一般采用实现Runnable接口的方式去创建线程:

class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println("创建了线程");
    }
}

启动线程的方式是创建Thread类的实例,接收一个Runnable参数,因为MyThread类实现了Runnable接口,所以把MyThread类的实例当作参数传入Thread类的构造方法,最后调用Thread类的start()方法就可以了。

new Thread(new MyThread()).start();

3. Callable+FutureTask

前面两种方式创建的线程都没有返回值,要想线程有返回值就可以使用第三种方式去创建线程:

FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return 12345;
    }
});
new Thread(task).start();
  • 让一个类实现Callable接口,重写里面的call方法,泛型就是返回值的类型,可以是任意数据类型。这里使用了匿名类的写法。
  • 创建一个FutureTask类的实例,将Callable对象作为参数传入其构造方法。
  • new一个Thread类的实例,将FutureTask对象当作参数传入,调用其start()方法启动线程。

这时候线程就成功创建并启动了,那怎么获取返回值呢?直接调用FutureTask的get()方法就可以了。

task.get();

这样就可以获取到返回值了,不过这里有个需要注意的地方,当调用get()方法的时候,线程就会阻塞在这里,直到成功获取返回值。我们可以来写段代码测试一下是不是这样:

FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000);
        return 12345;
    }
});
new Thread(task).start();
long start = System.nanoTime();
task.get();     //线程会阻塞在这里
long end = System.nanoTime();
long time = (end-start)/1000000;
System.out.println("耗费的时间为:"+time + "毫秒");

运行一下,看看结果:

从结果中可以看出,get()方法确实会阻塞线程。

4. 线程池

最后一种方式就是借助线程池,这也是《阿里巴巴Java开发手册》中推荐使用的方式。

但是比较尴尬的是我现在水平不够,还不会线程池🌝,等我什么时候学习了线程池再来写篇文章详细地讲一讲。

线程状态

Thread类中有个枚举State,规定了Java的线程状态分为6种,分别是NEWRUNNABLEBLOCKEDWAITINGTIME_WAITINGTERMINATED

  • NEW(初始状态):线程被创建出来了,但是还没有调用其start()方法。
  • RUNNABLE(运行状态):这种状态比较宽泛,不是单一的状态,当一个线程调用了start()方法后但还没有分配到CPU时间片,处于可运行状态;线程获得了CPU时间片,正在运行;线程调用了阻塞API,比如文件读取等,处于操作系统级别的阻塞状态。这三种状态都叫做RUNNABLE。
  • BLOCKED(阻塞状态):这种阻塞和上面的阻塞状态不同,这种阻塞阻塞于锁的。比如你现在想上厕所,但是里面有人,门被锁了,你就只能在门口等着。
  • WAITING(等待状态):表示线程进入等待状态,进入该状态表示当前线程需要等待其它线程做出一些特定动作。就像工厂流水线一样,只有等到前面一个人完成了他的工作你才能开始你的工作。例如join()方法。
  • TIME_WAITING(超时等待状态):和上一种等待状态不同,这种等待状态有时间限制。就像你在网吧上网,有时间限制。例如调用了Thread.sleep()方法。
  • TERMINATED(终止状态):表示当前线程已经执行完毕了。

线程并不会一直处于某一种状态下,会随着程序的运行而在几种状态中来回切换。我在《Java并发编程的艺术》中找了一张图:

接下来写段代码来看看线程的几种状态:

//线程被创建了,但是没有调用start()方法去启动线程,所以是NEW
Thread t1 = new Thread("t1") {
    @Override
    public void run() {
        System.out.println("running...");
    }
};

//线程会一直运行,所以是RUNNABLE
Thread t2 = new Thread("t2") {
    @Override
    public void run() {
        while(true) {

        }
    }
};
t2.start();

//打印完一句话后线程就运行完了,所以打印完后就进入了TERMINATED状态
Thread t3 = new Thread("t3") {
    @Override
    public void run() {
        System.out.println("hello");
    }
};
t3.start();

//调用了sleep方法,会处于等待状态,但有时间限制,所以是TIME_WAITING
Thread t4 = new Thread("t4") {
    @Override
    public void run() {
        synchronized (TestState.class) {
            try {
                Thread.sleep(60000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};
t4.start();

//调用了join方法,会等待t2线程运行结束,但不知道要等多久,所以就是WAITING状态
Thread t5 = new Thread("t5") {
    @Override
    public void run() {
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};
t5.start();

//和t4线程一样都加了锁synchronized (TestState.class),
//但是要等t4运行完后释放锁才能运行,所以被阻塞在了这里,是BLOCKED状态
Thread t6 = new Thread("t6") {
    @Override
    public void run() {
        synchronized (TestState.class) {
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};
t6.start();

System.out.println("t1 state : " + t1.getState());
System.out.println("t2 state : " + t2.getState());
System.out.println("t3 state : " + t3.getState());
System.out.println("t4 state : " + t4.getState());
System.out.println("t5 state : " + t5.getState());
System.out.println("t6 state : " + t6.getState());


从打印的结果中可以看出,和代码注释部分分析的一样。

Daemon守护线程

一个Java进程中,只有当所有的线程都运行结束后,Java进程才会结束。但是有一种线程例外,就是Daemon线程,又被叫做守护线程,一个线程是守护线程时,当其它的非守护线程运行结束后,无论守护线程有没有执行结束,程序都会停止。我们来看一段程序:

Thread daemonThread = new Thread("daemon") {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hello");
        }
    }
};
daemonThread.start();

Thread.sleep(1000);

代码很简单,daemonThread中每隔0.1秒就会打印一句话。主线程沉睡1秒后代码就执行完了,但是因为daemonThead不会结束,所以整个Java程序就不会结束,会一直运行下去。那我们再将daemonThread设置为守护线程,只要在调用start()方法启动线程之前调用setDaemon(true)方法就可以将其设为守护线程。

…………
daemonThread.setDaemon(true);	//将daemonThread设置为守护线程
daemonThread.start();

Thread.sleep(1000);
System.out.println("程序结束了");


从打印结果中我们可以看出,和我们预期的一样,当主线程结束后,整个Java程序就结束了。

常用方法

  • start()

start()方法的作用就是启动一个线程。但需要注意的一点就是它只是让线程进入就绪状态,并不会立即运行。也就是告诉了CPU这个线程已经准备好运行了,等到CPU分配了时间片给这个线程,这个线程才会真正地开始运行。

  • run()

run()方法很简单,就是线程启动后运行的代码,前面讲创建线程的时候也说过,继承Thread和实现Runnable接口两种方式都是重写了run()方法,线程执行的代码都写在了run()方法里面。

  • sleep(long millis)

sleep()是比较常用的让线程休眠的方法,就是让线程从运行状态进入到TIME_WAITING状态,让出CPU时间片,sleep()方法接收一个毫秒值作为参数,表示线程睡眠的时间。但是到时间后,线程并不一定会继续运行,还是要等到CPU分配时间片给该线程才能继续运行。

  • yield()

yield()方法表示该线程准备让出时间片给其它线程,线程也会从运行状态进入到就绪状态。但是能不能成功让出还是得看操作系统的任务调度器,假如现在没有其它线程想要CPU时间片,那么该线程又会继续运行。就像你做公交车,你准备把座位让给其他人坐,要是有人坐的话就让他坐,但要是其他人都不坐,你不就接着坐嘛。

  • join()join(long millis)

join()方法表示等待一个线程结束后再去执行自己的动作。join(long millis)方法接收一个毫秒值作为参数,表示最多等待多长时间。举个例子:你和你女朋友准备出去吃饭,但是她现在正在化妆,你只有等她搞好了才能走,join()方法就是一直等着,join(long millis)方法有个时间限制,比如半个小时,时间到了之后你女朋友还没搞好就不等她了,自己先走。然后你就凉凉了😑~~~

  • interrupt()interrupted()Thread.interrupted()

这三个方法三两句话说不清,我放在了下一节来说。

  • getId()getName()setName(String name)getState()isAlive()currentThread()

这几个方法比较简单,就是字面意思。获取线程Id、获取线程名称、设置线程名称、获取线程状态、判断线程是否存活、获取当前线程。

  • getPriority()setPriority(int newPriority)

这两个方法分别是用来获取线程优先级以及设置线程的优先级。线程的优先级优先级越高,获取CPU时间片的概率就越高,不过设置优先级通常都没什么用,还是要看CPU的调度。所以优先级这块儿我就没有单独拿出来说,而是在这里一笔带过。

怎么优雅的停止一个线程

虽然Thread提供了停止线程的stop()方法,但是不安全,处理不当会造成死锁,而且官方已经不建议使用了,所以就来说一说更加安全的两阶段终止模式

线程中断

  • interrupt()isInterrupted()

当一个正在运行的线程调用了interrupt()方法,线程就会被中断,中断是一个标志位,线程不会立刻停止,就相当于打了个招呼,告诉它可以停止了。打个比方,你正在聚精会神的敲代码,同桌拍了你一下,喊你去吃饭,你不会立即就停止,肯定想着把一段代码写完再去。我就是这样优秀的人,一认真就根本停不下来。

写段代码演示一下:

Thread thread = new Thread(()->{
    while (true){
        
    }
});
thread.start();
System.out.println(thread.isInterrupted());	//线程还没被打断,所以是false
thread.interrupt();	//打断线程
System.out.println(thread.isInterrupted());	//线程被打断了,所以是true


但是调用interrupt()方法打断正处于阻塞状态的线程是会清除中断标记的。就是调用之后还是中断标记还是false。

这很好理解,中断本身就是打断正在运行的线程,要它让出CPU时间片,那么线程已经sleep了,没有占用CPU时间片,又谈何中断呢。

  • Thread.interrupted()

这是个静态方法,同样也是用来测试当前线程是否已经中断,和isInterrupted()方法不同的地方在于Thread.interrupted()方法会清除中断标记,也就是说连续调用两次该方法第一次的结果是true,第二次是false,而isInterrupted()不会清除中断标记。

Thread thread = new Thread() {
    @Override
    public void run() {
        while (true) {
            if (Thread.interrupted()) { //true
                System.out.println("线程的中断标记:"+Thread.interrupted());
            }
        }
    }
};
thread.start();
thread.interrupt();


从结果中我们可以看出,既然能进到if语句里面,说明Thread.interrupted()的结果肯定是true,在if里面又调用了一次Thread.interrupted()方法,结果却是false,说明它确实会清除中断标记。

两阶段终止模式

理解了线程中断的概念之后,两阶段终止模式就很好理解了,直接上代码:

Thread thread = new Thread() {
    @Override
    public void run(){
        while (true) {
            System.out.println("线程正在运行");
            if (Thread.currentThread().isInterrupted()) {   //判断当前线程是否被中断
                System.out.println("准备停止运行,正在做最后的处理...");
                System.out.println("..................");
                System.out.println("处理完毕,可以停止运行");
                break;      //跳出循环,结束线程
            }
        }
        System.out.println("------线程结束了-------");
    }
};
thread.start();
thread.interrupt();     //将thread线程中断
Thread.sleep(100);
System.out.println("thread线程的线程状态:"+thread.getState());


在主线程中将thread线程中断,thread线程中,判断是否被中断,如果被中断了就执行一些处理后事的代码,然后一个break跳出循环结束程序。采用这种方式就很安全,给了线程处理后事的机会,而不是一刀切。

写在最后

写到这里就结束了,本篇文章先是简单地介绍了一些基本概念,然后讲了讲Java线程的一些基本知识,包括创建线程,常用方法以及如何停止一个线程等内容。希望看完这篇文章能让你有些收获。如果觉得文章写的还不错,不要忘了点赞,收藏,转发,关注。要是有什么好的意见欢迎留言。让我们下期再见!

微信公众号

posted @ 2020-07-25 16:12  Robod丶  阅读(231)  评论(0编辑  收藏  举报