JUC并发编程

文章目录

一、线程与进程

1. 进程/线程是什么?

进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

通俗的来说:

  • 进程:进程就是后台运行的一个程序,进程与操作系统有关。
  • 线程:一个进程由多个线程来组成。

2. 进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;

  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

3. 线程状态

Thread.State 枚举类:

public enum State {
    /**
     * 尚未启动的线程的线程状态(刚new出来,未调用start方法)。
     */
    NEW,

    /**
     * 可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统的其他资源,如处理器。
     * (调用的start方法,等待操作系统的调度)
     */
    RUNNABLE,

    /**
     * 等待监视器锁定时被阻止的线程的线程状态。
     * 
     * 处于阻塞状态的线程正在等待监视器锁进入同步块/方法,或在调用Object.wait()后重新输入同步块/方法。
     * 
     * 线程的 blocked状态往往是无法进入同步方法/代码块来完成的。这是因为无法获取到与同步方法/代码块相关联的锁。
     */
    BLOCKED,

    /**
     * 等待线程的线程状态
     * 
     * 由于调用以下方法之一,线程处于等待状态
     * Object.wait()
     * Thread.join()
     * LockSupport.park()
     *
     * 处于等待状态的线程正在等待另一个线程执行特定操作
     *
     * 进入wating状态的线程等待唤醒(notify或notifyAll)才有机会获取cpu的时间片段来继续执行。
     */
    WAITING,

    /**
     * 具有指定等待时间的等待线程的线程状态。
     * Thread.sleep
     * Object.wait(long)
     * Thread.join(long)
     * LockSupport.parkNanos
     * LockSupport.parkUntil
     */
    TIMED_WAITING,

    /**
     * 线程已完成执行
     */
    TERMINATED;
}
  • waitingblocked 的关系:
    wating 状态相关联的是等待队列,与 blocked 状态相关的是同步队列,一个线程由等待队列迁移到同步队列时,线程状态将会由 wating 转化为 blocked。可以这样说,blocked 状态是处于 wating 状态的线程重新焕发生命力的必由之路。

在这里插入图片描述

4. wait()/sleep()的区别?

wait/sleep功能都是当前线程暂停,有什么区别?

  • wait():放开手去睡,放开手里的锁
  • sleep():握紧手去睡,醒了手里还有锁

5. 什么是并发?什么是并行?

  • 并发(concurrent):同一时刻多个线程在访问同一个资源,多个线程对一个点。例子:小米9今天上午10点,限量抢购,春运抢票 ,电商秒杀…

  • 并行(parallel):多项工作一起执行,之后再汇总。例子:泡方便面,电水壶烧水,一边撕调料倒入桶中

6. notify、notifyAll、LockSupport.unpark(Thread)的区别

  • notify()只有一个等待线程会被唤醒,而且它不能保证哪个线程会被唤醒,这取决于线程调度器。
  • notifyAll():等待该锁的所有线程都会被唤醒,且所有被唤醒的线程都将争夺锁,因此 wait() 要放在循环里。
  • LockSupport.unpark(Thread):唤醒指定的线程

