Java多线程与线程池

时间:2021-09-08
本文章向大家介绍Java多线程与线程池,主要包括Java多线程与线程池使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

关于并发,不要忘了Java中有个十分强大的AQS框架

1.概念

1.1 并发 & 并行 & 串行

并发:宏观上多个任务同时执行,实际上同一时刻只有一个线程在执行。
并行:微观上多个任务同时执行,即同一时刻多个任务同时执行。
串行:单线程

1.2 并发的作用

并发通常是用于提高运行在单处理器上的程序的性能。乍看不对,多线程涉及轮转使用CPU增加了上下文切换时间,应该更慢。但是我们还需要考虑程序中线程阻塞。单线程程序线程阻塞会导致整个程序阻塞,但是多线程可以应对这种情况。也就是说,如果程序中没有任务会阻塞,多线程将毫无意义。

1.3 并发的多面性

优:
让程序执行更快(总体来看),更充分地利用CPU,提高系统性能。
劣:
带来线程安全问题,增加了系统风险,编程难度也提高了。所以在使用并发时,默认线程不安全,时刻提醒自己。

1.4 线程基本实现机制

底层原理是切分CPU时间,为线程分配CPU时间切片,获得CPU时间切片才可使用CPU。

2.创建线程

2.1 定义线程任务Runnable

实现Runnable接口是自定义功能简单的线程的常用方式。

public class RunnableThread implements Runnable {
    @Override
    public void run() {
    // ...
    }
}

光是定义一个Runnable接口的实现类是无法实现线程行为的,它就相当于一个普通类,必须要将这个任务显示地附着到线程上

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new RunnableThread());
        t.start();
    }
}

2.2 Thread类

如果想用Thread的方法,可以继承Thread创建任务。

public class MyThread extends Thread {
    @Override
    public void run() {
    // ...
    }
}

另外还有之中匿名方式,也是lambda。

public class Main {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("是只猪呀");
        }).start();
    }
}

2.3 Callable

还可以实现Callable接口,用得比较少,知道有就行。

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

2.4 线程池创建线程

关于线程池,必须要先理清几个接口和类的关系,文章后面会介绍使用。

  • Executor接口:执行提交的Runnable任务的对象
  • ExecutorService接口:提供了更多管理方法API。ExecutorService extends Executor
  • Executors类:定义了ExecutorServiceExecutorScheduledExecutorServiceThreadFactoryCallable类的工厂和方法。里面包含了大量的public static ExecutorService方法,用于创建线程池,而这些方法中部分又使用new ThreadPoolExecutor()来创建线程池。
  • ThreadPoolExecutor:阿里Java开发手册推荐使用的创建线程池的工具。

3.线程的生命周期

  • NEW:新建
  • RUNNABLE:运行
  • BLOCKED:阻塞
  • WAITING:等待
  • TIMED_WAITING:计时等待
  • TERMINATED:已终止
// 新建状态
Thread t = new MyThread();

// 运行状态
t.start();

// 阻塞状态
线程 t 需要 synchronized(obj),但是已经被别的线程持有,一直去尝试给这个对象加锁

// 等待状态
t.wait();
t.join();  // 等待别的线程结束
LockSupport.park();

// 计时等待 给定了时长的
t.sleep(1000);
t.wait(1000);

// 终止
线程任务执行完成

如果线程需要执行一个长时间任务,可能需要能中断线程

// 中断线程
t.interrupt();

// 检查线程是否中断 都调用native方法
t.isInterrupted();

4.守护线程

说到守护线程首先想到JVM的垃圾收集器。守护线程是为用户线程服务的,所有用户线程结束后,整个程序就结束了,而不会关心守护线程状态。守护线程是低优先级线程,任何守护线程都是为所有的用户线程服务,而不是某些用户线程。可以t.setDaemon(true);t.start();这样设置,要在start()之前。

5.死锁

死锁表现为:多个线程都持有部分资源,而又在相互尝试获取其它线程持有的资源的状态。

形成锁的四个必要条件:

  • 互斥:两个线程执行都需要相同的资源
  • 不可剥夺:线程持有的资源不能被其线程剥夺,只能自己释放
  • 请求与保持:持有部分资源,同时请求另一部分必要资源
  • 循环等待:如两个线程都在等待对方释放资源

死锁的解决办法,破坏四个条件中的任意一个

  • 互斥:是必然的,否则没有加锁的必要。
  • 不可剥夺:将资源定义成原子的,一次性获取所有需要的资源
  • 请求与保持:申请不到资源可以主动释放已占有的资源
  • 循环等待:按顺序申请资源,逆序释放

6.线程池的使用

6.1 Executors.newFixedThreadPool

创建固定大小线程池

// 5个线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);

// 提交任务
executor.submit(new RunnableTask());

// 关闭线程池
executor.shutdown(); 

// 立即关闭 不会等待线程执行结束
executor.shutdownNow();

// 指定延时后关闭 需要捕获InterruptedException
executor.awaitTermination(60, TimeUnit.SECONDS);

6.2 Executors.newCachedThreadPool

根据任务数量动态调整线程池的大小

6.3 ScheduledThreadPool

定时任务

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

// 10秒后执行任务 且只执行一次
ses.schedule(new RunnableTask(), 10, TimeUnit.SECONDS);

// 延迟10秒后开始执行任务 任务每5秒内执行一次
ses.scheduleAtFixedRate(new RunnableTask(), 10, 5, TimeUnit.SECONDS);

// 延迟10秒后开始执行任务 任务每隔5秒执行一次(这个是不包含执行任务的时间的,如任务执行需要2秒,那就是每7秒执行一次)
ses.scheduleWithFixedDelay(new RunnableTask(), 10, 5, TimeUnit.SECONDS);

6.4 ThreadPoolExecutor

Executors的方法也使用了ThreadPoolExecutor来创建线程池。使用ThreadPoolExecutor能够让我们更方便控制其核心参数。关于ThreadPoolExecutor可以参考另一篇博客

原文地址:https://www.cnblogs.com/farwalking/p/15242258.html