四、多线程/并发

1. Java中常见的锁,互斥锁,读写锁,信号量

从并发的角度来讲,按照线程安全的三种策略看,主要内容都集中在互斥同步里,我们所讨论的锁也集中在这个部分。这个部分的锁都是悲观锁,第二个部分是非阻塞同步,这个部分也就一种通过CAS进行原子类操作,这个部分可以看成乐观锁,其实也就是不加锁。第三个部分是无同步方案,包括可重入代码和线程本地存储。

我们这里主要讨论的就是互斥同步这一部分。

一. 按照其性质分类

公平锁(多个线程按照申请锁的顺序来获取锁)/非公平锁,乐观锁/悲观锁(主动加锁),独享锁(该锁一次只能被一个线程所持有)/共享锁,互斥锁/读写锁(独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中的具体实现就是ReentrantReadWriteLock,读共享,写互斥),可重入锁。

条件锁:体现的是一种协作,我准备好了,通知你开始吧,一般用于线程同步任务。

读写锁:体现的是一种竞争,我离开了,通知你进来,防止读写竞争。

注:可重入锁需要重点介绍一下。

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Reentrant Lock重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。(

两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。)

public sychrnozied void test() {

xxxxxx;

test2();

}

public sychronized void test2() {

yyyyy;

}

在上面代码段中,执行 test 方法需要获得当前对象作为监视器的对象锁,但方法中又调用了 test2 的同步方法。

如果锁是具有可重入性的话,那么该线程在调用 test2 时并不需要再次获得当前对象的锁,可以直接进入 test2 方法进行操作。

如果锁是不具有可重入性的话,那么该线程在调用 test2 前会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有,不可能再次获得。

如果锁是不具有可重入性特点的话,那么线程在调用同步方法、含有锁的方法时就会产生死锁。

二. 按照设计方案来分类

自旋锁(共享数据锁定状态只持续很短时间,为了这段时间挂起线程不值得。如果一个物理机有一个以上处理器,能让两个及以上的线程同时执行,那么让后面请求的锁稍等一会,不放弃处理器执行时间,看看持有锁的线程是否很快就会释放锁)/自适应自旋锁,锁粗化/锁消除,偏向锁/轻量级锁(cas)/重量级锁,分段锁(

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。)

2. 原子Atomic类,如何保证原子性?

什么是原子性:个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

在数据库中:事务要么被执行,要么一个都没被执行

在并发编程中:我们把一个线程中的一个或多个操作(不可分割的整体),在CPU执行过程中不被中断的特性,称为原子性。(执行过程中,一旦发生中断,就会发生上下文切换;例如i++操作包含三个步骤:取出i,i+1,对i赋值,这三个步骤中可以能发生中断情况,因此i++不是原子性的)

atomic类是非阻塞的,它的原子性是通过硬件层面的相关指令(CAS硬件指令,CAS原理见第6条)来完成的。

Atomic包核心:Atomic包里的类基本都是使用Unsafe实现的包装类,核心操作是CAS原子操作;

 鉴于并发包中的原子类其实现机理都差不太多,本章我们就通过AtomicInteger这个原子类来进行分析。我们先来看看对于num++这样的操作AtomicInteger是如何保证其原子性的。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

 我们来分析下incrementAndGet的逻辑:

  1.先获取当前的value值

  2.对value加一

  3.第三步是关键步骤,调用compareAndSet方法来来进行原子更新操作,这个方法的语义是:

    先检查当前value是否等于current,如果相等,则意味着value没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。

  compareAndSet调用了Unsafe类的compareAndSwapInt方法。Unsafe的compareAndSwapInt是个native方法,也就是平台相关的。它是基于CPU的CAS指令来完成的。

3. volatile,可见性问题的原因?(硬件架构,L3 Cache,QPI,乐观锁)?

当一个线程更新volatile变量时,另一个线程读取该变量时,都会刷新缓存,因此一个线程修改了变量后,另一个线程是可见的。

计算机内的硬件架构是什么样的呢?一般主存和cup进行数据交换时,cpu速度较快,主存速度较慢,这导致了cpu每次内存操作比较慢,因此引入在cpu和主存之间添加了高速缓存。高速缓存为cpu的三级缓存结构。

  • L1 Cache最接近CPU, 容量最小(如32K、64K、256K等)、速度最高,每个核上都有一个L1 Cache。
  • L2 Cache容量更大(如256K)、速度更低, 一般情况下,每个核上都有一个独立的L2 Cache。
  • L3 Cache最接近内存,容量最大(如12MB),速度最低,在同一个CPU插槽之间的核共享一个L3 Cache。

引入了缓存后就会有缓存一致性问题。

两个CPU执行两个线程,都执行count++,都先从主内存获取count=0,都将count=0从主存拷贝到各自的缓存区,且在缓存区count=0的状态为S(Shared,多线程共享状态)。

一个线程(左边的线程)先执行了修改count+=1,按MESI缓存协议规范,在该线程中count是M状态(即:已被修改),则其他拥有count变量的线程,count状态都变为(I)失效状态,即需要再去主内存拿新值。

可见,缓存一致性协议解决了多核硬件架构的一致性问题。那缓存一致性又和JMM有什么关系呢?

JMM也是遵照多核硬件架构的设计,用Java实现了一套JVM层面的“缓存一致性”。

4. 如何实现一个线程安全的数据结构