二、锁

  1. 低耦合高内聚:线程、操作(由资源类对外暴露的调用方法)、资源类
  2. 先判断(while判断),再干活(执行业务),最后通知(notifyAll())
  3. 防止虚假唤醒(判断只能用 while,不能用 if

1. 对象锁与类锁

(1)对象锁

在对象锁中的同步代码块,单个实例中,只能有一个线程执行这些代码块,各实例之间执行互不影响。

  1. 锁是非静态的
private Object lock = new Object();

public void test(){
    synchronized (lock){
        System.out.println("do some thing");
    }
}
  1. synchronized 修饰非静态方法
public synchronized void test(){
        System.out.println("do some thing");
}
  1. 使用 this 对象
public void test(){
    synchronized (this){
        System.out.println("do some thing");
    }
}

(2)类锁

类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的。

  1. 锁是静态的
private static Object lock = new Object();

public void test(){
    synchronized (lock){
        System.out.println("do some thing");
    }
}
  1. synchronized 修饰静态方法
public static synchronized void test(){
        System.out.println("do some thing");
}
  1. 使用 xxx.class 类当锁
class Test{
    public void test(){
        synchronized (Test.class) {
            System.out.println("do some thing");
        }
    }
}

2. ReentrantLock(可重入锁)

class Counter {
    private Integer number = 0;
    //创建一个 ReentrantLock
    Lock lock = new ReentrantLock();
    //可以定义多个Condition,唤醒指定condition中等待的线程(详见例子3)
    Condition condition = lock.newCondition();

    public void increment() throws InterruptedException {
    	//获取锁
        lock.lock();
        //获取锁后要紧跟 try,防止中间的代码报错后造成死锁
        try {
        	//判断线程执行的条件,要使用 while 判断,不能使用 if
            while (number != 0) {
                condition.await();
            }
			//执行业务逻辑
            number++;
            System.out.println(Thread.currentThread().getName() + ": " + number);

			//唤醒其它线程
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
        	//解锁要在finally的第一行写
            lock.unlock();
        }
    }

    public synchronized void descrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + ": " + number);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3. ReadWriteLock(读写锁)

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。

  • 读 + 读:相当于无锁,锁会在redis中记录好,都会加锁成功
  • 读 + 写:有读锁,写需要等待
  • 写 + 读:有写锁,读需要等待
  • 写 + 写:阻塞方式

例子:自定义一个缓存类,模拟读写

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 0; i < 20; i++) {
            final int z = i;
            new Thread(() -> {
                myCache.put(String.valueOf(z), String.valueOf(z));
                myCache.get(String.valueOf(z));
            }, String.valueOf(i)).start();
        }
    }
}

class MyCache {
    private Map<String, Object> cache = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key, Object val) {
        readWriteLock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + "开始写入");
            cache.put(key, val);
            System.out.println(Thread.currentThread().getName() + "写入完成:" + key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始读取");
            cache.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完成:" + key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

三、线程安全类

1. ArrayList 不安全

当并发往 ArraysList 中添加数据时,会报 java.util.ConcurrentModificationException 异常
解决办法:

  1. 使用 Vector,本质是在修改方法上加了 synchronized
  2. 使用 Collections.synchronizedList() 将不安全的 List 转为安全的 List,本质也是在方法上加了 synchronized
  3. 使用 CopyOnWriteArrayList,底层使用的是 ReentrantLock

CopyOnWriteArrayList 写时复制

CopyOnWriteArrayList 添加元素时的原码:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 获取锁,只能有一个线程写数据
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 将原数据复制一份,写数据的时候不影响其它线程从list中读数据
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 往新的数组中插入新的数据
        newElements[len] = e;
        // 用新的数组替换旧的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

2. HashSet不安全

  1. Collections.synchronizedSet() 方法将不安全的 Set 转为安全的 Set。
  2. 使用 CopyOnWriteArraySet,底层使用的是 CopyOnWriteArrayList

3. HashMap不安全

  1. Collections.synchronizedMap() 方法将不安全的 Map 转为安全的 Map。
  2. 使用 ConcurrentHashMap

四、实现Callable接口创建多线程

1. Future接口

Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。

Future 一般由 ExecutorServicesubmit()invokeAll() 方法返回的,用于跟踪、获取任务在线程池中的运行情况、等待运算结果,还可以取消任务。(还有其子接口 ScheduleFuture 则由 ScheduleExecutorServiceschedule() 等方法返回);

2. Callable接口

Callable 位于 java.util.concurrent 包下,它也是一个接口,在它里面声明了 call() 方法,只不过这个方法叫做,这是一个泛型接口,call() 函数返回的类型就是传递进来的 V 类型。Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。要获取返回结果时可以调用 get(),该方法会阻塞直到任务返回结果。因为 Future 只是一个接口,所以是无法直接用来创建对象使用的,因此就有了 FutureTaskFutureTask 类实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口和 Future 接口,所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

因此当我们想通过一个线程运行 Callable,但是 Thread 不支持构造方法中传递 Callable 的实例,我们需要通过 FutureTask 把一个 Callable 包装成 Runnable,然后再通过这个 FutureTask 拿到 Callable 运行后的返回值。

//FutureTask(Callable<V> callable)
public static void main(String[] args) throws ExecutionException, InterruptedException {
    //范型是返回值类型
    Callable<String> callable = () -> {
        System.out.println("callable线程");
        return "666";
    };
    FutureTask<String> futureTask = new FutureTask<>(callable);

    new Thread(futureTask, "A").start();

    //调用get()方法会阻塞当前线程,直到Thread执行完毕
    System.out.println(futureTask.get());
}

FutureTask 也可以接收一个 Runable 接口和一个结果值 result,当 Runable 中的线程运行成功时,返回给定的结果值

// FutureTask(Runnable runnable, V result)
public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask<String> futureTask1 = new FutureTask<>(() -> {
        System.out.println(123);

    }, "success");
    new Thread(futureTask1).start();
    String s = futureTask1.get();
    System.out.println(s);
}

