Java中的多线程-06

前言

之前我们已经说完了线程的创建的两种方式,线程的三种同步措施,发生了死锁应该如何应对以及线程之间的通信。基本上多线程中的基本知识都已经提及到了。这里我们讲到谈及的是JDK5之后新增的创建线程的方式,以及线程池的基本知识。前面我们也提到了JDK5中新增加的一个锁——ReentrantLock,可以手动的加锁和释放锁,减少了JVM的调度,提高了效率。

Callable接口

实现Callable接口也可以创建线程,这是JDK5中新增加的。不过我们已经有了一个实现多线程的接口Runnable,那么这两个接口有什么不同的地方呢?看一下官方文档对这个接口的说明。

@FunctionalInterface
public interface Callable<V>
  • 返回结果并可能引发异常的任务。实现者定义一个没有参数的单一方法,称为call
  • Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而, Runnable不返回结果,也不能抛出被检查的异常。
  • Executors类包含的实用方法,从其他普通形式转换为Callable类。
V call() 
// 计算一个结果,如果不能这样做,就会抛出一个异常。  

之前我们说过Runnable中的run方法是void run(),没有返回值并且不可以抛出异常。所以说我们实现这个方法的时候,也不可以用返回值,也不可以抛出异常。(子类重写父类的函数不给抛出范围更大的异常)

相比与Runnable来说,Callable的更能更加的强大。

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

那么这个FutureTask又是个什么鬼?

FutureTase实现了两个接口,一个是Future接口,一个是Callable接口,其中Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

public class Demo9 {
    public static void main(String[] args) {
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        Thread thread = new Thread(futureTask);
        thread.start();

        System.out.println("Main Thread");
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("Main End");
    }
}

class Task implements Callable<Integer> {

    @Override
    public Integer call() throws InterruptedException {
        int i = 1;
        int sum = 0;
        Thread.sleep(5000);
        while (i <= 100) {
            sum += i++;
        }

        return sum;
    }
}

上面就是这个类的具体的用法,其实背后的原理是非常的复杂的,但是这里我就只是做一个简单的了解。不可能深入的探究到底这个FutureTast做了什么事情。

可以看到使用实现Callable接口创建多线程的时候,首先我们需要创建Callable的实现对象。然后将其丢入FutureTask的构造函数之中,从而得到了一个FutureTask对象。然后还是像使用Runnable接口一样,将其放入到Thread类中,使用start方法进行执行。最后使用futureTask.get()方法可以取得线程计算的结果。不过主线程在进入到futureTask.get()的时候,如果我们创建的线程还没有计算结束,主线程会进入阻塞状态,直到等到线程计算的结果出来得到返回值。当然也可以通过get方法的参数设置等待的最长的时间是多少。

还有方法比较的常用。比如说cancel方法可以取消执行此任务。isDone方法可以返回是否任务已完成。这里就不多加演示了。

使用线程池

上面说到了可以使用Callable加上FutureTask异步的获取线程的返回值。这里我们需要说的是另外的一个创建线程的方式,也是最常用的方式——使用线程池。

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。

使用线程池有以下的好处。

  • 提高了相应的速度(减少了新建线程的时间)
  • 降低了资源的消耗(重复利用线程池中的线程,不需要每次都创建)
  • 便于线程的管理。
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大的线程数。
    • keepAliveTime:线程没有任务时最多保存多长的时间会终止。

那么我们该如何新建一个线程池呢?JDK5中直接给我们提供了现成的API——ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
  • void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

  • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池

  • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池

  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。

比如说之前写的那个生产者和消费者的模式就可以使用线程池来创建线程。

public class Demo10 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Clerk clerk = new Clerk();
        executorService.execute(new Productor(clerk));
        executorService.execute(new Productor(clerk));
        executorService.execute(new Consumer(clerk));
        executorService.execute(new Consumer(clerk));
        executorService.execute(new Consumer(clerk));

        executorService.shutdown();
    }
}

使用线程池将会更加的简洁明了,而且效率只会更加的高。

上面只是使用线程池执行Runnable,也可以使用线程池执行Callable。比如上上面写的那个Callable的例子就可以这么写。

@Test
public void test() {
    ExecutorService executorService = Executors.newCachedThreadPool();
    FutureTask<Integer> task = new FutureTask<>(new Task());
    executorService.submit(task);

    try {
        System.out.println(task.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

其实都是相似的,线程池的内部也是使用Thread的,所谓的线程池就是对多个Thread进行封装罢了。除了上面的代码可行之外,我发现不使用FutureTask好像也是可行的。

@Test
public void test() {
    ExecutorService executorService = Executors.newCachedThreadPool();
//        Future<Integer> submit = executorService.submit(new Task());
//        FutureTask<Integer> task = new FutureTask<>(new Task());
    Future<Integer> submit = executorService.submit(new Task());

    try {
        System.out.println(submit.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

使用submit的方法的返回值就行了。如果是第一种方式,也就是把futrueTask传递个submit的参数的话,返回值类型是Future<?>,下面调用submit.get()是null值。可见如果使用futureTask作为参数的话就只能使用futureTask来获取线程的执行的结果,如果只是我们写的Callable的话,使用submit方法的返回值就行了。

总结

上面的知识其实是非常复杂的,不过说过了,我们只是简单的了解一下,懂基本的使用方式就行了,所以很多细节我们都没哟深究,也没有参考什么很多别人写的很深入的文章和资料。其实多线程的基本知识的学习,到此基本上就结束了。下面准备搞一下Java中的集合的使用,比如看看源码还是其他什么的,不可能是死板的学习API的。


一枚小菜鸡