- 性能要求不高,来个大锁(对象锁)。
- 读多写少,就来读写锁;
- 读写均衡,可以来分区 /分段锁。
- 资源为简单类型,atomic。
- 对更改不敏感,COW & TheadLocal
- 实时性邀请不高,外部队列。

5. 如何避免死锁

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

造成死锁必须达成的4个条件(原因):

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
  • 互斥条件 ---> 独占锁的特点之一。
  • 请求与保持条件 ---> 独占锁的特点之一,尝试获取锁时并不会释放已经持有的锁
  • 不剥夺条件 ---> 独占锁的特点之一。
  • 循环等待条件 ---> 唯一需要记忆的造成死锁的条件。

所以,面对如何避免死锁这个问题,我们只需要这样回答!
在并发程序中,避免了逻辑中出现复数个线程互相持有对方线程所需要的独占锁的的情况,就可以避免死锁。
https://www.jianshu.com/p/44125bb12ebf

6. 如何解决ABA问题

cas(Compare and Swap):将内存中的值、我们的期望值、新值交给CPU进行运算,如果内存中的值和我们的期望值相同则将值更新为新值,否则不做任何操作。

ABA:线程1从内存取到了V但是还没有开始比较,CPU的时间片用完了,然后,CPU时间片分给了线程2使用,线程2捣鼓了一下V,从A变成B,之后又从B变成了A。接着线程1拿到了CPU的时间片,开始比较V,一看正好是A,所以就开始拿到锁了。

解决方法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A。java中可以使用AtomicStampedReference/AtomicMarkableReference等变量解决该问题。它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

https://www.cnblogs.com/549294286/p/3766717.html

7. Synchronized关键字的作用?

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
https://blog.csdn.net/luoweifu/article/details/46613015

锁原理

使用monitorenter和monitorexit指令实现的:

  • monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
  • 每个monitorenter必须有对应的monitorexit与之配对
  • 任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态

锁状态存储在对象头的信息中。

锁升级过程

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。无竞争时不需要进行CAS操作来加锁和解锁。

轻量级锁:无竞争时通过CAS操作来加锁和解锁。(自旋锁——是一种锁的机制,不是状态)

重量级锁:真正的加锁操作

重排序问题:临界区(synchronized?)

临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM会在退出临界区和进入临界区这两个关键时间点做一些特别处理,虽然线程A在临界区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。

8. Volatile关键字的作用?

1.一个线程修改的状态对另一个线程是可见的

原理:volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令:

  • 将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

2.禁止指令重排序

volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

内存屏障:Load读屏障 store写屏障,屏障前后的两条语句执行规则,例如

插入内存屏障storeload,那么就是前面的写操作写到主内存后完后,后面从主内存重新读数据

其他同理

JVM内存屏障插入策略:

每个volatile写操作的前面插入一个StoreStore屏障;
在每个volatile写操作的后面插入一个StoreLoad屏障;
在每个volatile读操作的后面插入一个LoadLoad屏障;
在每个volatile读操作的后面插入一个LoadStore屏障。

可以用于单例模式中的双重锁定的影响,将要初始化的单例声明为volitile变量来禁止指令重排序。

重排序场景

as-if-serial

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。

9. Java内存模型是怎样的?

java内存模型规定所有变量都存储在主内存。每条线程都有自己的工作内存,该工作内存保存了须使用的主内存的副本拷贝,线程对变量的操作必须在工作内存中进行。不同线程之间不能共享内存。

Happens-Before

用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

happens-before关系本质上和as-if-serial语义是一回事。as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

10. HashMap在多线程环境下使用需要注意什么?为什么?

需要注意线程安全问题。

在java7中,由于rehash的时候使用的头插法,会出现环形的数据结构,查询时会出现死循环。(具体原因是头插法会把链表元素的顺序反过来,下一个线程访问时元素的next指向前一个元素形成了环形结构)

其他情况下:在同一个hash位置上,多个线程同时插入数据,那么会出现后面插入的数据覆盖前面数据的可能。扩容时,也会出现相同的情形。

11. Java程序中启动一个线程是用run()还是start()?

start,run是线程启动后自动执行的逻辑。当然,你也可以直接调用run方法,但是这个方法不会开启一个线程,只是执行代码逻辑。

12. 什么是守护线程?有什么用?

线程分为两种,用户线程(user)和守护线程(daemon)。

定义:又称为服务线程,运行在后台的特殊进程。在没有用户线程可以服务时会自动退出。

优先级:优先级比较低,为其他用户线程提供服务。

使用:在调用start方法前,调用setDaemon(true)。

例子:垃圾回收线程是一个经典的守护线程,它始终在低优先级状态执行,用于监控和管理系统中的可回收资源。

生命周期:守护线程不依赖于终端,但是依赖于系统,与系统共生死。

13. 线程和进程的差别是什么?

线程是比进程更轻量级的调度执行单位,线程的引入将资源的调度和分配分开,各个线程既可以共享资源(内存地址,内存io等),又可以独立调度(线程是cpu调度的基本单位,基本上不拥有资源,只拥有必须的资源,如寄存器,栈空间等)。

一个应用程序就是一个进程,它包含了应用程序所需的所有资源,包含线程,进程的资源被线程共享。

14. Java里面的Threadlocal是怎样实现的?

ThreadLocal是线程内部的数据存储类。

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

