多线程三部曲——线程概念与启动

平时开发过程中,多线程编程开发是我们必须掌握的技能。那么什么是多线程?如何创建多线程,什么又是线程间通信以及线程安全?下面我们就一个问题一个问题的来解答。

一、基本概念

在讲什么是多线程以及多线程编程注意项之前我们需要先了解几个基本概念。

(1)、CPU核心数和线程数的关系

如今手机都是四核、八核。那么这个核说的就是CPU的核心数。而核心数与线程数是1:1的关系,也就是说一个4核的CPU,能够同时执行4个线程,但是有些因特尔的4核CPU却能够同时执行8个线程,这得益于因特尔的超线程技术(该技术将一个物理CPU模拟成两个逻辑CPU,不要问我为什么知道,我也是百度来的。)。

(2)、CPU时间片轮询机制是什么

但是在开发过程中,我们完全感觉不到上面说的这种CPU核心数与线程数的限制关系。这就要归功于CPU的时间片轮询机制。系统在运行期会将我们的CPU进行切片,然后轮询执行每一片的程序,而每一片的执行时间都短到肉眼感受不到,所以在我们肉眼看来我们的创建的线程都是立即执行的。

(3)、什么是进程和线程,两者有什么区别

进程是一个程序执行时资源分配的最小单位,在Android中一般一个应用对应一个进程(这个进程是应用启动时由系统的Zygote进程孵化出来的一个子进程,至于什么是Zygote进程,请自行百度,因为我也还在学习,说不清怕误导。暂且叫他Android世界的盘古进程吧),当然也有一个应用对应多个进程的情况,比如启动一个服务,注册服务时给这个服务指定对应的process。这里就不一一讲解了。

线程是CPU调度的最小单位,且线程必须依附于进程而存在,且一个进程下的所有线程共享该进程的资源。

(4)、并行和并发是什么

并行:同一个时间,能够同时运行的线程数,叫做并行。

并发:单位时间内,能够运行完成的线程数,叫做并发。

(5)高并发编程的好处和注意项

好处:能够在单位时间内,执行多个任务。这样我们就能够将耗时任务放到非UI线程中去执行。

注意项:前面说了,线程会共享进程的资源,所以如果多个线程同时访问一段代码块或者同一个变量时,就会出现线程安全问题以及同步问题,如果我们增加了锁操作,还可能会造成死锁等现象。

二、Java中有几种启动多线程的方式

我记得刚开始参加工作时,师傅就问了我这个简单到不行的问题,但我依旧是没能答的上来,当时心脏怦怦跳,脸上火辣辣的。那么启动线程的方式有几种呢?上代码

/**
 *  扩展Thread
 */
public static class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        //TODO 执行我们自己的任务
        System.out.println("MyThread 执行我们自己的任务");
    }
}

/**
 * 实现Runnable接口
 */
public static class MyRunnable implements Runnable {

    @Override
    public void run() {
        //TODO 执行我们自己的任务
        System.out.println("MyRunnable 执行我们自己的任务");
    }
}

/**
 * 实现Callable接口,允许有返回值
 */
public static class MyCallable implements Callable<String>{

    @Override
    public String call() throws Exception {
        System.out.println("MyCallable 执行我们自己的任务");
        return "MyCallable 执行我们自己的任务";
    }
}

public static void main(String[] args){
    //第一种:扩展Thread,重写run方法
    MyThread thread = new MyThread();
    thread.start();
    //第二种,实现Runnable接口,重写run方法,然后交给Thread去执行
    MyRunnable runnable = new MyRunnable();
    new Thread(runnable).start();
    //第三种,实现Callable接口,重写call方法,然后创建出Callable实例后,交给FutureTask进行分装,最后交给Thread去执行,并通过futureTask.get()获取返回值
    MyCallable callable = new MyCallable();
    FutureTask<String> futureTask = new FutureTask<>(callable);
    new Thread(futureTask).start();
    try {
        System.out.println(futureTask.get());
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

这里需要理清楚几个概念:

 1、 Runnable和Callable并不是线程,在操作系统中对线程的抽象只有一个Thread。而Runnable和Callable只是对任务的抽象。所以我们说这里有三种启动线程任务的方式,而不是三种创建线程的方式,因为创建线程的方式只有一种就是创建Thread

 2、Runnable、Callable与Thread并不是一对一的关系。一个线程可以执行很多个Runnable,这在线程池中就有很好的体现。

3、Thread、Runnable的run方法或者Callable的call方法只是一个普通的方法,并没有创建新的线程。只有调用了Thread的Start方法才是真正的请求虚拟机分配一个新的线程资源,并执行对应的任务。

三、线程的生命周期和使用

前面都在说如何启动一个新的线程,那么一个线程什么时候结束,如何终止一个线程呢?在说这个问题前我们先来看一下一个线程的生命周期是什么样的,如下图:

般来说,当一个线程执行完它的run方法,那么这个线程就结束了,但有时候,我们希望在某种条件下,能够动态的终止正在执行的线程该如何做呢?

JDK同样为我们提供了终止一个线程的方法,而且不止一个。他们分是:

stop:强制终止一个线程:不管当前线程锁持有的资源是否被释放了,直接杀死。这就有可能造成当前线程所持有的部分资源没有被彻底释放。

suspend:挂起当前线程,但是不会释放当前线程持有的资源,如:锁,如果当前线程被挂起了,但是又没有释放它所持有的锁,这个时候就会导致一个类似死锁的情况,直到这个线程被重启或结束,其他线程才能够获取到锁资源。

interrupt:中断当前线程,通常与isInterrupted配合使用,这里需要注意的是,interrupt并没有立即终止当前线程,也没有立即中断当前线程,可以理解为它只是给当前线程设置了一个标志位,然后需要我们在run方法中通过isInterrupted来判断这个标志位,并手动return结束当前线程。也就是说,如果我们不通过isInterrupted来判断这个这个标志位并手动结束线程,那么这个interrupt方法就不会起任何作用。这在JAVA中叫做协作式。这里还要提一个Thread.interrupted()方法,这个方法也是用来判断中断标志的,但它和isInterrupted区别是,interrupted会在判断完中断标志后,重置中断标志位。

线程的其他常用方法:

(1)yield方法:使当前线程让出cpu的占有权,但是并不会释放锁等资源。且让出cpu占有权进入可执行状态的线程可能会立马再次被执行

(2)join方法:把指定的线程加入到当前线程,可实现线程的按顺序执行,也叫做任务插队。比如在线程B中调用线程A的join方法,直到线程A执行完成后,才会继续执行线程B。

(3)sleep方法:这是一个Thread的static静态方法,导致此线程暂停执行指定时间,让出执行机会给其他线程,但是监控状态依然保持,时间一到会自动恢复,调用sleep 不会释放对象锁。由于没有释放对象锁,所以不能调用里面的同步方法。

(4)wait方法:wait方法其实是Object类的方法,当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时会释放所持有的对象锁(暂时失去锁,wait(long timeout)超时时间到达后还需要返还对象锁)。wait()可以使用notify或者notifyAll或者指定睡眠时间来唤醒当前等待池中的线程。他与sleep最大的区别就是他会释放锁资源,而sleep不会。

发布了29 篇原创文章 · 获赞 3 · 访问量 904

猜你喜欢

转载自blog.csdn.net/LVEfrist/article/details/92088403