五、阻塞队列

阻塞队列是一个队列:
在这里插入图片描述

线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素 :

  • 当队列是空的,从队列中获取元素的操作将会被阻塞;试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素

  • 当队列是满的,从队列中添加元素的操作将会被阻塞;试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

阻塞队列的用处:

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。BlockingQueue 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了。在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

1. 常用队列

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE )阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
  • LinkedTransferQueue:由链表组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表组成的双向阻塞队列。

继承关系:
在这里插入图片描述

2. 通用方法

方法类型抛出异常特殊值阻塞超时
插入add()offer()put()offer(e, timeout, unit)
移除remove()poll()take()poll(timeout, unit)
检查(查看队首元素)element()peek()不可用不可用
名词队列满时,插入队列空时,移除队列空时,检查
抛出异常抛 IllegalStateException: Queue full 异常抛 NoSuchElementException 异常抛 NoSuchElementException 异常
特殊值返回 false返回 null返回 null
阻塞阻塞,直到队列不满阻塞,直到队列不为空
超时阻塞,直到可以插入值(返回true)或者超时(返回false)阻塞,直到可以获取值或者超时(返回null)

六、线程池(Executor)

1. 线程池的优势

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用;控制最大并发数;管理线程。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

依赖关系
在这里插入图片描述

2. 线程池七大参数

  1. corePoolSize:线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于 corePoolSize。如果当前线程数为 corePoolSize,继续提交的任务被保存到队列中,等待被执行。
    1. 如果执行了线程池的 prestartAllCoreThreads() 方法,线程池会提前创建并启动所有核心线程。
    2. 核心线程数默认空闲时也不会被销毁,可以通过 allowCoreThreadTimeOut 参数来设置
  2. maximumPoolSize:线程池中能够容纳同时执行的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于 maximumPoolSize
  3. keepAliveTime:多余的空闲线程的存活时间。当前池中线程数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 时,多余线程会被销毁直到只剩下 corePoolSize 个线程为止。默认情况下,该参数只在线程数大于 corePoolSize 时才有用。
  4. unit:keepAliveTime 的单位
  5. workQueue:用于保存等待执行任务的阻塞队列,当线程池中的线程数超过它的 corePoolSize 的时候,线程会进入阻塞队列进行等待。一般来说,我们应该尽量使用有界队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
  6. threadFactory:创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,还可以将所有的线程设置为守护线程。Executors 静态工厂里默认的 ThreadFactory,线程的命名规则是"pool-数字-thread-数字"。
  7. handler:线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    a)AbortPolicy:直接抛出异常,默认策略。
    b)CallerRunsPolicy:用调用者所在的线程来执行任务。
    c)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务。
    d)DiscardPolicy:直接丢弃任务。
    当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