同时它的构造函数是懒加载的,所以当没有数据的时候,是不会创建的。

它由一个私有、静态内部类实现(ThreadLocalMap),ThreadLocalMap中是一个entry[]数组,entry的key是ThreadLocal对象,是一个弱引用weekReference(

为什么是弱引用呢,因为当threadlocal对象不在使用的时候将其置位null,但是这个时候entry的key还是指向的threadlocal对象,如果这个时候是强引用就会导致threadlocal对象没办法回收会造成内存泄漏,所以改成弱引用的话当只有一个弱引用的entry的key指向threadlocal对象的时候Threadlocal对象在垃圾回收的时候就会被回收掉。

),v为key对应的值。这里主要有两个方法,一个是get,从threadLocalmap中获取当前线程的threadLocalMap,若存在则根据当前的threadLocal获取entry值(ThreadLocalMap.getEntry(this)),若不存在,则初始化ThreadLocalMap,或者将当前的值set进去。另一个方法是set,如果当前线程已存在ThreadLocalMap,则直接使用,若不存在,则创建map,最后set(this,v)。

那么,父线程的ThreadLocal变量可以被子进程获取到吗?答案是不能的,因此有了InheritableThreadLocal。InheritableThreadLocal通过创建map时复制父线程的map中的值实现的。

//thread的init方法
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        //............代码省略
        //如果使用了inheritThreadLocals,且父线程的Threadlocals不为空,那么复制一份到当前线程
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

以下代码为复制父线程的map的过程


        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

但是使用线程池时,父线程的变量,子线程还是不能访问到,这时候就需要使用到阿里开源的代码了TransmittableThreadLocalhttps://github.com/alibaba/transmittable-thread-local

主要原理:使用类TransmittableThreadLocal来保存值,并跨线程池传递。

TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。

相比InheritableThreadLocal,添加了

  1. copy方法
    用于定制 任务提交给线程池时 的ThreadLocal值传递到 任务执行时 的拷贝行为,缺省传递的是引用。
    注意:如果跨线程传递了对象引用因为不再有线程封闭,与InheritableThreadLocal.childValue一样,使用者/业务逻辑要注意传递对象的线程安全。
  2. protectedbeforeExecute/afterExecute方法
    执行任务(Runnable/Callable)的前/后的生命周期回调,缺省是空操作。

内存泄露问题

该类中有一个remove方法:将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)

ThreadLocal只是操作Thread中的ThreadLocalMap,每个Thread都有一个map,ThreadLocalMap是线程内部属性,ThreadLocalMap生命周期是和Thread一样的,不依赖于ThreadMap。

ThreadLocal通过Entry保存在map中,key为Thread的弱引用(GC时会自动回收),value为存入的变量副本,一个线程不管有多少个ThreadLocal,都是通过一个ThreadLocalMap来存放局部变量的,可以再源码中看到,set值时先获取map对象,如果不存在则创建,threadLocalMap初始大小为16,当容量超过2/3时会自动扩容。

1.使用ThreadLocal,建议用static修饰 static ThreadLocal<HttpHeader> headerLocal = new ThreadLocal();

2.使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
链接:https://juejin.im/post/5ba9a6665188255c791b0520

15. ConcurrentHashMap的实现原理是?

java7中采用分段锁结构,将整个hash拆分成多段,每段对应一个segment分段锁,段与段之间可以并发访问。

java8中取消了分段锁的思想,使用cas+sychronized控制并发操作,并添加了红黑树的实现,逻辑还是很复杂的,请仔细阅读源码。

16. sleep和wait区别?

sleep是Thread中的方法,sleep只是让出的cpu,不会释放他持有的管程(操作系统中的p、v操作);不需要在同步代码块中调用。

wait是object中的方法。wait会让当前线程暂时退出同步资源锁,会释放他持有的对象的管程和锁。必须在同步代码块中调用。

17. notify和notifyAll区别?

notify随机唤醒一个线程,notifyAll唤醒所有线程(唤醒的是wait状态的线程,不是sleep状态的线程);

在生产者和消费者模式中,一个生产者和一个消费者的情况下,使用一个wait和notify方法没有问题,但是一个生产者,多个消费会出现死锁,不能继续生产和消费。

当一个线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A进入到该对象的等待池中。如果一个线程调用了该对象的notify方法,那么仅有一个处于该对象等待池中线程会进入到对象的锁池。如果另一个线程调用了该对象的notifyAll方法,那么处于该对象等待池中的所有线程会进入该对象的锁池中。

死锁举例:

  1. 现在有三个线程,生产者P1, 消费者C1和C2.开始运行的时候,三个都在锁池中等待竞争,假设C1抢到锁了,C1执行时由于没有资源可以消费 调用wait()方法,释放锁并进入等待池。
  2. C2抢到了锁,开始消费,同理,C2也进入了等待池。现在锁池里面只剩下了P1.
  3. P1获得了锁,开始生产,生产完成后,P1开始调用notify()方法唤醒等待池中的C1或者C2,然后P1调用wait()方法释放锁,并进入了等待池。
  4. 假设唤醒的是C1,C1进入锁池并获得锁,消费后notify()方法唤醒了C2,C2进入锁池,C1进入等待池,现在锁池中只有C1。
  5. C1获得了锁,发现没有任何资源可以消费,wait()后释放了锁,进入了等待池,现在三个线程全都在等待池,锁池中没有任何线程。导致死锁!

