Java线程创建与使用
# Java线程创建与使用
Java中创建线程的方式有以下四种:
- 继承Thread类
- 实现Runnable接口
- 使用Callable接口和FutureTask
- 使用线程池
关于源码部分,版本为JDK 1.8
# 继承Thread类
集成Thread类,重写run方法,执行没有返回值(void)。
public class TheadDemo{
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 调用start()方法开启线程
myThread.start();
}
}
/**
* 继承Thread类,重写run方法
*/
class MyThread extends Thread{
public void run(){
for(int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName() + " 线程跑起来了");
}
}
}
# start源码
/**
* 调用这个方法线程会开始执行,JVM会负责调用线程的run方法
* 两个线程同时运行:当前线程(它从对start方法的调用中返回)和另一个线程(它执行它的run方法)。
* 多次启动一个线程是不合法的。特别是,线程在完成执行后可能不会重新启动。
*/
public synchronized void start() {
// ......
start0();
// ......
}
private native void start0();
# 实现Runnable接口
实现Runnable接口,实现run方法,接口的实现类的实例作为Thread的target作为参数传入带参的Thread构造函数,通过调用start()方法启动线程。
public class RunnableDemo {
public static void main(String[] args) {
MyRunnale myRunable = new MyRunnale();
// 实现类的实例作为Thread的targe带入构造函数,调用start()方法开启线程
new Thread(myRunable).start();
}
}
/**
* 实现Runnable,实现run方法
*/
class MyRunnale implements Runnable{
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName() + " 线程跑起来了");
}
}
}
# runnable原理
// 1.分配一个新的Thread对象。名称形式为“Thread-”+n,其中n为整数。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 2.用当前的AccessControlContext初始化一个线程。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
// 3.初始化线程,找到target的赋值操作
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ......
// 赋值操作,private Runnable target;
this.target = target;
// ......
}
/**
* 4.真正调用的地方。
* 如果这个线程是使用一个单独的Runnable运行对象构造的,那么这个Runnable对象的run方法就会被调用;
* 否则,此方法不执行任何操作并返回。Thread的子类应该重写这个方法。
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
# 与继承Thread类对比
查看源码我们可以知道
- 继承Thread的原理是由子类重写了Thread类的run(),当调用start()时,JVM会调用线程的run()方法。
- 实现Runnable接口则是通过构造函数传入Runnable的引用,最后赋值给了线程中的target成员变量,最后在run()方法被调用。run()方法内部判断成员变量target的引用是否为空,不为空时执行的就是实现Runable接口时重写的run方法。
两者的优劣:
- 继承Thread可以直接使用Thread类中的方法,代码编写简单。缺点是由于Java只能单继承,如果已经有了父类,就不能用这种方法
- 实现Runnable接口则摆脱了单继承的限制,子类只需要实现接口,并通过Thread构造线程即可。缺点也很明显,需要先获取到线程对象后,才能使用Thread中的方法。
# 使用Callable和FutureTask接口
# Callable接口
JDK 5.0 新增了Callable接口,该接口与Runnable接口非常相似,Callable 接口的主要特征如下:
- 声明了一个call方法。执行器运行任务时,该方法会被执行器执行。
- 该方法有返回结果,必须返回声明中指定类型的对象。
- 可能抛出异常。call()方法可以抛出任何一种校验异常。可以实现自己的执行器并重载
afterExecute()
方法来处 理这些异常。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable接口与Runnable接口类似,两者都是为其实例可能由另一个线程执行的类设计的。然而,Runnable接口的run方法不返回结果,也不能抛出检查异常。此外,Executors类包含将其他常见形式转换为Callable类的实用方法。
# Future接口
Future接口是JUC下的一个重要接口,Future表示异步计算的结果。提供了一些方法来检查计算是否完成、等待其完成以及检索计算的结果。结果只能在计算完成时使用get方法进行检索,必要时阻塞直到准备就绪。取消由cancel方法执行。提供了其他方法来确定任务是正常完成还是被取消。一旦计算完成,计算就不能取消。
FutureTask是Future接口实现类之一,该类提供了一个Future的基本实现 。可用于包装Callable或Runnable对象,既可以作Runnable被线程执行,又可以作为Futrue得到Callable的返回值
# 实现步骤
- 创建一个实现Callable接口的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建callable接口实现类的对象
- 将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用star方法(也可以使用Executor)
- 获取callable接口中call方法的返回值
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 启动线程,执行callable的业务
new Thread(futureTask).start();
// 同步等待callable的返回值
System.out.println(futureTask.get());
System.out.println("main线程运行结束");
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= 100; i++){
sum +=i;
}
Thread.sleep(5000);
return sum;
}
}
还可以通过Executor,实现执行器并重载afterExecute()
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
1, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10)) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
// 如果在call方法执行过程中有错误,则可以在此处进行处理
System.out.println("任务执行完毕 " + r);
}
};
Future<Integer> future = executor.submit(myCallable);
System.out.println(future.get());
executor.shutdown();
}
# 原理解析
有了Runnable,为什么还要有Callable接口?
我们假设一共有四个程序需要执行,第三个程序时间很长。如果使用Runnable接口实现会按照顺序依次执行,会等第三个程序执行完毕,才去执行第四个。而如果使用Callable接口,我们可以把时间长的第三个程序单独开启一个线程去执行,第1、2、4 线程执行不受影响。比如,主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务,主线程就去做其他的事情,过一会儿才去获取子任务的执行结果
生活中的例子:
- 老师上着课口渴了,去买水不合适。讲课线程继续,老师可以单起个线程找班长帮忙买水,水买回来了放桌上,老师需要的时候再去get。
- 4个同学,A算1+20、B算21+30、C算31*到40、D算41+50,C的计算量有点大,那么可以使用FutureTask单起个线程给C计算,我先汇总ABD结果,最后等C计算完了再汇总C,拿到最终结果
- 高考,会做的先做,不会的放在后面做。
注意事项:
get( )方法建议放在最后一行,防止线程阻塞。获取结果的常见方式
// 1.不见不散,执行到该行,线程开始阻塞,直到获得计算结果 Object o = futureTask.get(); // 2.过时不候,一个可遇见的阻塞时间 Object o2 = futureTask.get(2L, TimeUnit.SECONDS); // 3.使用轮询, 解决阻塞(不推荐,建议使用CompletableFuture) // - 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞,但是轮询的方式会消耗无畏的CPU资源,而且也不见得能及时地得到计算的结果 while(true){ if(futureTask.isDone()){ System.out.println("使用轮询来解决阻塞,值为:"+futureTask.get()); break; }else{ System.out.println("阻塞中....."); } }
一个FutureTask,多个线程调用call( )方法只会调用一次
如果需要调用call方法多次,则需要多个FutureTask
# 使用线程池
通过线程池创建线程
public class ThreadPool {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i = 0; i < 10; i++)
{
RunnableThread thread = new RunnableThread();
executorService.execute(thread);
}
// 关闭线程池
executorService.shutdown();
}
}
class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");
}
}
# Thread常用方法
# 获取和设置Thread信息
方法名称 | 说明 |
---|---|
getName()/setName(String name) | 获取/设置线程的名称 |
currentThread | Thread类的静态方法,返回当前正在执行的线程对象的引用 |
getId() | 返回线程的标识符。该标识符是在钱程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。 |
getState() | 返回线程的状态 |
getPriority()/setPriority() | 获取或设置线程的优先级,线程默认优先级是5(范围是1-10)。 线程优先级高仅仅表示线程获取的CPU时间的几率高,往往需要多次运行的时候才能看到想要的效果。 |
线程两种调度模型
- 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片。
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,则随机选择一个。优先级高的线程获取的CPU时间片相对多一些 (Java使用抢占式调度模型)
# 线程控制
方法名称 | 说明 |
---|---|
sleep(long millis) | Thread类的静态方法,将线程的执行暂停ms时间 (休眠线程,不释放锁) |
join()/join(int millis) | 当前线程暂停,等待指定的线程执行结束后,当前线程再继续 (相当于插队加入),可以设置固定时间 |
yield() | 让出cpu的执行权(礼让线程) |
isDaemon()/setDaemon() | 获取或设置Thread对象的守护条件 |
interrupt() | 中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记 |
interrupted() | 判断目标线程是否被中断,但是将清除线程的中断标记 |
isinterrupted() | 判断目标线程是否被中断,不会清除中断标记 |
setUncaughtExceptionHandler() | 当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。 |
守护线程
守护线程区别于用户线程,是程序在运行时在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程。
守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。