3. 线程池工作机制

在这里插入图片描述
1)如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务。
2)如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue
3)如果无法将任务加入 BlockingQueue(有界队列已满),则创建新的线程来处理任务。
4)如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution() 方法。

4. 提交任务

Java线程池中有2种方式提交任务:execute()submit(),主要区别如下:

  1. execute() 提交的是 Runnable 类型的任务,而 submit() 提交的是 Callable 或者 Runnable 类型的任务。

  2. execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit() 方法用于提交需要返回值的任务,线程池会返回一个 Future 类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

  3. execute() 提交的时候,如果有异常,就会直接抛出异常,而 submit() 在遇到异常的时候,通常不会立马抛出异常,而是会将异常暂时存储起来,等待你调用 Future.get() 方法的时候,才会抛出异常。

5. 关闭线程池

可以通过调用线程池的 shutdown()shutdownNow() 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt() 方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow() 首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown() 只是将线程池的状态设置成SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。只要调用了这两个关闭方法中的任意一个,isShutdown() 方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed() 方法会返回 true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown() 方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow() 方法。

6. 合理地配置线程池参数

首先,不管从哪个角度来分析,都建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如5000。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存(OOM),导致整个系统不可用。

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:

(1)任务的性质

CPU密集型任务、IO密集型任务和混合型任务。

a)CPU密集型任务(大部份时间用来做计算、逻辑判断等操作)应配置尽可能小的线程,如核心线程数为 服务器CPU个数+1。服务器 CPU 线程数可以通过Runtime.getRuntime().availableProcessors() 方法获得。

b)由于IO密集型任务(大部分的状况是CPU在等I/O 的读/写操作)线程并不是一直在执行任务,则应配置尽可能多的线程,如 服务器CPU个数的2倍。对于IO型的任务的最佳线程数,有个公式可以计算:

Nthreads = NCPU * UCPU * (1 + W/C)

其中:
NCPU是处理器的核的数目
UCPU是期望的CPU利用率(该值应该介于01之间)
W/C是等待时间与计算时间的比率。等待时间与计算时间我们在Linux下使用相关的vmstat命令或者top命令查看。

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

(2)任务的优先级

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。

(3)任务的执行时间

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

(4)任务的依赖性

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

7. 示例

1. JDK预定义3种线程池

在JDK1.8中,已经预定义了5种线程池,除了WorkStealingPool是JDK1.8新加入的,其余4个在JDK1.5就有了。5种预定义线程池的使用都非常简单,就不一一用代码演示了,下面分别简单的介绍:

(1)FixedThreadPool

创建使用固定线程数的 FixedThreadPool 的API。适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

参数说明
线程池类型ThreadPoolExecutor
corePoolSize创建时自己指定
maximumPoolSize与 corePoolSize 相同
keepAliveTime0L多余的空闲线程会被立即终止
BlockingQueueLinkedBlockingQueue (容量为Integer.MAX_VALUE)

创建方法:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
(2)SingleThreadExecutor

创建使用单个线程的 SingleThreadExecutorAPI,于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。

参数说明
线程池类型ThreadPoolExecutor
corePoolSize1
maximumPoolSize1
keepAliveTime0L多余的空闲线程会被立即终止
BlockingQueueLinkedBlockingQueue (容量为Integer.MAX_VALUE)

创建方法:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
(3)CachedThreadPool

创建一个会根据需要创建新线程的 CachedThreadPool 的API。大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。

参数说明
线程池类型ThreadPoolExecutor
corePoolSize0
maximumPoolSizeInteger.MAX_VALUE可以理解为最大线程数没有限制
keepAliveTime60s
BlockingQueueSynchronousQueueSynchronousQueue 容量为1,这意味着如果主线程提交任务的速度高于线程池中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

创建方法:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
(4)ScheduledThreadPool

适用于需要多个后台线程执行延时或者周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。