notifyAll()后,不存在只唤醒同类线程的情况,故也就不会出现以上死锁的情况。

应用场景:

一旦任务结束则所有等待的线程都可以执行自己的业务逻辑了。那么我们就可以使用notifyAll

另一个场景:只有一个等待的线程被唤醒后可以执行一些有意义的动作(例如:获取锁),此时就应该使用notify,当然你也可以使用nofityAll,但是没有必要。应该其它被唤醒的线程并不能够做什么。

每个同步对象都有自己的锁池和等待池。

锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池。

https://juejin.im/post/5c930141e51d450ad30e43d3

18. 两个线程如何串行执行

1.使用join
     thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

t.join();      //调用join方法,等待线程t执行完毕
t.join(1000);  //等待 t 线程,等待时间是1000毫秒。


public class ThreadTest1 {
// T1、T2、T3三个线程顺序执行
public static void main(String[] args) {
    Thread t1 = new Thread(new Work(null));
    Thread t2 = new Thread(new Work(t1));
    Thread t3 = new Thread(new Work(t2));
    t1.start();
    t2.start();
    t3.start();
 
}
static class Work implements Runnable {
    private Thread beforeThread;
    public Work(Thread beforeThread) {
        this.beforeThread = beforeThread;
    }
    public void run() {
        if (beforeThread != null) {
            try {
                beforeThread.join();
                System.out.println("thread start:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("thread start:" + Thread.currentThread().getName());
        }
    }
 }
}

2 .使用countDownLatch

CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。


public class ThreadTest2 {
 
// T1、T2、T3三个线程顺序执行
public static void main(String[] args) {
    CountDownLatch c0 = new CountDownLatch(0); //计数器为0
    CountDownLatch c1 = new CountDownLatch(1); //计数器为1
    CountDownLatch c2 = new CountDownLatch(1); //计数器为1
 
    Thread t1 = new Thread(new Work(c0, c1));
    //c0为0,t1可以执行。t1的计数器减1
 
    Thread t2 = new Thread(new Work(c1, c2));
    //t1的计数器为0时,t2才能执行。t2的计数器c2减1
 
    Thread t3 = new Thread(new Work(c2, c2));
    //t2的计数器c2为0时,t3才能执行
 
    t1.start();
    t2.start();
    t3.start();
 
}
 
//定义Work线程类,需要传入开始和结束的CountDownLatch参数
static class Work implements Runnable {
    CountDownLatch c1;
    CountDownLatch c2;
 
    Work(CountDownLatch c1, CountDownLatch c2) {
        super();
        this.c1 = c1;
        this.c2 = c2;
    }
 
    public void run() {
        try {
            c1.await();//前一线程为0才可以执行
            System.out.println("thread start:" + Thread.currentThread().getName());
            c2.countDown();//本线程计数器减少
        } catch (InterruptedException e) {
        }
 
    }
 }
}

3 .使用锁和条件锁

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 使用条件队列实现线程的交替打印
 */
public class PrintMain {

    static final Lock lock = new ReentrantLock();
    static final Condition threadAPrint = lock.newCondition();
    static final Condition threadBPrint = lock.newCondition();

    static int num1 = 0;
    static int num2 = 1;
    static int end = 100;

    volatile static int state = 0;

