阅读 2108

Java线程、中断、线程池

Java多线程学习
多线程-系列
大话Android多线程(五) 线程池ThreadPoolExecutor详解
面试最常问的 Java 并发进阶常见面试题总结

一、进程和线程

进程

一个程序运行后被抽象为一个进程;
一个程序至少有一个进程,一个进程至少有一个线程.

线程

线程是程序执行时的最小单位,是CPU调度和分派的基本单位;
它是进程的一个执行流,一个进程可以由很多个线程组成; 线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量;
线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行

并行、并发

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行;
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

二、线程生命周期

image

  • 新建(New):创建Thread对象
  • 就绪(Runnable):start,但未运行,虚拟机为其创建方法调用栈和程序计数器,但运行时间取决于JVM调度
  • 运行(Running):
  • 阻塞(Blocked):线程暂停等待某个条件发生
  • 终止(Dead):正常执行结束、Exception或Error、stop();(isAlive()判断是否终止)

线程由阻塞状态,只能进入到就绪状态,而不能直接变为运行状态

运行和阻塞

运行

单核CPU,同一时间只能有一个线程运行,不同系统由不同的策略分配给各个线程执行时间;
多核CPU,同时可有多个线程并行(不是并发)

阻塞

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪,notify通知);
以下情况会发生阻塞:

  • sleep

  • 调用IO方法等待返回

  • 等待获取资源监视器monitor(synchronize)

  • wait等待获取notify通知

  • 调用suspend()将线程挂起(容易死锁,避免使用)

  • yield(不会阻塞,直接由运行状态变为就绪状态)

三、创建线程

Thread

new MyThread().start();
复制代码

Runnable

<!--1-->
new Thread(runnable).start();

<!--2-->
//用户需要做统一配置的操作
ThreadFactory factory = new ThreadFactory() {
    AtomicInteger threadCc = new AtomicInteger();

    @Override
    public Thread newThread(@NotNull Runnable runnable) {
        return new Thread(runnable, "thread_index_" + threadCc.getAndIncrement());
    }
};
factory.newThread(runnable).start();
factory.newThread(runnable).start();
factory.newThread(runnable).start();
复制代码

Future、Callable

在Java中使用Callable、Future进行并行编程
大话Android多线程(四) Callable、Future和FutureTask

  • Callable中的call方法作为线程的执行体,可以提供一个返回值,可以通过Future的get()方法拿到这个值
  • get()方法阻塞线程
<!--#1-->
Callable<String> callable = new Callable<String>(){
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        return Thread.currentThread().getName()+", Done!";
    }

};
FutureTask<String>futureTask = new FutureTask(callable);
new Thread(futureTask, "future task").start();
try {
    String taskResult = futureTask.get();
    System.out.println("taskResult:"+taskResult);
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}

<!--#2-->
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> future = executorService.submit(callable);
//executorService.shutdown();
while (true){
    if (future.isDone()) {
        try {
            String result = future.get();
            System.out.println("future:"+result);
            break;
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }else if(future.isCancelled()){
        break;
    }else {
        System.out.println("返回之前做其它事");
    }
}
复制代码

线程池

ExecutorService executor1 = Executors.newCachedThreadPool(factory);
ExecutorService executor2 = Executors.newFixedThreadPool(20, factory);
ScheduledExecutorService executor3 = Executors.newScheduledThreadPool(20, factory);
ExecutorService executor4 = Executors.newSingleThreadExecutor(factory);

//自定义线程池
LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,
                                50, TimeUnit.SECONDS,
                                workQueue);
executor.execute(runnable);
executor.execute(runnable);
复制代码

四、线程控制

4.1 sleep()

暂停执行一段时间,进入阻塞状态;
时间过去后,进入就绪状态

4.2 join()