参数说明
线程池类型ScheduledThreadPoolExecutor
corePoolSize创建时自己指定
maximumPoolSizeInteger.MAX_VALUE可以理解为最大线程数没有限制
keepAliveTime0多余的空闲线程会被立即终止
BlockingQueueDelayedWorkQueue

ScheduledThreadPool 中常见的方法:

public static void main(String[] args) throws InterruptedException {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    //延时5s后执行
    scheduledExecutorService.schedule(() -> {
        System.out.println("执行成功1");
    }, 5, TimeUnit.SECONDS);
    //延时5s执行,然后以 (上一次任务执行开始的时间 + 2s)为周期执行
    scheduledExecutorService.scheduleAtFixedRate(() -> {
        try {
            Thread.sleep(1000);
            System.out.println("执行成功2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, 5, 2, TimeUnit.SECONDS);
    //延时5s执行,然后以 (上一次任务执行完毕的时间 + 2s)为周期执行
    scheduledExecutorService.scheduleWithFixedDelay(() -> {
        try {
            Thread.sleep(1000);
            System.out.println("执行成功3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, 5, 2, TimeUnit.SECONDS);

    System.out.println("main执行完毕");
}
(5)SingleThreadScheduledExecutor

等价于只有一个线程的 ScheduledThreadPool

(6)WorkStealingPool

创建一个 ForkJoinPool 线程池

参数说明
线程池类型ForkJoinPool(详见第七章 ForkJoin)
parallelismRuntime.getRuntime().availableProcessors()默认值为电脑的核心数
asyncModetrue使用异步调用

2. 自定义线程池

阿里巴巴规范中:

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPoolSingleThreadPool:
  允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool:
  允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

public static void main(String[] args) {

    ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), new ThreadPoolExecutor.AbortPolicy());

    try {
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName());
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        executor.shutdown();
    }

}

七、ForkJoin(分支合并)

参考:https://www.cnblogs.com/hongshaodian/p/12452105.html

1. 原理

ForkJoin 采用的是分而治之。分而治之思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。分而治之的策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为m个规模较小的子问题,这些子问题互相独立且与原问题形式相同,多个线程递归地解这些子问题,然后将各子问题的解合并得到原问题的解,这种算法设计策略叫做分治法。用一张图来表示 ForkJoin 原理。

在这里插入图片描述

2. 工作窃取

工作窃取是指当前线程的 Task 已经全被执行完毕,则自动取到其他线程的 Task 队列中取出 Task 继续执行。ForkJoinPool 中维护着多个线程在不断地执行 Task,每个线程除了执行自己职务内的 Task 之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的 Task,如此一来就能能够减少线程阻塞或是闲置的时间,提高 CPU 利用率。

在这里插入图片描述

3. ForkJoin的使用

  1. 创建一个 ForkJoin 任务,继承 ForkjoinTask 的子类 RecursiveTask<V>(有返回值) 或者 RecursiveAction(无返回值)
  2. ForkJoin 任务中,实现 compute() 方法,在任务中执行 forkjoin 的操作机制。
  3. 创建 ForkJoinPool 线程池,使用线程池执行 ForkJoin 任务,并获取返回结果
/**
 * 继承 ForkjoinTask 的子类,提供执行 fork 和 join 的操作机制
 * RecursiveTask<V>:有返回值,返回值类型为范型V
 * RecursiveAction:没有返回值
 */
class MyTask extends RecursiveTask<Integer> {
    private static final int ADJUST_VALUE = 10;

    private int begin;
    private int end;
    private int result;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (end - begin <= ADJUST_VALUE) {
            // 当左右的差值小于等于10时,计算和
            for (int i = begin; i <= end; i++) {
                result += i;
            }
        } else {
            // 当左右的差值大于10时,对半分成小任务
            int middle = (end + begin) / 2;
            MyTask task01 = new MyTask(begin, middle);
            MyTask task02 = new MyTask(middle + 1, end);
            // 使用 fork() 方法执行分裂的小任务
            task01.fork();
            task02.fork();
            // 将任务1与任务2的执行结果拼起来
            result = task01.join() + task02.join();
        }
        return result;
    }
}

public class ForkJoinDemo {
    public static void main(String[] args) {
        // 创建任务,从 0 加到 100
        MyTask myTask = new MyTask(0, 100);

        // 使用 ForkJoinPool 线程池执行,默认的线程数是cpu核数(Runtime.getRuntime().availableProcessors(),最大不超过32767)
        ForkJoinPool forkJoinPool = new ForkJoinPool();

        try {
            //提交任务
            ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
            //获取结果
            System.out.println(forkJoinTask.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            //关闭线程池
            forkJoinPool.shutdown();
        }
    }
}

八、CompletableFuture(异步编程)

参考:JDK1.8新特性CompletableFuture总结

一个 completetableFuture 就代表了一个任务。他能用 Future 的方法。还能做一些之前说的 executorService 配合 futures 做不了的。

之前 future 需要等待 isDonetrue 才能知道任务跑完了。或者就是用 get() 方法调用的时候会出现阻塞。而使用 completableFuture 的使用就可以用 thenwhen 等等操作来防止以上的阻塞和轮询 isDone 的现象出现。

1. 使用方法

public static void main(String[] args) throws ExecutionException, InterruptedException {

 	// runAsync() 没有返回值,supplyAsync() 有返回值
    // 可以指定第二个参数 Executor,表示在哪个线程池中执行
    CompletableFuture.runAsync(() -> {
        // int i = 4 / 0;
        System.out.println("runAsync()正在执行");
    }).whenCompleteAsync((t, u) -> {
        //异步执行没有报错时,执行这里
        System.out.println("runAsync()执行成功");
    }).exceptionally(e -> {
        //报错时,执行这里
        System.out.println(e.getMessage());
        System.out.println("runAsync()执行失败");
        return null;
    })
    		//ompletableFuture这套使用异步任务的操作都是创建成了守护线程
            //这里要调用get()方法阻塞主线程,防止主线程执行完毕守护线程退出
            .get();

    System.out.println("=========");

    //有返回值
    String s = CompletableFuture.supplyAsync(() -> {
        // int i = 4 / 0;
        return "123";
    }).whenCompleteAsync((res, exception) -> {
    	// res 是上一步执行成功的结果,exception 是上一步执行出错时抛出的异常
        System.out.println(res);
        System.out.println(exception);
        //虽然可以得到异常信息,但是无法修改返回数据。
    }).exceptionally(f -> {
    	//可以感知异常并返回默认值
        System.out.println(f.getMessage());
        return "456";
    })
    		//ompletableFuture这套使用异步任务的操作都是创建成了守护线程
            //这里要调用get()方法,防止主线程执行完毕守护线程退出
            .get();

    System.out.println("---------" + s);
}

注意completableFuture 这套使用异步任务的操作都是创建成了守护线程。那么我们没有调用 get() 方法不阻塞这个主线程的时候。主线程执行完毕。所有线程执行完毕就会导致一个问题,就是守护线程退出。

2. 常用方法

  1. allOf/anyOf:这两个方法的入参是一个 completableFuture 数组、allOf() 就是所有任务都完成时返回。但是是个 Void 的返回值。anyOf() 是当入参的 completableFuture 组中有一个任务执行完毕就返回。返回结果是第一个完成的任务的结果。
  2. getNow():这个方法是执行这个方法的时候任务执行完了就返回任务的结果,如果任务没有执行完就返回你的入参。
  3. get():阻塞线程,等待返回值
  4. whenXXX,在一个任务执行完成之后调用的方法。
  5. whenComplete():当主线程执行到 whenComplete() 方法时,completableFuture 这个任务已经执行完毕了,那么就使用主线程调用 whenComplete();如果任务没有执行完毕,那么就会用完成 completableFuture 这个任务所使用的线程调用 whenComplete()。所以主线程有可能出线阻塞。传入的两个参数,分别是结果和异常。无返回值,不能修改原来的返回结果。
  6. whenCompleteAsync():这个方法就是新创建一个异步线程执行。所以不会阻塞。
  7. handle()/handleAsync():程序执行完的后续处理,不管是执行成功还是失败,有返回值 ,可以修改原来的返回结果。
  8. thenCompose():使用的是执行完上一个任务的线程,可以使用前面返回的结果接着执行,并返回新的结果.
  9. thenCombine():结合两个任务的结果。

3. 线程串行化

当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

只要上面的任务执行完成,就开始执行thenRun,无法获取上一步的执行结果
public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)
  • 带有 Async 默认是异步执行的。同之前。以上都要前置任务成功完成。
  • Function<? super T, ? extends U>
    • T:上一个任务返回结果的类型
    • U:当前任务的返回值类型

4. 任务组合

(1)两个任务必须都完成,才执行后续任务。

组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) 
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor)

组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有返回值。
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor)

组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后,处理该任务。
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor)
  • CompletionStage<?> other:另一个 CompletableFuture 任务
  • T:上一个任务返回结果的类型
  • Uother 任务的返回值类型
  • V:返回值类型

