Java 并发编程:线程讲解

2020-05-03 0 By admin

Java 在语言级别提供了支持多线程开发需要的类、接口和相关方法,支持线程的设计是JAVA的重要特征之一。

一、线程说明

跟进程比较,线程的特点:

  1. 线程本身不能单独运行,它必须在一个程序中运行。
  2. 线程是程序的内部控制流,一个进程在执行过程中,为了同时完成多个操作,可以创建多个线程,形成多条执行线索。每个线程有自己的堆栈、自己的程序设计器和自己的局部变量。
  3. 每个进程都有一段专用的内存区域。而同一个进程的各线程间可以共享相同的内存空间(代码空间和数据空间),并且利这些共享内存来实现数据交换、实施通信以及必要的同步工作。

线程是程序中的一个执行流,一个执行流是由CPU运行程序代码并操作程序的数据形成的。JAVA 中的线程模型就是一个CPU、程序代码和数据的封装体。线程在JAVA 中是由 java.lang.Thread 类来定义和描述的,程序中的线程都是 Thread 的实例。

二、线程的创建

JAVA 提供了一下两创建线程的方法:

  1. 继承类 Thread。定义一个类,作为类 Thread 的子类,在该类上重写方法run()。
  2. 实现 Runnable 接口。定义一个类,实现接口 Runnable,在该类上重写方法run()。

2.1、继承 Thread 类

方法 run() 是线程在运行时所要执行的动作,但是在执行线程过程中,并不是直接调用该方法,而是在由该类创建实例对象后,通过方法start()来启动线程。

2.2、实现 Runnable 接口

接口 Runnable 只有一个抽象方法 run(),在实现该接口的类中需要重写该方法。
在线程运行时,实现了接口 Runnable 的对象需要由类 Thread 封装为线程实例,类 Thread 的构造方法可以接受接口 Runnable 的实例,然后线程实例通过 start() 方法启动线程。

2.3、两种方法比较

两种创建线程的方法有所区别,各有利弊。
1、使用继承类 Thread 的方法相对简单,比较直观,但是由于 JAVA 语言是单继承机制,使得一个类继承了类 Thread 之后不能再继承其他类。
2、通过实现 Runnable 接口的方法来创建线程时,虽然在生成线程实例时需要再通过 Thread 实例对 Runnable 实例进行封装;但定义时该类可以再继承其他类。
Thread 类不支持线程重用。在处理多任务时,通常会创建多个线程,让一个线程执行一个任务。其实也可以创建几个线程,然后重用这些线程执行任务,相关可以学习线程池。

三、线程的属性

线程本身有一些特有的属性,例如:线程标识符、线程名以及线程间的优先级属性等。通过这些属性,可以用来识别一个线程、了解线程的状态、控制线程的优先权等。

3.1、线程标识符

该属性为每一个线程存储了唯一的一个标识符,通过线程标识符,可以对线程进行区分。使用方法getId()。
thread: this.getId()
Runnable: Thread.currentThread().getId()

3.2、线程名

每个线程默认有一个名字,默认的名字采用 Thread-1,Thread-2 ….形式;也可以对创建的线程设置线程名。通过getName()可以获取线程名。

3.3、线程的状态

一个线程从创建、运行到终止称为一个生命周期。线程在其生命周期中要经历创建、就绪、运行、阻塞和终止5中状态。通过getState()获取运行状态。
1、创建 New
当一个 Thread 类或其子类使用 new 关键词声明一个对象实例时,此时线程处于创建的状态。处于创建状态的线程有自己的内存空间,但是线程还没有运行。
2、就绪 Runnable
处于创建状态的线程通过调用start()方法启动后,线程进入就绪状态。这时的线程就已经拥有了运行所需的所有条件,将进入线程队列等待CPU调度。由于还没有获取到CPU的时间片,所以不会立即执行。
3、运行 Running
当处于就绪状态的线程被调度,并获得CPU资源时,便进入运行状态。

  1. 运行状态的线程执行 Run()方法中的操作。
  2. 运行状态线程不是一直占用CPU的,而是根据CPU的调度策略有关。CPU时间片用完后,就会转为就绪状态。
  3. 操作系统的调度策略,会考虑线程的优先级。高优先级的线程获得CPU调度几率要大。
  4. 多于处理器内核数目的线程数,并不能提供执行效率。

4、阻塞 Blocked
在某些情况下,一个正在运行的线程会让出正在使用的CPU资源,进入阻塞状态。

  1. 某些共享资源(打印机或文件资源)被占用。
  2. 等待I/O操作。
  3. 调用了wait、sleep或者 suspend 等方法。
  4. 尝试获得锁,而该锁正在被其他线程使用。

为了提供CPU的利,当一个线程被阻塞时,另一个线程就获得了运行的机会。

5、终止 Terminated
线程到达终止状态可能有几种原因:

  1. 线程的方法 run() 执行结束。
  2. 线程通过某些方法(如 Destory()) 被提前终止。
  3. 在 run() 方法执行中发生了未捕获的异常。

3.4、线程的优先级

