今天来聊聊多线程编程理论知识

1.什么是进程和线程

进程:

是操作系统进行资源分配的最小单位,资源包括CPU、内存空间、磁盘IO等。一个进程是一个独立的运行环境,它可以被看做是一个应用(Android中,一个应用程序就是一个独立的进程)

线程: 线程是进程中运行的多个子任务,是CPU调度的最小单位,必须依赖于进程而存在。

2.CPU核心数和线程数的关系

目前主流的CPU都是多核的,增加核心数是为了增加线程数,因为操作系统是通过线程来执行任务的。一般情况下它们是1:1的对应关系,也就是说四核CPU一般拥有四个线程,但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。

3.CPU时间片轮转机制

我们开发中感觉并没有受cpu核心数的限制,这是因为操作系统提供了一种CPU时间片轮转机制。

每个进程被分配一个时间段,称为它的时间片,表该进程允许允许的时间。系统会将所有的就绪进程按先进先出的原则排成一个队列,新来的进程会被加到队尾,然后每次执行进程调度的时候,都会选择队首进程,让它在CPU上运行一个时间片的时间,直到分配的时间片结束,被移到队尾,CPU重新被剥夺并分配给队首进程,如果进程在时间片前结束或阻塞也会切换。这样就可以保证就绪队列中所有的进程,在一定的时间内,均能获得一时间片的执行时间。

4.并行和并发

并行: 指应用能够同时执行不同的任务。

并发: 指应用能够交替执行不同的任务,强调单位时间内并发量。

5.多线程编程的好处和注意事项

好处: 1)充分利用CPU的资源 2)加快响应用户的时间 3)代码模块化,异步化,简单化

注意事项: 1)线程之间的安全性 2)线程之前的死锁 3)线程过多导致消耗完系统内存以及CPU的“过渡切换”

6.线程启动与中止

启动: (There are two ways to create a new thread of execution)

①继承 Thread 类重写 run() 方法

②实现 Runnable 重写 run() ⽅法,然后交给Thread运行。

Thread和Runnable的区别:

Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行。

中止:

  • 线程自然终止

run执行完成或者抛出一个未处理的异常导致线程提前结束

  • 不建议使用stop停止线程

stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放的机会,因此会导致程序可能工作在不确定状态下。不建议使用的过期方法。

  • 安全中止interrupt

interrupt() 给线程设置一个中断标识。Java里线程是协作式的,不是抢占式的,线程通过方法 isInterrupted()

来进行判断是否被中断,也可以调用 Thread.interrupted()

来进程当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改为false,即清除中断状态。

如果一个线程处于阻塞状态( 如线程调用了Thread的sleep、join、wait等,支持中断的检查

),则线程在检查中断标识时发现为true,会抛出InterruptedException异常,抛出异常后会立即将线程的中断标识清除,即设为false。

注意:处于死锁状态的线程无法被中断

7.run()和start()

Thread类是Java里对线程概念的抽象,我们通过new

Thread()其实只是new出来一个Thread实例,还没有操作系统中真正的线程挂起钩来,只有执行了start()方法后,才能真正意义上启动线程。

start()方法让一个线程进入就绪队列等待分配CPU,分到CPU后才调用实现的run()方法,start()方法不能重复调用,否则会抛出异常。

run()方法是业务逻辑实现的地方,本质上和普通方法没任何区别,可以重复执行,也可以被单独调用。

8.线程的状态和线程常用方法

线程的状态.png

  • yield

使当前线程让出CPU占用权,但让出的时间是不可设定的,也不会释放锁资源。(并不是每个线程都需要锁的,而且执行yield()的线程不一定就会持有锁,我们也完全可以释放锁后再调用yield()方法。

执行完yield()的线程进入就绪状态,有可能被系统再次选中马上又执行。

  • join

把指定的线程加入到当前线程,可以将两个交替的线程合并为顺序执行。

public class JoinThreadTest {

  private static class JoinThread extends Thread {

    public JoinThread(String name) {

      super(name);

    }

    @Override

    public void run() {

      String threadName = Thread.currentThread().getName();

      for(int i=0;i<5;i++) {

        System.out.println(threadName + "-"+i);

      }

    }

  }

  public static void main(String[] args) throws InterruptedException {

    Thread threadA = new JoinThread("ThreadA");

    Thread threadB = new JoinThread("ThreadB");

    Thread threadC = new JoinThread("ThreadC");

    threadA.start();

    threadB.start();

    threadC.start();

  }

}

运行结果:

ThreadA-0

ThreadA-1

ThreadA-2

ThreadB-0

ThreadA-3

ThreadC-0

ThreadC-1

ThreadC-2

ThreadC-3

ThreadA-4

ThreadB-1

ThreadC-4

ThreadB-2

ThreadB-3

ThreadB-4

可以看到A、B、C线程是交替执行的。

public static void main(String[] args) throws InterruptedException {

  Thread threadA = new JoinThread("ThreadA");

  Thread threadB = new JoinThread("ThreadB");

  Thread threadC = new JoinThread("ThreadC");

  threadA.start();

  threadA.join();//threadA.join()要放到threadB.start()之前

  threadB.start();

  threadB.join();//threadB.join()要放到threadC.start()之前

  threadC.start();

}

ThreadA-0

ThreadA-1

ThreadA-2

ThreadA-3

ThreadA-4

ThreadB-0

ThreadB-1

ThreadB-2

ThreadB-3

ThreadB-4

ThreadC-0

ThreadC-1

ThreadC-2

ThreadC-3

ThreadC-4

可以看到A、B、C线程是顺序执行的。

9.线程的优先级

可以通过setPriority(int)来修改线程优先级,范围1~10,默认为5,优先级高的线程分配时间片的数量要多于优先级低的线程。

针对频繁阻塞(休眠或I/O操作)的线程需要设置较高优先级,偏重计算(需要较多的CPU时间或偏运算)的线程设置较低优先级,确保处理器不会被独占。不同JVM以及操作系统上,线程规划存在差异,有的甚至会忽略对线程优先级的设定。

10.守护线程

守护(Daemon)线程是一种支持型线程,主要被用作程序中后台调度以及支持性工作,如垃圾回收线程就是守护线程。可以通过userThread.setDaemon(true)设置将userThread线程设置为守护线程。

当一个Java虚拟机中不存在非Daemon线程时(守护线程要守护的对象已经不存在),Java虚拟机将会退出。但是Java虚拟机退出时,守护线程中的finally块不一定会执行,也就是我们在构建守护线程时,不能依靠finally块来确保执行关闭或清理资源的逻辑。

正文完