(2)两个任务有一个完成的时候,就执行后续任务

当两个任务中,任意一个 future 任务完成的时候,执行任务。

两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。
public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor)

两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。
public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action,  Executor executor)

两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返回值。
public CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor)
  • CompletionStage<?> other:另一个 CompletableFuture 任务
  • T:两个任务中先执行完的任务的返回结果类型
  • U:当前任务的返回值类型

(3)多个任务组合

等待所有任务完成,如果要获取结果,就分别调用参数的 get() 方法
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

只要有一个任务完成,返回一个新的CompletableFuture,结果是第一个完成任务的结果
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

注意:调用完 allOf()/anyOf() 方法后一定要调用 get() 方法,才能阻塞当前线程。

5. 异步编程中使用 ThreadLocal

public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
  	//构建OrderConfirmVo
    OrderConfirmVo confirmVo = new OrderConfirmVo();

    //获取当前用户登录的信息
    MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

    //获取当前线程请求信息(解决Feign异步调用丢失请求头问题)(底层使用的是 ThreadLocal)
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

    CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
        //因为是异步执行,所以新的线程没有主线程的 ThreadLocal
        //因此要先将主线程的 ThreadLocal 拿来放入到异步调用的线程中
        RequestContextHolder.setRequestAttributes(requestAttributes);
        //1、远程查询所有的收获地址列表
        List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
        confirmVo.setMemberAddressVos(address);
    });

    CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
        RequestContextHolder.setRequestAttributes(requestAttributes);
        //2、远程查询购物车所有选中的购物项
        List<OrderItemVo> currentCartItems = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(currentCartItems);
    });

    CompletableFuture.allOf(getAddressFuture, cartFuture).get();

    return confirmVo;
}