把指定的线程加入到当前线程,让一个线程等待另一个线程执行完以后再执行,可以将两个交替执行的线程合并为顺序执行的线程

        ...
        try {
            threadA.start();
            threadA.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //当前线程等待threadA线程执行完以后,再往下执行
        ...
复制代码

4.3 yield()

线程让步:暂时让出自己的时间片给通优先级线程
当前线程由执行状态,转入就绪状态,不会阻塞线程

只是转入就绪状态,让线程调度器重新调度一次:
与当前线程优先级相同或更高,且处于就绪状态的线程获取执行,进入运行状态。 也有可能yield以后调度器重新立刻执行该线程。

4.4 后台线程

Daemon Thread,又称“守护线程”,在后台运行,为其它线程提供服务;
所有前台线程死亡,后台线程自动死亡;

setDaemon(true),在start()之前设置
isDaemon()判断是否为后台线程
复制代码
前台/后台线程
  • 创建的线程默认是前台线程
  • setDaemon(true)设置为后台线程
  • 在前台/后台 线程中创建的线程 默认为 前台/后台
线程优先级
    mThread.setPriority(Thread.MAX_PRIORITY);//10
    mThread.setPriority(Thread.NORM_PRIORITY);//5
    mThread.setPriority(Thread.MIN_PRIORITY);//1
    可以直接写数字,但是不同操作系统优先级不同,也不能很好的对应Java的10个优先级
    所以,尽量使用Thread提供的优先级静态字段。
复制代码

4.5 停止线程

stop()
  • 立即停止,强行中断
  • 对于代码逻辑不可控,粗暴式中断
interrupt()
  • 只是一个标记
  • 需要当前线程的逻辑支持,自行中断
Thread mThread = new Thread() {
    public void run() {
        if (mThtread.isInterrupted()) {
            //收尾
            return;
        }
    }
};

mThread.start();
mThread.stop();//强行终止
mThread.interrupt();//标记,配合终止
复制代码
判断interrupted状态:
  • thead.isInterrupted():仅仅返回当前状态
  • Thread.interrupted():返回当前状态,并重置该状态
    The interrupted status of the thread is cleared by this method
interrupt()引起的异常
  • 当mThread.interrupt()时,sleep、join、wait会抛异常,并重置了该状态,true --> false
  • SystemClock.sleep(2000),内部处理了异常,并从新设置为中断状态,true --> false --> true
mThread.interrupt();//标记,配合终止

Thread mThread = new Thread() {
    public void run() {
    
        //只返回状态
        if (mThread.isInterrupted()) {
            //收尾
            return;
        }
        
        //返回状态,并重置状态
        if (Thread.interrupted()) {
            //收尾
            return;
        }

        try {
            Thread.sleep(2000);
            //interrupt()时,能唤醒sleep、join、wait
            //内部使用Thread.interrupted()检查、重置、抛异常
        } catch (InterruptedException e) {
            e.printStackTrace();
            //收尾
            return;
        }

        //被打断,不会重置状态,内部实现里又重新设置回来了
        SystemClock.sleep(2000);
    }
};

总结:
在mThread.isInterrupted()、Thread.interrupted()、InterruptedException中做收尾工作
复制代码
线程中断机制

当对一个线程,调用 interrupt() 时,

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

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行

  • 在线程中检查Thread.interrupted状态,做相应处理
  • 捕获 InterruptedException异常,做相应处理

五、线程同步:

5.1 synchronize

使用位置:

方法、代码块

锁类型:
  • 对象锁:
    每个对象都会有一个 monitor 对象,通常会被称为“内置锁”或“对象锁”;
    类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
  • 类锁:
    针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁;
    每个类只有一个 Class 对象,所以每个类只有一个类锁。
synchronize本质
互斥性:

保证方法或代码块内部对资源的互斥访问;
即同一时间,同一个monitor监视的代码,最多只能有一个线程访问

image

同步性:

保证线程之间,对监视资源的数据同步;
即任何线程在获得monitor的第一时间,现将共享内存中的数据复制到自己的缓存中;
任何线程在释放monitor的第一时间,将自己缓存中的数据复制到共享内存中

image

5.2 Lock 【暂未整理】

同样是“加锁”机制,使用方式更灵活,但也更麻烦

Java 并发:Lock 框架详解
读写锁...

5.3 原子操作

程序的最小执行单位,不能被分割执行,如 a = 1+2;
注意:i++不是原子操作 --> tmp = i+1; i = tmp

volatile

轻量级的synchronize

  • volatile 修饰的成员变量在每次被线程访问时,都强迫从主存(共享内存)中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主存(共享内存);
    这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,这样也就保证了同步数据的可见性
  • 只对基本类型的赋值操作和对象的引用赋值操作有效;
    自增/减,不是原子操作,不保证
  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。
    但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
    synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
Atomic类

作用和volatile基本一致,对于操作基本类型做了封装,更方便;
AtomicInteger、AtomicBoolean...

AtomicInteger ato = new AtomicInteger();
ato.addAndGet(1);
ato.getAndAdd(2);
ato.getAndIncrement();
ato.incrementAndGet();
复制代码

5.4 总结

无论是线程安全问题,还是衍生出的锁机制问题,它们的核心都在于“保证资源的安全性”,而不是某个方法或某几行代码

线程安全问题的本质

数据的安全性

多个线程访问共同的资源时,在某一个线程对资源进行写操作的中途,其它线程对该资源进行读、写,导致出现了数据的错误。

锁机制的本质

对资源进行访问限制

使同一时间只有一个线程可以访问资源,保证数据的准确性。

六、线程通信

wait

线程释放共享资源锁,进入等待队列,直到被再次唤醒

notify、notifyAll

唤醒该共享资源锁上等待的单个/全部线程,唤醒哪个和顺序:取决于优先级和JVM

  • 在synchronized方法或代码块中使用,否则报错:IllegalMonitorStateException
  • 调用的是当前monitor的Object方法,而不是Thread方法
    虽然在Thread中使用,但阻塞是因为在等monitor释放并获取通知
private String testStr;

public void setStr(String str) {
    synchronized (obj){
        this.testStr = str;
        obj.notify();
    }
}

public String getStr() {
    /**
     为什么不用if而要用while?
     因为wait不确定会被谁唤醒(notify、interrt)
     1. wait()一般都是这样搭配的标准写法
     2. wait不只会被notify唤醒,还可能会被intrupt唤醒;
        此时如果是if的话,wait被唤醒,就不查是否为null直接return了
     */
    synchronized (obj){
        //if (testStr == null){
        while (testStr == null) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return testStr;
        }
    }
}
    