    public static void main(String args[]) {

        final Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    /**
                     * 获取锁,获取锁不成功时,当前线程休眠,直到获取锁,继续执行
                     */
                    lock.lock();
                    try {
                        if (state == 1) {
                            try {

                                /**
                                 * Causes the current thread to wait until it is signalled or
                                 * {Thread#interrupt interrupted}.
                                 *
                                 * The lock associated with this {Condition} is atomically
                                 * released and the current thread becomes disabled for thread scheduling
                                 * purposes and lies dormant until <em>one</em> of four things happens...
                                 */
                                threadAPrint.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        state = 1;
                        if (num1 >= end) {
                            break;
                        }
                        System.out.println(num1);
                        num1 += 2;
                        /**
                         * Wakes up one waiting thread.
                         */
                        threadBPrint.signal();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {

                while (true) {
                    lock.lock();

                    try {
                        if (state == 0) {
                            try {
                                threadBPrint.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        state = 0;
                        if (num2 >= end) {
                            break;
                        }
                        System.out.println(num2);
                        num2 += 2;
                        threadAPrint.signal();
                    } finally {
                        lock.unlock();
                    }
                }

            }
        });

        threadA.start();
        threadB.start();
    }
}

https://blog.csdn.net/Evankaka/article/details/80800081

https://my.oschina.net/xinxingegeya/blog/745899

19. 上下文切换是什么含义

       一句话:一个线程切换到另一个线程

CPU 上下文切换,就是先把前一个任务的 CPU 上下文( CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

这些保存下来的上下文,存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务给大家的感觉还是在连续运行。

内核态:操作系统内核中的一种状态(cpu中)。

用户态:应用程序最开始都是运行在用户态,他们可以通过系统调用使用内核态相关内容。

上下文切换分类

一. 进程上下文切换

  1. CPU 的时间片会公平的分配给各个进程。当某个进程的时间片用完后,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
  2. 进程在系统资源不够时,要等到资源满足后才能运行,这时进程也会被挂起,并由系统调度其他进程运行。
  3. 当进程通过 sleep 这样的方法将自己主动挂起时,也会重新调度。
  4. 当有优先级更高的进程运行时,当前进程会被挂起,高优先级进程优先执行。

二. 线程上下文切换

当进程只有一个线程时,可以认为进程就等于线程。当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。

  1. 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
  2. 前后两个线程属于同一个进程。大家都知道,同一个进程下的线程是资源共享的,这时候因为虚拟内存是进行共享的,所以在切换时,虚拟内存这些资源不变,只需要切换线程的私有数据、寄存器等不共享的数据,这种情况的线程上下文切换就要比进程间的切换消耗更少的资源,所以这也是多线程相比较多进程的优势。

三. 中断上下文切换

上下文切换有时也因硬件中断而触发。硬件中断是指硬件设备(如键盘、鼠标、调试解调器、系统时钟)给内核发送的一个信号,该信号表示一个事件(如按键、鼠标移动、从网络连接接收到数据)发生了。

20. 可以运行时kill掉一个线程吗?

一般来说是不推荐的,因为kill一个线程的时候,并没有收尾动作,它拥有的资源都不能回收和使用了,这样就会破坏了全局状态。

除了资源回收的问题之外,还有一个弊端,那就是锁。其实严格来说,锁也算一种资源。当我们使用多个线程,去访问一个共享对象时,不可避免的要使用锁来做线程同步(当然了,你可以说用lock-free,但lock-free并不是万金油,在逻辑上必须进行条件等待的时候你还是得乖乖等待)。当我们的一个线程获取了一个锁,正在访问某个共享方法的时候(比如调一个API啊,打印一个日志啊,balabala),还没来得及解锁就被咔嚓了,那这个锁就永远不会被解掉了,于是所有依赖这个锁的其它线程都华丽丽的死锁掉了。

https://blog.csdn.net/markl22222/article/details/33310953

21. 什么是协程(用户态线程,减少数据拷贝,降低CPU开销,无callback函数)?

背景:

使用synchronized创建生产者和消费者有哪些损耗(wait,notify)

1.涉及到同步锁。

2.涉及到线程阻塞状态和可运行状态之间的切换。

3.涉及到线程上下文的切换。

协程是在线程里面跑的,因此协程又称为微线程或纤程。一个线程里面可以含有多个协程。

协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样,协程减少了上下文切换的消耗。

原子操作,由于协程是用户调度的,所以不会出现执行一般代码片段被强制中断了。

协程的实现:

迭代器:java的foreach遍历数组。对象可以通过next获取下一个的值。

生成器:python使用yield函数,可以多次返回值。

协程举例:在Python中,使用了yield的函数为生成器函数,即可以多次返回值。则生成器可以暂停一下,转而执行其他代码,再回来继续执行函数往下的代码。

https://juejin.im/post/5b0014b7518825426e023666

22. 线程池ThreadPoolExecutor的实现原理?ThreadPool的深入考察; BlockingQueue的使用。new ThreadPoolExecutor(10,100,10,TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10));一个这样创建的线程池,当已经有10个任务在运行时,第11个任务提交到此线程池执行的时候会发生什么,为什么?

为什么要使用线程呢?因为计算机有多个cpu,合理创建线程,会最大化的利用CPU和io资源使程序运行的更快

一 为什么使用线程池

1 线程池可复用线程资源,以提高系统的响应速度,节省线程频繁的开关资源。

2 可对已有的线程进行管理

二 线程池工作原理

创建线程主要是ThreadPoolExecutor类来完成,该类的主要的构造函数为:

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

下面解释一下各个参数的含义

1 corePoolSize:核心线程池大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动。

2. maxmumPoolSize:线程池能创建线程的最大个数。当阻塞队列已满,且当前线程池线程个数小于该参数时,就会创建新线程执行任务。

3. keepAliveTime:线程的空闲时间。如果当前线程池线程个数超过了corePoolSize,bingqie xiancheng kongxianshijian chaoguole keepAliveTime时就会将这些空闲线程销毁,这样能降低资源消耗。

4. unit:空闲时间的时间单位

5. workQueue:阻塞队列。用于保存任务的队列。

1 ArrayBlockingQueue

有界缓冲区。需设置大小,一旦设定不可改变。在读写操作上需要锁住整个容器,吞吐量与一般实现是相似的,适合生产者消费者模式。且不能保证公平性(线程先来后到的顺序),如果保证公平性,通常会降低吞吐量。代码如下

private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);

2 LinkedBlockingQueue

可以设置大小,不设置时,默认为Inter.max_value.

1与2的区别

  • 锁的实现:1生产和消费使用的同一把锁,2生产使用的putLock,消费使用的takeLock,提高了吞吐率
  • 生产或消费的操作不同:1直接将枚举对象插入或移除的,2将枚举对象转换为Node<E>实现的,对于GC可能存在较大影响。
  • 队列大小:1必须设置,2可设可不设

PriorityBlockingQueue

无边界的队列,允许插入null,需实现Comparale接口给优先级排序

4 SynchronousQueue

队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。(一手交钱一手交货)

5 LinkedTransferQueue

LinkedTransferQueue是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了TransferQueue接口,与其他阻塞队列相比主要有以下不同的方法:

6 DelayQueue

无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过poll等方法获取数据元素则返回null。所谓数据延时期满时,则是通过Delayed接口的getDelay(TimeUnit.NANOSECONDS)来进行判定,如果该方法返回的是小于等于0则说明该数据元素的延时期已满。

transfer(E e)
如果当前有线程(消费者)正在调用take()方法或者可延时的poll()方法进行消费数据时,生产者线程可以调用transfer方法将数据传递给消费者线程。如果当前没有消费者线程的话,生产者线程就会将数据插入到队尾,直到有消费者能够进行消费才能退出;

6. ThreadFactory:创建线程的工厂类。

7. handler:拒绝策略。一般有以下几种

  •  AbortPolicy:默认的拒绝策略;拒绝任务,并跑抛出RejectExecutionException异常
  •  DiscardPolicy:不处理,直接丢弃任务
  • DiscardOldestPolicy:丢掉阻塞队列中存档时间最久的任务,执行当前任务
  • CallerRunsPolicy:使用调用者所在线程执行任务,除非当前线程关闭这个任务才会被丢弃

创建线程池后,提交任务的执行过程是怎么样的呢?下面看一下execute源码:

  public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
  • 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步;
  • 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
  • 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给4饱和策略进行处理

三 线程池关闭

关闭线程池,可以通过shutdownshutdownNow这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdownshutdownNow还是有不一样的地方:

  1. shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
  2. shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程

可以看出shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated方法才会返回true。

四 如何合理的配置线程池参数

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。

CPU密集型任务(一个完整请求,I/O操作可以在很短时间内完成, CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分):配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。原因是

计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。

IO密集型任务(I/O 操作占比很大部分):由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。(https://www.jianshu.com/p/f30ee2346f9f)公式里面的cpu利用率的解释应该不够确切,cpu利用率=cpu耗时/(cpu耗时+io耗时)

最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时))

按照上面公式,假如几乎全是 I/O耗时,所以纯理论你就可以说是 2N(N=CPU核数)

混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。

我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
https://www.jianshu.com/p/125ccf0046f3

五 BlockingQueue中take,poll的区别,put,offer的区别

取队首位对象take,poll,remove,若BlockingQueue为空时,结果不一致