九、辅助类

1. CountDownLatch

等班里的6个同学都走完后,再锁门。

public static void main(String[] args) throws InterruptedException {
    CountDownLatch count = new CountDownLatch(6);
    for (int i = 0; i < 6; i++) {
        new Thread(() ->{
            System.out.println(Thread.currentThread().getName() + "同学出教室");
            //每当一个线程执行完时,计数器减1
            count.countDown();
        }, String.valueOf(i)).start();
    }
    //count不为0时,当前线程等待
    count.await();
    System.out.println("锁门");
}

CountDownLatch 主要有两个方法,当一个或多个线程调用 await() 方法时,这些线程会阻塞。其它线程调用 countDown() 方法会将计数器减1(调用 countDown() 方法的线程不会阻塞),当计数器的值变为 0 时,因 await() 方法阻塞的线程会被唤醒,继续执行。

2. CyclicBarrier

每集到七颗龙珠,召唤一次神龙

CountDownLatch 是减,CyclicBarrier 是加,当等待的线程数达到指定的数量时,再执行相应的方法

public static void main(String[] args) throws InterruptedException {
	// 当等待的线程达到7时,才会执行后面的线程
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
        System.out.println("召换神龙");
    });
    for (int i = 0; i < 7; i++) {
        new Thread(() ->{
            System.out.println("收集到第 " + Thread.currentThread().getName() + " 颗龙珠");
            try {
            	// 线程等待,当等待的线程数到达指定的阀值时,才能继续往下执行
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName() + "执行完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }, String.valueOf(i)).start();
    }
}

CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程通过 CyclicBarrierawait()
方法进入屏障。

3. Semaphore(信号量)

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
在信号量上我们定义两种操作:

  • acquire(获取):当一个线程调用 acquire() 操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
  • release(释放):实际上会将信号量的值加1,然后唤醒等待的线程。

例子:4 个车位,10 辆车抢占

public static void main(String[] args) throws InterruptedException {
	//创建信号量,4个车位
    Semaphore semaphore = new Semaphore(4);
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try {
            	// 抢占车位,信号量减1,当抢不到时,会阻塞。
                semaphore.acquire();
                // 尝试抢车位,抢到返回true,抢不到返回false,不会阻塞
                //boolean b = semaphore.tryAcquire();
                System.out.println(Thread.currentThread().getName() + " 抢到车位");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println(Thread.currentThread().getName() + " 离开车位");
                // 释放占车位,信号量加1
                semaphore.release();
            }
        }, String.valueOf(i)).start();
    }
}

