线程和线程池

2019-01-27  本文已影响5人  蚂蚁绊倒象

线/进程、多线/多进程、线程并发/并行

一、线程、进程概念

二、线程

如何创建线程

创建线程有两种方式:继承Thread类;实现Runnable接口;
1.继承Thread类

public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" 正在执行。。。");
    }
}

调用start()和run()方法的区别

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyThread myThread1 = new MyThread("thread-1");
        myThread1.start();
        MyThread myThread2 = new MyThread("thread-2");
        myThread2.run();
    }

打印结果:调用run方法的先打印

I/System.out: main 正在执行。。。
I/System.out: thread-1 正在执行。。。

说明:

  1. run方法的调用不会创建新的线程,而是在主线程中直接调用run()方法,跟普通的方法调用无区别。
  2. 虽然thread-1的start方法调用在thread-2的run方法前面调用,但是先输出的是thread-2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。

2.实现Runnable接口

public class MyRunnable implements Runnable {

    private String name;

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(name + " 正在执行。。。");
    }
}
    //调用时
    new Thread(new DownloadThread("第一个")).start();
    new Thread(new DownloadThread("第二个")).run();

打印结果:

I/System.out: 第二个 正在执行。。。
I/System.out: 第一个 正在执行。。。

两种方式的区别和选择:
需要根据需求,因为Java只允许单继承,如果该类有继承其它类,再想要实现的话就只能选择实现Runnable接口了。

引入线程的好处:
  1. 线程占用资源要比进程少的多
  2. 创建一个新的线程花费的代价小
  3. 切换线程方便
  4. 提高并发性

三、多线程

多线程举例:比如用浏览器,同时进行浏览网页、播放视频、下载资源、听音乐等操作。

多线程缺点:

  1. 多线程比多进程成本低,不过性能也更低
  2. 一个线程的崩溃可能影响到整个程序的稳定性
  3. 线程多了之后,线程本身的调度也麻烦,需要消耗较多的CPU
  4. 无法直接获取系统的资源,总体能够达到的性能上限有限制
  5. 线程之间的同步和加锁控制比较麻烦

四、多进程

多进程举例:比如同时运行QQ、微信、截图工具、视频播放器等
多进程优点:

  1. 每个进程互相独立,子进程崩溃不影响主程序的稳定性
  2. 通过增加CPU,就可以容易扩充性能
  3. 进程能直接获取系统的资源,总体能够达到的性能上限非常大

五、同一进程的线程共享的资源

  1. ==堆==由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的)
  2. ==全局变量==它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的.
  3. ==局部静态变量==虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的。
  4. ==文件等公用资源==这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。

线程独享的资源

  1. 栈是独享的。
  2. 寄存器, 这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC。
  3. 线程ID。

并行和并发

并行: 指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行。

并发: 指应用能够交替执行不同的任务,其实并发有点类似于多线程的原理,多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已。

两者区别:一个是交替执行,一个是同时执行.

线程池

JDK自带的四种线程池

为什么要用线程池

相比new Thread,Java提供的四种线程池的好处在于:

Java通过Executors提供四种线程池,分别为:

1.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int finalI = i;
    pool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getId() + "  执行的任务-" + finalI);
        }
    });
}
打印结果:
I/System.out: 432  执行的任务-2
I/System.out: 434  执行的任务-4
I/System.out: 431  执行的任务-1
I/System.out: 433  执行的任务-3
I/System.out: 430  执行的任务-0
I/System.out: 433  执行的任务-8
I/System.out: 431  执行的任务-9
I/System.out: 435  执行的任务-5
I/System.out: 430  执行的任务-7
I/System.out: 436  执行的任务-6

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

2.newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    final int finalI = i;
    pool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getId() + "  执行的任务-" + finalI);
        }
    });
}
打印结果:
I/System.out: 422  执行的任务-3
I/System.out: 422  执行的任务-5
I/System.out: 422  执行的任务-6
I/System.out: 422  执行的任务-7
I/System.out: 422  执行的任务-8
I/System.out: 422  执行的任务-9
I/System.out: 423  执行的任务-4
I/System.out: 421  执行的任务-2
I/System.out: 420  执行的任务-1
I/System.out: 419  执行的任务-0

