当前位置 博文首页 > RtxTitanV的博客:Java并发总结之基础
本文主要对Java并发基础知识进行简单总结。
解释一:并发是指两个或多个事件在同一时间间隔发生。
解释二:同一时刻只能有一个任务在一个CPU核心上执行,但多个任务在一个时间段内被快速地轮流交替执行,从宏观角度看具有多个进程同时执行的效果,但从微观角度看并不是同时执行的。
解释一:并行是指两个或者多个事件在同一时刻发生。
解释二:同一时刻有多个任务在多个CPU核心上同时执行。无论从微观还是从宏观角度来看,二者都是一起执行的。
多个任务由一个线程按顺序执行,不存在线程不安全问题。
进程是资源分配的基本单位,是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。进程控制块(Process Control Block,PCB)描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。
线程是独立调度的基本单位。一个进程中可以有多个线程,它们共享进程资源。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
多线程:指一个应用程序有多条执行路径,即在一个应用程序中可以同时运行多个不同的线程来执行不同的任务。
优势:提高CPU的利用率。
劣势:
一个 Java程序的运行是主(main)线程和多个其他线程同时运行。JVM的启动是多线程的。
先从总体上来说:
再深入到计算机底层来探讨:
注意:
main
方法所在的主(main)线程就是一个用户线程,不能设置成守护线程,main
方法启动的同时在JVM内部同时还启动了很多守护线程,比如垃圾回收线程。main线程结束,其他线程一样可以正常运行;main线程结束,其他线程也可以立即结束,当且仅当这些子线程都是守护进程。- 所有用户线程结束,JVM退出,不管这个时候有没有守护线程运行。而守护线程不会影响JVM的退出。
setDaemon()
方法可以将一个线程设置为守护线程,setDaemon(true)
必须在start()
方法前调用,否则会抛出IllegalThreadStateException
异常。- 在守护线程中产生的新线程也是守护线程。
- 不是所有的任务都可以分配给守护线程来执行。
- 守护线程中不能靠
finally
代码块的内容来确保执行关闭或清理资源的逻辑。因为一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,finally
代码块可能无法被执行。
多线程编程中一般线程的个数都大于CPU核心数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的CPU时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux相比与其他操作系统(包括其他类Unix系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
定义一个类继承Thread
类,重写run()
方法。创建自定义的线程类对象调用start()
方法启动线程,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的run()
方法。
public class MyThread extends Thread {
@Override
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
定义Runnable
接口的非抽象实现类,重写run()
方法。创建Runnable
接口实现类对象,以该对象作为创建线程的参数来创建线程,调用start()
方法启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
定义Callable
接口的非抽象实现类,重写call()
方法,与Runnable
相比,Callable
可以有返回值,返回值通过FutureTask
进行封装,以Callable
接口实现类对象为参数创建FutureTask
对象,再以FutureTask
对象为参数创建线程,调用start()
方法启动线程。
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
return 1;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
实现
Runnable
和Callable
接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过Thread来调用。
任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。
java.util.concurrent
提供了一种灵活的线程池实现作为Executor框架的一部分。在Java类库中,任务执行的主要抽象不是Thread,而是Executor。
Executor是一个简单的接口(是线程池的顶级接口但并不是一个线程池),但它却为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。该框架提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。通过使用Executor,可以实现各种调优、管理、监视、记录日志、错误报告和其他功能。Executor基于生产者一消费者模式,提交任务相当于生产者,执行任务的线程则相当于消费者。
调用Executors
中的静态工厂方法来创建线程池,返回的线程池都实现了ExecutorService
接口。主要有newFixedThreadPool
、newCachedThreadPool
、newSingleThreadExecutor
、newScheduledThreadPool
。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
executorService.shutdown();
}
Runnable
接口的run()
方法无返回值,Callable
接口的call()
方法有返回值,是个泛型,和Future
、FutureTask
配合可以用来获取异步执行的结果。Runnable
接口的run()
方法只能抛出运行时异常,且无法捕获处理,Callable
接口的call()
方法允许抛出异常,可以获取异常信息。start()
方法用于启动线程,run()
方法用于执行线程的运行时代码。start()
只能调用一次,run()
可以重复调用。start()
方法启动线程后先处于就绪状态,获取CPU时间片后进入运行状态,通过调用该线程的run()
方法以完成其运行状态,run()
方法运行结束, 此线程终止,然后再调度其它线程,真正实现了多线程运行。run()
方法是在本线程里的只是一个普通方法,如果直接调用就是在当前线程中调用一个普通方法而已,执行路径还是只有一条,没有多线程的特征,所以在多线程执行时需要调用start()
方法而不是直接调用run()
方法。新建一个线程后线程进入了新建状态,调用start()
方法会启动线程并使线程进入就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行run()
方法的内容,这是真正的多线程工作。而直接执行run()
方法,会把run()
方法当成一个main
线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。一句话总结,调用start()
方法方可启动线程并使线程进入就绪状态,而run()
方法只是Thread
的一个普通方法调用,还是在主线程里执行。
call
)将返回一个值并可能抛出一个异常,其中这个返回值可以被Future拿到。在Executor中包含了一些辅助方法能将其他类型的任务封装为一个Callable。FutureTask
里面可以传入一个Callable
的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成get
方法将会阻塞。一个FutureTask
对象可以对调用了Callable
和Runnable
的对象进行包装,由于FutureTask
也是Runnable
接口的实现类,所以FutureTask
也可以放入线程池中。线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
yield
方法让出了对CPU的使用权。sleep
方法使线程进入睡眠状态。线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
在Java中,每一个线程有一个优先级,新线程的优先级最初设置为等于创建线程的优先级,可以用setPriority
方法更改线程的优先级,优先级的范围在MIN_PRIORITY
(1)与MAX_PRIORITY
(10)之间,默认值为NORM_PRIORITY
(5)。默认情况下,一个线程继承它的父线程的优先级。线程调度器选择新线程时首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。
·注意:如果确定要使用优先级,应该避免以下错误。如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程完全饿死。
优先级相关方法如下:
java.lang.Thread
:
public final void setPriority(int newPriority)
:更改线程的优先级。如果优先级不在MIN_PRIORITY
到MAX_PRIORITY
范围内,会抛出IllegalArgumentException
异常。如果当前线程不能修改此线程,会抛出SecurityException
异常。public final int getPriority()
:返回此线程的优先级。java.lang.ThreadGroup
:
public final void setMaxPriority(int pri)
:设置线程组的最大优先级。线程组中具有较高优先级的线程不受影响。如果参数pri
在MIN_PRIORITY
到MAX_PRIORITY
范围外,组的最大优先级保持不变。如果当前线程不能修改此线程组,会抛出SecurityException
异常。public final int getMaxPriority()
:返回此线程组的最大优先级。作为此组的一部分的线程的优先级不能高于最大优先级。java.lang.Thread
:
public static void sleep(long millis)
:
millis
值为负数,会抛出IllegalArgumentException
异常。InterruptedException
异常(sleep
方法抛出)。java.lang.Thread
:
public static void yield()
:
java.lang.Thread
:
public final void setDaemon(boolean on)
:
on
设置为true则将此线程标记为守护线程,设置为false则标记为用户线程。IllegalThreadstateException
异常。SecurityException
异常。public final boolean isDaemon()
:判断该线程是否是守护线程,返回true表示是守护线程,false表示不是。java.lang.Thread
:
public void interrupt()
:
Object
类的wait()
、wait(long)
或wait(long, int)
方法,或者sleep(long)
、sleep(long, int)
、join()
、join(long)
、join(long, int)
这类方法,则它的中断状态将被清除并会收到一个InterruptedException
异常,从而提前结束该线程,但是不能中断I/O阻塞和synchronized锁阻塞。public static boolean interrupted()
:
public boolean isInterrupted()
:
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(就是线程中断后会抛出
InterruptedException
的方法)就是在监视线程的中断状态,一旦线程被中断,则该线程的中断状态就被置为true,然后该线程的中断状态将被清除并且这些方法会抛出中断异常。
java.util.concurrent.ExecutorService
接口:
void shutdown()
:
List<Runnable> shutdownNow()
:
execute
方法抛出一个未检查的RejectedExecutionException
。注意:试图终止线程的方法是通过调用
Thread.interrupt()
方法来实现的,这种方法的作用有限,如果线程中没有sleep
、wait
、Condition
、定时锁等应用,interrupt()
方法无法中断当前线程。所以shutdownNow()
并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。但是大多数时候都能立即退出。
boolean isShutdown()
:判断该executor是否已被关闭(shutdown),true表示已关闭,false表示未关闭。
boolean isTerminated()
:如果所有提交的任务在shutdown后都完成,则返回true,否则返回false。注意,除非先调用shutdown
或shutdownNow
,否则isTerminated
永远不会为true。
boolean awaitTermination(long timeout, TimeUnit unit)
:
timeout
和TimeUnit
用于设定超时时间及单位。shutdown
方法组合使用。InterruptedException
异常。java.lang.Thread
:
public final void join()
:
InterruptedException
异常(join
方法抛出)。public final void join(long millis)
:
millis
毫秒。millis
为0则意味着永久等待,此时等价于join()
。millis
值为负数,会抛出IllegalArgumentException
异常。InterruptedException
异常(join
方法抛出)。java.lang.Object
:
public final void wait()
:
notify()
或notifyAll()
方法唤醒,等同于执行wait(0)
。notify()
或notifyAll()
方法来通知等待该对象锁的线程,直到该线程重新获取对象锁才能进入就绪状态。IllegalMonitorstateException
异常。InterruptedException
异常(wait
方法抛出)。public final void wait(long timeout)
:
notify()
或notifyAll()
方法唤醒或者超过指定超时时间。