当信号量指定为 1 时(new Semaphore(1)),就和锁一样了。

十、例子

1. 卖票

  1. 低耦合高内聚:线程、操作(对外暴露的调用方法)、资源类
  2. 判断/干活/通知
  3. 防止虚假唤醒(判断只能用 while,不能用 if
class Ticket {//资源类
    //票
    private int number = 30;

    public synchronized void saleTicket() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "\t卖出第:" + (number--) + "\t还剩下:" + number);
        }
    }
}

/**
 * 题目:三个售票员   卖出   30张票
 * 多线程编程的企业级套路+模板
 * 1.在高内聚低耦合的前提下,线程    操作(对外暴露的调用方法)     资源类
 */
public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "A").start();

		//这样写不符合阿里规范:在if/else/for/while/do语句中必须使用大括号,即使只有一行代码
        new Thread(() -> { for (int i = 1; i <= 40; i++) ticket.saleTicket(); }, "B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "C").start();
    }
}

2. 计数器

定义一个变量,实现多个线程对该变量加1,多个线程对该变量-1,交替打印 “ 1, 0 ”,持续多轮

class Counter {

    private Integer number = 0;

    public synchronized void increment() throws InterruptedException {

        while (number != 0) {
            this.wait();
        }

        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);
        this.notifyAll();
    }

    public synchronized void descrement() throws InterruptedException {
        while (number == 0) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);
        this.notifyAll();
    }
}

public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {
        Counter counter = new Counter();
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    counter.increment();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    counter.descrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    counter.increment();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "C").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    counter.descrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "D").start();
    }
}

3. 线程的顺序执行

线程 A 打印 “A”,线程 B 打印 “B”,线程 C 打印 “C”,按照 ABCABC 的顺序打印多轮

class Print {

    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    int flag = 1;

    public void printA() {
        lock.lock();
        try {
            while (flag != 1) {
                condition1.await();
            }
            System.out.println("A");
            flag = 2;
            condition2.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (flag != 2) {
                condition2.await();
            }
            System.out.println("B");
            flag = 3;
            condition3.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            while (flag != 3) {
                condition3.await();
            }

            System.out.println("C");
            flag = 1;
            condition1.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadOrderAccess {

    public static void main(String[] args) {
        Print print = new Print();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                print.printA();
            }

        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                print.printB();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                print.printC();
            }
        }).start();
    }
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值