  • take

阻断进入等待状态直到Blocking有新的对象被加入为止

  • remove

抛出异常

  • Poll

可以等time参数的时间,取不到返回null

添加对象、put,add,offer,当队列满,结果不一致情况

  • put

阻塞到有空间再继续

  • offer

如果可以添加,返回true,否则返回false

  • add

如果可以添加,返回true,否则抛出异常

六 JDK 提供的线程池及使用场景

JDK 为我们内置了五种常见线程池的实现,均可以使用 Executors 工厂类创建。

1.newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

不招外包,有固定数量核心成员的正常互联网团队。

可以看到,FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。

此外 keepAliveTime 为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义了)。

而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。

因此这个线程池执行任务的流程如下:

  1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
  2. 线程数等于核心线程数后,将任务加入阻塞队列 
    • 由于队列容量非常大,可以一直加加加
  3. 执行完任务的线程反复去队列中取任务执行

FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。

2.newSingleThreadExecutor


public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

不招外包,只有一个核心成员的创业团队。

从参数可以看出来,SingleThreadExecutor 相当于特殊的 FixedThreadPool,它的执行流程如下:

  1. 线程池中没有线程时,新建一个线程执行任务
  2. 有一个线程以后,将任务加入阻塞队列,不停加加加
  3. 唯一的这一个线程不停地去队列里取任务执行

听起来很可怜的样子 - -。

SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

3.newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

全部外包,没活最多待 60 秒的外包团队。

可以看到,CachedThreadPool 没有核心线程,非核心线程数无上限,也就是全部使用外包,但是每个外包空闲的时间只有 60 秒,超过后就会被回收。

CachedThreadPool 使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存。

因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。

它的执行流程如下:

  1. 没有核心线程,直接向 SynchronousQueue 中提交任务
  2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
  3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜

由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。

4.newScheduledThreadPool


public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

定期维护的 2B 业务团队,核心与外包成员都有。

ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor, 最多线程数为 Integer.MAX_VALUE ,使用 DelayedWorkQueue 作为任务队列。

ScheduledThreadPoolExecutor 添加任务和执行任务的机制与ThreadPoolExecutor 有所不同。

ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:

  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

它俩的代码如下:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      -unit.toNanos(delay),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

可以看到,这两种方法都是创建了一个 ScheduledFutureTask 对象,调用 decorateTask() 方法转成 RunnableScheduledFuture 对象,然后添加到队列中。

看下 ScheduledFutureTask 的主要属性:

private class ScheduledFutureTask<V>
        extends FutureTask<V> implements RunnableScheduledFuture<V> {
 
    //添加到队列中的顺序
    private final long sequenceNumber;
    //何时执行这个任务
    private volatile long time;
    //执行的间隔周期
    private final long period;
    //实际被添加到队列中的 task
    RunnableScheduledFuture<V> outerTask = this;
    //在 delay queue 中的索引,便于取消时快速查找
    int heapIndex;
    //...
}

DelayQueue 中封装了一个优先级队列,这个队列会对队列中的 ScheduledFutureTask 进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的顺序 sequenceNumber ,先提交的先执行。

ScheduledThreadPoolExecutor 的执行流程如下:

  1. 调用上面两个方法添加一个任务
  2. 线程池中的线程从 DelayQueue 中取任务
  3. 然后执行任务

具体执行任务的步骤也比较复杂:

  1. 线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask 
    • DelayQueue.take()
  2. 执行完后修改这个 task 的 time 为下次被执行的时间
  3. 然后再把这个 task 放回队列中 
    • DelayQueue.add()

ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

如何选择?

  • CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
  • FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
  • SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
  • ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

两种提交任务的方法

ExecutorService 提供了两种提交任务的方法:

  1. execute():提交不需要返回值的任务
  2. submit():提交需要返回值的任务

execute

execute() 的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功。

ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
    @Override
    public void run() {
        //do something
    }
});

submit

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

submit() 有三种重载,参数可以是 Callable 也可以是 Runnable

同时它会返回一个 Funture 对象,通过它我们可以判断任务是否执行成功。

获得执行结果调用 Future.get() 方法,这个方法会阻塞当前线程直到任务完成。

提交一个 Callable 任务时,需要使用 FutureTask 包一层:

FutureTask futureTask = new FutureTask(new Callable<String>() {    //创建 Callable 任务
    @Override
    public String call() throws Exception {
        String result = "";
        //do something
        return result;
    }
});
Future<?> submit = executor.submit(futureTask);    //提交到线程池
try {
    Object result = submit.get();    //获取结果
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

23. J.U.C下的常见类的使用

J.U.C:java.util.concurrent包。

一 J.U.C常用类

1 CountDownLatch

构造器:

 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

功能:count个线程执行完成后再执行后面的线程

方法:

用法:

public class Test {
     public static void main(String[] args) {   
         final CountDownLatch latch = new CountDownLatch(2);
          
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
          
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                     Thread.sleep(3000);
                     System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                     latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
          
         try {
             System.out.println("等待2个子线程执行完毕...");
            latch.await();
            System.out.println("2个子线程已经执行完毕");
            System.out.println("继续执行主线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     }
}

2 Semaphore

构造器:

public Semaphore(int permits) {          //参数permits表示许可数目,即同时可以允许多少线程进行访问
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

作用:

Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

方法:

public void acquire() throws InterruptedException {  }     //用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
public void acquire(int permits) throws InterruptedException { }    //获取permits个许可
public void release() { }          //在释放许可之前,必须先获获得许可。
public void release(int permits) { }    //释放permits个许可

// 这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:
public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

//获取可用许可数
public int availablePermits() {
        return sync.getPermits();
    }

应用:

假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

public class Test {
    public static void main(String[] args) {
        int N = 8;            //工人数
        Semaphore semaphore = new Semaphore(5); //机器数目
        for(int i=0;i<N;i++)
            new Worker(i,semaphore).start();
    }
     
    static class Worker extends Thread{
        private int num;
        private Semaphore semaphore;
        public Worker(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }
         
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("工人"+this.num+"占用一个机器在生产...");
                Thread.sleep(2000);
                System.out.println("工人"+this.num+"释放出机器");
                semaphore.release();           
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3 CyclicBarrier

构造函数:

 public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
  public CyclicBarrier(int parties) {
        this(parties, null);
    }

作用:循环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做循环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

参数parties指让多少个线程或者任务等待至barrier状态;

参数barrierAction为当这些线程都达到barrier状态时,会选择一个最后执行完任务的线程来执行barrierAction中的内容。

重要方法:

public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;

第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务

用法:

假若有若干个线程都要进行写数据操作,并且只有所有线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了:

public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处理其他任务...");
        }
    }
}

执行结果:

线程Thread-0正在写入数据...
线程Thread-3正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...

       从上面输出结果可以看出,每个写入线程执行完写数据操作之后,就在等待其他线程写入操作完毕。

  当所有线程线程写入操作完毕之后,所有线程就继续进行后续的操作了。

  如果说想在所有线程写入操作完之后,进行额外的其他操作可以为CyclicBarrier提供Runnable参数。

另外CyclicBarrier是可以重用的,看下面这个例子:

public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
         
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
         
        try {
            Thread.sleep(25000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
         
        System.out.println("CyclicBarrier重用");
         
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
             
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有线程写入完毕,继续处理其他任务...");
        }
    }
}

   执行结果:

线程Thread-0正在写入数据...
线程Thread-1正在写入数据...
线程Thread-3正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-0写入数据完毕,等待其他线程写入完毕
Thread-0所有线程写入完毕,继续处理其他任务...
Thread-3所有线程写入完毕,继续处理其他任务...
Thread-1所有线程写入完毕,继续处理其他任务...
Thread-2所有线程写入完毕,继续处理其他任务...
CyclicBarrier重用
线程Thread-4正在写入数据...
线程Thread-5正在写入数据...
线程Thread-6正在写入数据...
线程Thread-7正在写入数据...
线程Thread-7写入数据完毕,等待其他线程写入完毕
线程Thread-5写入数据完毕,等待其他线程写入完毕
线程Thread-6写入数据完毕,等待其他线程写入完毕
线程Thread-4写入数据完毕,等待其他线程写入完毕
Thread-4所有线程写入完毕,继续处理其他任务...
Thread-5所有线程写入完毕,继续处理其他任务...
Thread-6所有线程写入完毕,继续处理其他任务...
Thread-7所有线程写入完毕,继续处理其他任务...

在初次的4个线程越过barrier状态后,又可以用来进行新一轮的使用。而CountDownLatch无法进行重复使用。

下面对上面说的三个辅助类进行一个总结:

  1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

  2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

https://www.cnblogs.com/dolphin0520/p/3920397.html

4 Callable,Future,FutureTask

创建线程有两种方式,继承Thread,实现Runable接口,这两种方式在执行完任务之后无法获取结果。

jdk1.5后提供了Callable和Future来获取执行结果。

@FunctionalInterface
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;
}

应用:

一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);//较少使用
Future<?> submit(Runnable task);

Future:

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future提供了三种功能:

  1)判断任务是否完成;

  2)能够中断任务;

  3)能够获取任务执行结果。

 因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