在 JAVA 语言中,每个线程都有一个优先级,不同线程可以被赋予不同的优先级。
通过getPriority()方法获取线程优先级,通过setPriority()方法设置线程优先级。
线程优先级共分为10个级别,最低为1,最高为10,默认优先级为5。

3.5、守护线程

前面提到的都是用户线程,另外还有一个是守护线程。两者没有什么太大不同,守护线程的唯一作用是为用户线程提供服务。
通过 setDaemon() 方法,将线程设置为守护线程。

四、线程的管理

当线程启动后,可以管理线程,使线程休眠、等待或中断执行等。

4.1、join() 方法:等待

在某些情况下,一个线程需要等另外一个线程执行结束后,才能执行。这种功能可以使用join()方法实现。
当调用了某个线程类的对象实例的join()方法,则会等待该对象执行结束。

4.2、sleep() 方法

线程的 sleep() 方法使一个线程暂停一段固定的时间,该方法能够把CPU让给优先级比其低的线程。

4.3、yield() 方法:让步

为了防止某个线程独占 CPU 资源,可以让当前执行的线程休息一下,yield()方法可以实现该功能。
yield()方法用于使当前线程让出CPU的使用权,但是这并不能保证CPU接下来调用的不是该线程。

4.4、interrupt() 方法:中断

一个线程除了正常执行结束外,也可以人为地中断线程的执行,线程的中断可以使用interrupt()方法。
除非是一个线程正在尝试中断它本身,否则中断的请求一般都会被接受。如果一个线程由于调用wait()方法或join()方法正处于阻塞队列中,则中断请求不会被响应,并会抛出 InterruptedExcepton 异常。
在程序中调用 interrupt() 方法后,通过需要在线程的 run() 方法中使用类 Thread 的方法 isInterrupted() 进行判断。

4.5、其他方法

用于管理线程的方法,还有stop()、suspend()和resume()等,这里就不说明了。

五、线程分组

如果有若干个正在做同一个工作的线程,为了方便管理,可以对这些线程进行分组,从而把同一组的线程作为一个整体进行操作。
在线程 Thread 的构造方法中,可以指明线程属于哪一个分组:
public Thread(ThreadGroup group,Runnable target)
其中,参数group 可以指明该线程属于哪一个线程组。

六、线程数目的确定

在并发编程过程中,有一个疑问:在程序中创建多少个线程最合适?创建多少个线程才能使程序性能运行最佳?
线程数目的多少对程序性能有一定的影响。
数目少了,处理器会处于闲置状态,浪费资源。
数目多了,多余的线程不能马上执行,浪费了软件资源。
在 JAVA 中提供了相关的方法,可用于获取处理器可以同时处理的最大线程数:
int nthreads = Runtime.getRuntime().availableProcessors()

七、线程本地化

类 ThreadLocal 是一个非常有用的类。 JAVA 通过类 ThreadLocal 实现线程本地对象,使用类 ThreadLocal 将会使变量在每个线程的私有区域内有一个副本,每个线程都可以相对独立的改变自己的副本,而不会影响到其他线程的副本。值得说明的是,ThreadLocal 并不是一个线程,而是表示线程的一个局部变量。
ThreadLocal 类提供了方法 set()get() 用于设置和读取线程的本地值。一个线程首次获取一个线程本地对象值的时候将调用 initialValue() 方法,该方法用于对每个线程对象进行初始化。

八、带返回值的线程

无论是从 Thread 类继承还是实现 Runnable 接口来创建线程,方法 run() 是没有返回值的。带返回值的线程可以通过 Callable 来定义,通过 Future 接口来获得线程的返回值。

8.1、Callable 接口

接口 Callable 和 Runnable 类似,其定义的一般形式:

public interface Callable<V> {
  V call() throws Exception;
}

其中,类型参数 V 指明了线程返回值的类型。Callable<Integer> 将返回一个 Integer 型的值。
使用接口 Callable 创建的线程必须实现 call() 方法,call() 方法的返回值类型由 V 来指定。

8.2、Future 接口

Future 接口允许在未来某个时间获得线程运行的结果,它保存了使用 Callable 接口定义的线程异步运行的结果。当启动一个 Future 对象后,相当于启动了一个计算, Future 对象的计算结果在计算好后得到。

Future 接口的常用方法:

  1. V get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  2. V get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,则抛出 TimeOutException 异常。
  3. boolean cancel(boolean mayInterrupt) 尝试取消任务的运行。
  4. boolean isCancelled 判断是否被取消。
  5. boolean isDone 判断任务是否已经完成。

FutureTask 包装器同时实现了接口 Runnable 和 Callable,它可以很方便地对 Callable 对象进行封装并转换成 Future 对象。

public FutureTask(Callable<V> callable) {}
public FutureTask(Runnable runnable, V result) {}

8.3、Callable 和 Runnable 比较

  1. Callable 是有返回值的,而 Runnable 没有返回值。
  2. Callable 执行入口方法是 call() 方法,Runnable 的执行入口方法是 run()。
  3. call() 方法可以抛出异常,run() 方法不可以。
  4. 运行 Callable 相当于启动了一个异步计算,将来在通过 Future 得到计算的结果;而且可以使用 Future 的 cancel() 方法取消方法的执行。