线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3.newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。

        ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
        pool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "  3秒后执行...");
            }
        }, 3, TimeUnit.SECONDS);
        pool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "  1秒后 每两秒执行一次...");
            }
        }, 1, 2, TimeUnit.SECONDS);

此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

4.newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    final int finalI = i;
    pool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getId() + "  执行的任务-" + finalI);
        }
    });
}
打印结果:
System.out: 427  执行的任务-0
System.out: 427  执行的任务-1
System.out: 427  执行的任务-2
System.out: 427  执行的任务-3
System.out: 427  执行的任务-4
System.out: 427  执行的任务-5
System.out: 427  执行的任务-6
System.out: 427  执行的任务-7
System.out: 427  执行的任务-8
System.out: 427  执行的任务-9

5.自定义线程池

自定义线程池需要继承ThreadPoolExecutor

public class CustomThreadPool extends ThreadPoolExecutor {

    public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }
}

参数说明:

corePoolSize:核心线程数。允许同时执行任务的最大线程数。
maximumPoolSize:最大线程数。允许同时处理任务的最大线程数。  
keepAliveTime:超出核心线程数的空闲线程最大存活时间。   
unit:上一个参数的时间单位。eg:TimeUnit.SECONDS 
workQueue:阻塞任务队列,存储待执行的任务    
threadFactory:线程工厂,用于创建线程,可指定线程命名规则。
handler:饱和策略(拒绝策略),当线程池阻塞队列已满时对新任务的处理。

ThreadPoolExecutor任务调度基本思路:

如果当前线程池线程个数小于corePoolSize则开启新线程执行提交的任务;否则添加任务到任务队列;如果任务队列满了,则尝试开启新线程执行任务,如果当前线程池中的线程数>maximumPoolSize则执行拒绝策略。

举例说明corePoolSize和maximumPoolSize:https://www.cnblogs.com/dolphin0520/p/3932921.html

假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来; 然后就将任务也分配给这4个临时工人做;如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。

这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。

线程的关闭

https://www.jianshu.com/p/bdf06e2c1541

如果程序中不再持有线程池的引用,并且线程池中没有线程时,线程池将会自动关闭。

线程池自动关闭的两个条件:1、线程池的引用不可达;2、线程池中没有线程;
线程池中没有线程是指线程池中的所有线程都已运行完自动消亡。然而我们常用的FixedThreadPool的核心线程没有超时策略,所以并不会自动关闭。
关闭线程池的方法:shutdown()

调用shutdown方法后

  1. 是否可以继续接受新任务?继续提交新任务会怎样?
  2. 等待队列里的任务是否还会执行?
  3. 正在执行的任务是否会立即中断?
1、是否可以继续接受新任务?继续提交新任务会怎样?
public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    executor.execute(() -> System.out.println("before shutdown"));
    executor.shutdown();
    executor.execute(() -> System.out.println("after shutdown"));
}

输出结果:

before shutdown
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task PoolTest$$Lambda$2/142257191@3e3abc88 rejected from java.util.concurrent.ThreadPoolExecutor@6ce253f1[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 1]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
    at PoolTest.main(PoolTest.java:12)

当线程池关闭后,继续提交新任务会抛出异常。这句话也不够准确,不一定是抛出异常,而是执行拒绝策略,默认的拒绝策略是抛出异常。可参见 线程池之ThreadPoolExecutor构造 里面自定义线程池的例子,自定义了忽略策略,但被拒绝时并没有抛出异常。

2、等待队列里的任务是否还会执行?
public class WaitqueueTest {
    public static void main(String[] args) {
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
        for(int i = 1; i <= 100 ; i++){
            workQueue.add(new Task(String.valueOf(i)));
        }
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, workQueue);
        executor.execute(new Task("0"));
        executor.shutdown();
        System.out.println("workQueue size = " + workQueue.size() + " after shutdown");
    }
    
    static class Task implements Runnable{
        String name;
        
        public Task(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            for(int i = 1; i <= 10; i++){
                System.out.println("task " + name + " is running");
            }
            System.out.println("task " + name + " is over");
        }
    }
}

