第二章、Java多线程入门类和接口
-
-
继承
Thread
类,并重写run方法; -
实现
Runnable
接口的run方法;
-
1.1、继承Thread类
public class Ex_Thread extends Thread{ @Override public void run() { System.out.println("Ex_Thread = " ); } public static void main(String[] args) { Ex_Thread ex_thread = new Ex_Thread(); ex_thread.start(); } }
1.2、实现Runnable接口
@FunctionalInterface public interface Runnable { public abstract void run(); }
可以看到Runnable
是一个函数式接口,这意味着我们可以使用Java 8的函数式编程来简化代码。
示例代码:
public class Impl_Runnable implements Runnable { @Override public void run() { System.out.println("Impl_Runnable"); } public static void main(String[] args) { new Thread(new Impl_Runnable()).start(); new Thread(()->{ System.out.println("Java 8 匿名内部类"); }).start(); } }
1.3、Thread类构造方法
- Runnable : 指定要执行的任务;
- ThreadGroup : 线程组,指定这个线程是在哪个线程组下
- String: 线程的名字,多个线程的名字是可以重复的。如果不指定名字
//我们大多是直接调用下面两个构造方法: Thread(Runnable target) Thread(Runnable target, String name)
1.4、Thread类的几个常用方法
这里介绍一下Thread类的几个常用的方法:currentThread():静态方法,返回对当前正在执行的线程对象的引用;
-
-
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
-
yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
Java线程中的Thread.yield( )方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。 yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保 证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行! 举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,有可能是其他人先上车了,也有可能是Yield先上车了。 但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已, 最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。
-
sleep():静态方法,使当前线程睡眠一段时间;
-
join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait()方法实现的;
从join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞。调用wait方法必须要获取锁,所以join方法是被synchronized修饰的,synchronized修饰在方法层面相当于synchronized(this),this就是Thread本身的实例。join阻塞的是主线程 - wait():
- wait() 是针对已经获取对象锁的线程进行操作。
- 当线程获取对象锁后,调用 wait() 主动释放对象锁,同时该线程休眠
- 直到其他线程调用 notify() 唤醒该线程,才继续获取对象锁,并执行
- 调用 notify() 唤醒线程并不是实时的,而是等相应的 synchronized 语句块执行结束,自动释放对象锁
- 再由 JVM 选取休眠的线程赋予对象锁,并执行,从而实现线程间同步、唤醒的
-
实现一个自定义的线程类,可以有继承Thread
类或者实现Runnable
接口这两种方式,它们之间有什么优劣呢?
-
-
由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
-
Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
-
Runnable接口出现,降低了线程对象和线程任务的耦合性。
-
如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。
-
所以,我们通常优先使用“实现Runnable
接口”这种方式来自定义线程类。
通常来说,我们使用Runnable
和Thread
来创建一个新的线程。但是它们有一个弊端,就是run
方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
JDK提供了Callable
接口与Future
接口完成后有一个返回值为我们解决这个问题,这也是所谓的“异步”模型。
2.1、Callable接口
Callable
与Runnable
类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable
提供的方法是有返回值的,而且支持泛型。
@FunctionalInterface public interface Callable<V> { V call() throws Exception; }
public class Impl_Callable implements Callable<String> { @Override public String call() throws Exception { // 模拟计算需要一秒 Thread.sleep(1000); return "Impl_Callable"; } public static void main(String[] args) throws Exception{ ExecutorService executor= Executors.newCachedThreadPool(); Impl_Callable impl_callable = new Impl_Callable(); // 注意调用get方法会阻塞当前线程,直到得到结果。 // 所以实际编码中建议使用可以设置超时时间的重载get方法。 Future<String> result = executor.submit(impl_callable); System.out.println(result.get()); } }
2.2、Future接口
Future
接口只有几个比较简单的方法:
public abstract interface Future<V> { public abstract boolean cancel(boolean paramBoolean); public abstract boolean isCancelled(); public abstract boolean isDone(); public abstract V get() throws InterruptedException, ExecutionException; public abstract V get(long paramLong, TimeUnit paramTimeUnit) throws InterruptedException, ExecutionException, TimeoutException; }
cancel
法是试图取消一个线程的执行。
注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean
类型的返回值是“是否取消成功”的意思。参数paramBoolean
表示是否采用中断的方式取消线程执行。
所以有时候,为了让任务有能够取消的功能,就使用Callable
来代替Runnable
。如果为了可取消性而使用 Future
但又不提供可用的结果,则可以声明 Future
形式类型、并返回 null
作为底层任务的结果。
2.3、FutureTask类
上面介绍了Future
接口。这个接口有一个实现类叫FutureTask
。FutureTask
是实现的RunnableFuture
接口的,而RunnableFuture
接口同时继承了Runnable
接口和Future
接口:
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
public class FutrueTask_Demo implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "FutrueTask_Demo"; } public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newCachedThreadPool(); FutureTask<String> task = new FutureTask<>(new FutrueTask_Demo() ); executor.submit(task); System.out.println(task.get()); } }
使用上与第一个Demo有一点小的区别。首先,调用submit
方法是没有返回值的。这里实际上是调用的submit(Runnable task)
方法,而上面的Demo,调用的是submit(Callable task)
方法。
然后,这里是使用FutureTask
直接取get
取值,而上面的Demo是通过submit
方法返回的Future
去取值。
在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。这块有兴趣的同学可以参看FutureTask源码。
/** * * state可能的状态转变路径如下: * NEW -> COMPLETING -> NORMAL * NEW -> COMPLETING -> EXCEPTIONAL * NEW -> CANCELLED * NEW -> INTERRUPTING -> INTERRUPTED */ private volatile int state; private static final int NEW = 0; private static final int COMPLETING = 1; private static final int NORMAL = 2; private static final int EXCEPTIONAL = 3; private static final int CANCELLED = 4; private static final int INTERRUPTING = 5; private static final int INTERRUPTED = 6;
state表示任务的运行状态,初始状态为NEW。运行状态只会在set、setException、cancel方法中终止。COMPLETING、INTERRUPTING是任务完成后的瞬时状态。
以上就是Java多线程几个基本的类和接口的介绍。可以打开JDK看看源码,体会这几个类的设计思路和用途吧!