FutureTask:实现了RunableFuture接口,而RunableFuture集成Runable和Future接口,所以它既可以作为Runable被线程执行,也可以作为Future获取Callable返回值。事实上,FutureTask是Future接口的一个唯一实现类。

用法:

Callable+Future获取多线程的执行结果。

public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}
public class FutureDemo {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        executor.shutdown();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("主线程在执行任务");

        try {
            System.out.println("task运行结果"+result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("所有任务执行完毕");
    }
}

class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++){
            sum += i;
        }
        return sum;
    }
}

使用Callable+FutureTask获取多线程的执行结果。

public class FutureTask {

    public static void main(String[] args) {
        //第一种方式
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();

        //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();*/

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("主线程在执行任务");

        try {
            System.out.println("task运行结果"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("所有任务执行完毕");
    }
}


class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++){
            sum += i;
        }
        return sum;
    }
}

Future和FutureTask的区别:Future是接口,只能作为结果返回。FutureTask是类,可以实例化。

5 ReentratLock && Condition

示例:

为空时阻塞take,满时,阻塞put

package sync;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created with IntelliJ IDEA.
 * User: ASUS
 * Date: 14-9-1
 * Time: 下午9:20
 * To change this template use File | Settings | File Templates.
 */
public class ConditionBoundedBuffer<T> {

    protected final Lock lock = new ReentrantLock();

    //条件谓词:notFull
    private final Condition notFull = lock.newCondition();
    //条件谓词:notEmpty
    private final Condition notEmpty = lock.newCondition();
    private final T[] items;
    private int tail, head, count;

    protected ConditionBoundedBuffer(int size) {
        items = (T[]) new Object[size];
    }

    /**
     * 阻塞并直到notFull
     *
     * @param x
     * @throws InterruptedException
     */
    public void put(T x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                // 阻塞,等待非满条件
                System.out.println("not full await");
                notFull.await();
            }

            items[tail] = x;
            if (++tail == items.length) {
                tail = 0;
            }

            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }


    /**
     * 阻塞并直到notEmpty
     *
     * @return
     * @throws InterruptedException
     */
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                // 阻塞,等待非空条件
                System.out.println("not empty await");
                notEmpty.await(); //现在有界缓存为空,要等到非空状态才能取出元素
            }

            T x = items[head];
            items[head] = null;
            if (++head == items.length) {
                head = 0;
            }
            --count;
            notFull.signal(); //元素已被取出,通知非满状态
            return x;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String args[]) {

        final ConditionBoundedBuffer buffer = new ConditionBoundedBuffer(10);

        //线程t2打印缓存中的消息
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println(buffer.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        //线程t1放入缓存消息
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    try {
                        buffer.put(new String("sadsasd"));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        t2.start();
        t1.start();
    }
}

二 lock实现

Lock是一个接口,下面以ReentranteLock为例进行介绍。

Lock存储结构:一个int类型状态值(用于锁状态更新),一个双向链表(用于存储等待中的线程)

Lock获取锁的过程:通过CAS来获取状态值,如果当场没有获取到,会将线程加入到线程等待链表中,如果获取到,更新数据,并修改状态值。

Lock释放锁的过程:修改状态值,调整等待列表

Lock大量使用CAS+自旋,因此建议使用在锁冲突很低的情况下,在非必要情况下,建议使用Synchronized做同步

24.  False Sharing,Cache Line

伪共享和缓存行

25.  线程池分类

线程池的创建是由Executor类提供的。

一 Executors.newFixedThreadPool(3);

构造函数:

   public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

创建线程数量固定的线程池,因为corePoolSize=MaxPoolSize,因此当没有线程空闲时,放到等待队列中

二 Executors.newCachedThreadPool();
构造函数

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

缓存一般都离不开过期时间。同步队列没有容量,每次插入数据时,必须等消费完成。

当有新任务来时,线程池没有线程,只能创建新线程来完成任务,处理完成后,缓存60s,线程过期前有新任务来时使用缓存线程,否则新建线程执行任务,由于线程池大小为Integer.MAX_VALUE,可能会造成系统瘫痪。

三 Executors.newScheduledThreadPool(5);

构造函数

public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

代码示例:

package test;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorTest {
 public static void main(String[] args) {
  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
   //延迟3s执行
   scheduledThreadPool.schedule(new Runnable() {
   public void run() {
    System.out.println("delay 3 seconds");
   }
  }, 3, TimeUnit.SECONDS);
 }
}

四 Executors.newSingleThreadExecutor();

构造函数:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

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

示例代码如下:


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExecutorTest {
 public static void main(String[] args) {
  ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  for (int i = 0; i < 10; i++) {
   final int index = i;
   singleThreadExecutor.execute(new Runnable() {
    public void run() {
     try {
      System.out.println(index);
      Thread.sleep(2000);
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }
   });
  }
 }
}

猜你喜欢

转载自blog.csdn.net/u013984781/article/details/102454672