使用 Runnable、 Callable 创建工作线程,并使用 ExecutorService 并发执行任务。确定死锁、饥饿、活锁和竞争条件中的潜在线程处理问题。
线程
简单地理解,并发意味着同时并行地做事情。
Java中并发是通过线程来完成的
线程是可以同时执行的代码单元。它们有时被称为轻量级进程,但实际上,线程是在进程中执行的(每个进程至少有一个线程,即主线程)。
创建线程
两种方法创建线程
Runable(推荐)
其为函数式接口可以使用lambda
- 1.实现 java.lang.Runnable 接口
class RunnableTask implements Runnable {
public void run() {
System.out.println("Running");
}
}
- 2.将一个实例传递给java.lang.Thread
的构造函数。
线程类并请求线程启动(它可能不会立即启动)。此外,执行线程的顺序和时间并不保证)
Thread thread = new Thread(new RunnableTask());
thread.start();
或者通过匿名内部类
Thread thread = new Thread(new RunnableTask() {
public void run() {
System.out.println("Running");
}
});
thread.start();
或者lamdba
Thread thread = new Thread(() -> {
System.out.println("Running");
});
thread.start();
Thread(不推荐)
- 1.重写run方法
class ThreadTask extends Thread {
public void run() {
System.out.println("Running");
}
}
- 2.创建实例调用start方法
Thread thread = new ThreadTask();
thread.start();
实现 Runnable是推荐的方法
因为有了新的并发 API,不必再直接创建 Thread 对象(实现接口也是推荐的面向对象的方法)。
ExecutorService
Java5引入了一个高级的并发 API,其中大部分是在 Java.util.concurrent 包中实现的
- Executor 接口,它提供了启动和管理线程的替代(更好的)方法
说到这推荐一本书,这本书的作者就是JUC包的作者,可想而知含金量
Executor接口
只有execute()方法,
- 执行线程
Runnable r = ...
Executor e = ...
e.execute(r);
用于替换
Runnable r = ...
Thread t = new Thread(r);
t.start();
ExecutorService接口
继承Executor,以提供更多的特性,比如 submit ()方法,它接受 Runnable 和 Callable 对象,并允许它们返回值
ScheduledExecutorService接口
继承ExecutorService,以重复的间隔或特定的延迟执行任务。
线程池
Executors使用线程池。这些线程与使用 Thread 类创建的线程不同。
当创建工作线程时,它们只是站在空闲位置,等待工作。当工作到达时,执行程序将它分配给线程池中的空闲线程。
线程是通用的,它们独立于它们所执行的 Runnable 任务(与使用 Thread 类创建的传统线程相反)。
线程池的一种类型是固定线程池,它有固定数量的线程在运行。如果一个线程在使用期间终止,它将自动被一个新线程替换。还有可扩展的线程池。
Executors
一般加s的都是工具类,且提供的大多为static方法,供调度使用。例如Collections、Paths、Executors
ExecutorService比 Executor 具有更多的功能。因为它们是接口,创建 Executor 的实例,必须使用java.util.concurrent.Executors提供的方法
Executors创建ExecutorService的静态static方法
- 创建一个使用单个工作线程的 Executor
static ExecutorService newSingleThreadExecutor()
- 创建一个线程池,该线程池重用固定数量的线程
static ExecutorService
newFixedThreadPool(int nThreads)
- 创建一个线程池,可以调度任务
static ScheduledExecutorService
newScheduledThreadPool(int corePoolSize)
有一个线程的ExecutorService
while (true) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable r = () -> {
IntStream.rangeClosed(1, 4)
.forEach(System.out::println);
};
System.out.println("before executing");
executor.execute(r);
System.out.println("after executing");
executor.shutdown();
}
关闭线程
使用 ExecutorService 之后,必须关闭它以终止线程并关闭资源。否则会OOM,线程池的数量是有最大值的和具体的JVM参数和机器内存等有关
- 关闭方法有2种
void shutdown()
Shutdown ()方法告诉执行程序停止接受新任务,但是允许以前的任务继续执行,直到完成。
在此期间,isTerminated ()方法将返回 false,直到所有任务完成,而 isShutdown ()方法将始终返回 true。
2.
List<Runnable> shutdownNow()
shutdownNow ()方法也会告诉执行者停止接受新任务,但是它会尝试立即停止所有执行任务(通过中断线程,但是如果线程没有响应中断,它可能永远不会终止) ,并返回一个从未启动的任务列表。
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable r = () -> {
try {
Thread.sleep(5_000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
executor.execute(r);
executor.shutdownNow();
Interrupted被打印出来,程序将立即终止
Executors返回Future对象
Executors类的静态static内部类DelegatedExecutorService有返回Future对象的方法
- 执行 Runnable 并返回表示该任务的 Future
Future<?> submit(Runnable task)
- 执行一个 Callable 并返回一个表示任务结果的 Future。
<T> Future<T> submit(Callable<T> task)
- 执行给定的任务,返回已完成的任务的结果,如果有异常,则不抛出异常。其他的任务被取消
<T> T invokeAny(
Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException
- 执行给定的任务,返回一个Future的List,其中包含所有完成时的状态和结果(正常或异常)
<T> List<Future<T>> invokeAll(
Collection<? extends Callable<T>> tasks)
throws InterruptedException
Future
Future方法
- 试图取消任务的执行。如果参数为 true,则线程被中断。否则,允许完成任务。
boolean cancel(boolean mayInterruptIfRunning)
- 等待任务完成(无限期) ,然后检索其结果。
V get() throws InterruptedException, ExecutionException
- 最多等待给定的时间,然后检索结果(如果达到时间而结果没有准备好,则抛出 TimeoutException)。
V get(long timeout, TimeUnit unit)
throws InterruptedException,
ExecutionException,
TimeoutException
- 如果此任务在正常完成之前被取消,则返回 true。
boolean isCancelled()
- 如果任务已完成,返回 true
boolean isDone()
Callable接口
也是一个函数式接口,只有一个抽象方法。
与Runnable 的区别在于 Callable 可以返回一个值并抛出一个Exception
V call() throws Exception;
- Runnable不返回结果
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable r = () -> {
IntStream.range(1, 1_000_000).forEach(System.out::println);
};
Future<?> future = executor.submit(r);
try {
// Blocks until the Runnable has finished
future.get();
} catch (InterruptedException
| ExecutionException e) { /** ... */ }
- Callable返回结果
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Long> c = () ->
LongStream.rangeClosed(1, 1_000_000).sum();
Future<Long> future = executor.submit(c);
try {
// Blocks 1 second until the Callable has finished
Long result = future.get(1, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException
| TimeoutException e) { /** ... */ }
- 示例
List<Callable<String>> callables = new ArrayList<>();
callables.add(() -> "Callable 1");
callables.add(() -> "Callable 2");
callables.add(() -> "Callable 3");
ExecutorService executor = Executors.newFixedThreadPool(3);
try {
String result = executor.invokeAny(callables);
System.out.println(result);
} catch(InterruptedException | ExecutionException e) {/** ... */}
invokeAny ()执行给定的任务,返回成功完成任务的结果。你不能保证你会得到哪些 Callable 的结果,只能得到其中一个结果:
或者
invokeAll ()执行给定的任务,返回一个 Future 对象列表,该列表将保存状态和结果,直到所有任务完成。Isdone ()对返回的列表中的每个元素返回 true:
ExecutorService executor = Executors.newFixedThreadPool(3);
try {
List<Future<String>> futures = executor.invokeAll(callables);
for(Future<String> f : futures){
System.out.format("%s - %s%n", f.get(), f.isDone());
}
} catch(InterruptedException | ExecutionException e) { /** ... */ }
线程问题
- 问题
一个应用程序有两个或多个线程,线程并行执行的方式使得它们有时不得不竞争访问资源,而在其他时候,一个线程的操作会对另一个线程的操作产生副作用(比如修改或删除一些共享值) - 锁的解决方案
一个解决方案是锁定的概念,其中资源或代码块之类的东西被某种机制锁定,一次只有一个线程可以使用或访问它。
锁的问题
使用不当锁资源可能会导致以下三个问题
死锁
两个或多个线程永远被阻塞,等待彼此获取/释放某些资源。
饥饿锁
一个线程不断地等待一个锁,而且永远不能使用它,因为其他拥有更高优先级的线程不断地获取它。
VIP客户插队
活锁
两个(或更多)线程相互阻塞,但在活锁中,每个线程都试图自己解决问题,而不是等待(死)。他们没有受到阻碍,但他们无法取得进一步的进展。
互相让资源
锁竞争
两个线程同时竞争以导致意外结果(通常是无效数据)的方式访问或修改同一资源的情况。
解决
1.不修改变量(为final),例如lambda和内部类因为堆栈存储可见性问题,导致局部变量使用时必须定义为fianl防止改变
2.原子地执行读写操作(在单个步骤中一起执行)。
3.是确保发生问题的部分一次只由一个线程正确执行
总结
- 在底层通过两种方式创建线程,一种是实现Runnable,另一种是继承thread并重写run()方法。
- 框架层面使用Executors,执行器使用线程池,线程池又使用工作线程。
- 一种类型的线程池是固定线程池,它有固定数量的线程在运行。也可以使用单线程池。
- ExecutorService具有执行线程池的方法,这些线程池接受可运行或可调用的任务。Callable返回一个结果并抛出一个Exception。
- submit()方法返回表示任务结果的Future对象(如果任务是可运行的,则返回null)。
- 使用shutdown()(优雅地)或shutdownNow()(强制地)关闭池线程
- 当两个或多个线程被永远阻塞,等待彼此获取/释放某些资源时,就会出现死锁情况。
- 当一个线程一直在等待一个锁,却因为其他优先级更高的线程一直在获取它而永远无法获取它时,就会发生饥饿锁。
- 在两个(或更多)线程相互阻塞的意义上,活锁类似于死锁,但是在活锁中中,每个线程都试图自己(活动)解决问题,而不是仅仅等待(死)。
- 争用条件是指两个线程同时竞争访问或修改同一资源,从而导致意外结果的情况。
Comments | 0 条评论