这个demo解释一下,我们用LinkedBlockingQueue构造了一个线程池,在线程池启动前,我们先将工作队列填充100个任务,然后执行task 0 后立即shutdown()线程池,来验证线程池关闭队列的任务运行状态。
输出结果:

......
task 0 is running
task 0 is over
workQueue size = 100 after shutdown //表示线程池关闭后,队列任然有100个任务
task 1 is running
......
task 100 is running
task 100 is over

从结果中我们可以看到,线程池虽然关闭,但是队列中的任务任然继续执行,所以用 shutdown()方式关闭线程池时需要考虑是否是你想要的效果。

如果你希望线程池中的等待队列中的任务不继续执行,可以使用shutdownNow()方法,将上述代码进行调整,如下:

public class WaitqueueTest {
    public static void main(String[] args) {
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
        for(int i = 1; i <= 100 ; i++){
            workQueue.add(new Task(String.valueOf(i)));
        }
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, workQueue);
        executor.execute(new Task("0"));
        // shutdownNow有返回值,返回被抛弃的任务list
        List<Runnable> dropList = executor.shutdownNow();
        System.out.println("workQueue size = " + workQueue.size() + " after shutdown");
        System.out.println("dropList size = " + dropList.size());
    }
    
    static class Task implements Runnable{
        String name;
        
        public Task(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            for(int i = 1; i <= 10; i++){
                System.out.println("task " + name + " is running");
            }
            System.out.println("task " + name + " is over");
        }
    }
}

输出结果:

task 0 is running
workQueue size = 0 after shutdown
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
dropList size = 100
task 0 is over

从上述输出可以看到,只有任务0执行完毕,其他任务都被drop掉了,dropList的size为100。通过dropList我们可以对未处理的任务进行进一步的处理,如log记录,转发等;

3、正在执行的任务是否会立即中断?

要验证这个问题,需要对线程的 interrupt 方法有一定了解。


推荐阅读 ——线程中断机制

关于 interrupt() 方法:
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
Thread.interrupt() 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。

具体来说,当对一个线程,调用 interrupt() 时:

  1. 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
  2. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就可以这样做。

  1. 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
  2. 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)

public class InteruptTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        executor.execute(new Task("0"));
        Thread.sleep(1);
        executor.shutdown();
        System.out.println("executor has been shutdown");
    }

    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            
            for (int i = 1; i <= 100 && !Thread.interrupted(); i++) {
                Thread.yield();
                System.out.println("task " + name + " is running, round " + i);
            }
            
        }
    }
}

输出结果如下:

task 0 is running, round 1
task 0 is running, round 2
task 0 is running, round 3
......
task 0 is running, round 28
executor has been shutdown
......
task 0 is running, round 99
task 0 is running, round 100

为了体现在任务执行中打断,在主线程进行短暂 sleep , task 中 调用 Thread.yield() ,出让时间片。从结果中可以看到,线程池被关闭后,正则运行的任务没有被 interrupt。说明shutdown()方法不会 interrupt 运行中线程。再将其改修改为shutdownNow() 后输出结果如下:

task 0 is running, round 1
task 0 is running, round 2
......
task 0 is running, round 56
task 0 is running, round 57
task 0 is running, round 58
task 0 is running, round 59
executor has been shutdown

修改为shutdownNow() 后,task任务没有执行完,执行到中间的时候就被 interrupt 后没有继续执行了.

总结,想要正确的关闭线程池,并不是简单的调用shutdown方法那么简单,要考虑到应用场景的需求,如何拒绝新来的请求任务?如何处理等待队列中的任务?如何处理正在执行的任务?想好这几个问题,在确定如何优雅而正确的关闭线程池。

上一篇 下一篇

猜你喜欢

热点阅读