#范式
    等待方:
    synchronized( 对象 ) {
        //not if
        while(条件) {
            对象.wait();
        }
        对应的处理逻辑
    }
    
    通知方:
    synchronized( 对象 ) {
        改变条件;
        对象.notifyAll();
    }
复制代码

七、线程池

使用线程池-廖雪峰
Java 四种线程池的用法分析
如何优雅的使用和理解线程池
线程池没你想的那么简单
Java线程池(ThreadPoolExecutor)原理分析与使用
Java面试经典题:线程池专题

  • Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间
  • 如果可以复用一组线程,那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
  • 简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
  • 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
    说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换"的问题。---《阿里巴巴 Java 手册》

new Thread和线程池比较

new Thread()的缺点
  • 每次new Thread()耗费性能
  • 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪
  • 不利于扩展,比如如定时执行、定期执行、线程中断
采用线程池的优点
  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可控性:可以进行统一的分配,调优和监控。可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
  • 提供管理功能:提供定时执行、定期执行、单线程、并发数控制等功能控制

线程池实现

Java标准库提供了ExecutorService接口表示线程池,常用实现类:

  • FixedThreadPool:线程数固定的线程池;
  • CachedThreadPool:线程数根据任务动态调整的线程池;
  • SingleThreadExecutor:仅单线程执行的线程池。

创建这些线程池的方法都被封装到Executors这个类中

/**
* corePoolSize 为线程池的基本大小。
* maximumPoolSize 为线程池最大线程大小。
* keepAliveTime 和 unit 则是线程空闲后的存活时间。
* workQueue 用于存放任务的阻塞队列。
* handler 当队列和最大线程池都满了之后的饱和策略。
**/
ThreadPoolExecutor(int corePoolSize, 
                   int maximumPoolSize,
                   long keepAliveTime, TimeUnit unit, 
                   BlockingQueue<Runnable> workQueue, 
                   RejectedExecutionHandler handler)

ExecutorService executor1 = Executors.newCachedThreadPool(threadFactory);
ExecutorService executor2 = Executors.newFixedThreadPool(20, threadFactory);
ScheduledExecutorService executor3 = Executors.newScheduledThreadPool(20, threadFactory);
ExecutorService executor4 =Executors.newSingleThreadExecutor(threadFactory);
复制代码
向线程池提交任务
  • execute(Runnable runnable):提交不需要返回值的任务,但无法判断任务是否被线程池执行成功
  • submit(Callable task):提交需要返回值的任务,会返回一个future类型的对象。通过这个future对象可以判断任务是否执行成功、返回值等
线程池状态

image

  • RUNNING 自然是运行状态,指可以接受任务执行队列里的任务
  • SHUTDOWN 指调用了 shutdown() 方法,不再接受新任务了,但是队列里的任务得执行完毕。
  • STOP 指调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。
  • TIDYING 所有任务都执行完毕,在调用 shutdown()/shutdownNow() 中都会尝试更新为这个状态。
  • TERMINATED 终止状态,当执行 terminated() 后会更新为这个状态。
关闭线程池
  • shutdown()
    • 停止接受新任务,会把队列的任务执行完毕,中断在等待的任务
    • 线程池转态置为SHUTDOWN
    • 此时还可以执行shutDownNow()
    • 任务队列和线程池为空时,状态置为TIDYING
  • shutDownNow()
    • 停止接受新任务,中断所有的任务
    • 线程池转态置为STOP
    • 线程池为空时,状态置为TIDYING
  • awaitTermination(long timeout, TimeUnit unit)
    监测ExecutorService是否已经关闭,当等待超过设定时间时,若线程池已关闭返回true,否则返回false。
    线程阻塞。
安全关闭

shutDown和shutDownNow前者给在等待的线程调用interrupt(),后者给工作线程调用interrupt()。
了解线程中断机制的话就会明白,interrupt()只是设置了一个标志位而已,并且在线程阻塞的时候可能抛出异常,仅此而已,并不能实质性的中断线程。需要我们在线程中做中断处理策略。
所以,可以结合shutDown、shutDownNow、awaitTermination方法和线程中断策略,安全的关闭线程池

<!--关闭线程池-->
threadPool.shutdown(); // Disable new tasks from being submitted
try {
    int tryCc = 5;
    for (int i = 0; i < tryCc; i++) {
        if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {//此处线程阻塞
            threadPool.shutdownNow();//取消正在执行的任务
        }else {
            System.out.println("线程池任务正常执行结束");
            return;
        }
    }
    System.err.println("线程池任务未正常执行结束");
} catch (InterruptedException ie) {
    threadPool.shutdownNow();
}

<!--线程中的中断策略-->
1. 检查isInterrupted状态
2. 处理InterruptedException异常
复制代码
合理的配置线程池
  • CPU核数
  • 任务优先级、执行时间长短、任务类型(IO、计算等)
文章分类
Android
文章标签