JUC并发编程

一、简介

1、JUC介绍

java.util.concurrent 包是在并发编程中使用的工具类,简称JUC,有以下三个包

java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks

JDK8官方在线文档:https://www.matools.com/api/java8

2、进程与线程

2.1 异同介绍

进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,它是操作系统动态执行的基本单元。在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

线程:通常在一个进程中可以包含若干个线程,一个进程中至少有一个线程,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

2.2 线程状态

线程有6种状态,可以进入Thread.State查看源码分析:

public enum State { 
      //线程刚创建       
      NEW,
      //在JVM中运行的线程
      RUNNABLE,
      //线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行
      BLOCKED,
      //等待状态
      WAITING,
      //调用sleep() join() wait()方法可能导致线程处于等待状态
      TIMED_WAITING,
      //线程执行完毕,已经退出
      TERMINATED;
}

2.3 线程wait/sleep区别

  • 两个方法来自不同的类
    sleep来自Thread类,wait来自Object类
  • 释放资源不同(有没有释放锁)
    sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。sleep(100L)占用cpu,线程休眠100毫秒后继续执行;而wait是无限期的除非用户主动notify()
  • 使用范围不同
    wait()、notify()和notifyAll()只能在同步控制方法或者同步控制块里面使用,而sleep()可以在任何地方使用
  • 是否需要捕获异常
    sleep()必须捕获异常,而wait()、notify()和notifyAll()不需要捕获异常。

3、并行与并发

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

并行(parallel):在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

二、Lock接口与synchronized

1、synchronized关键字

synchronized 是 Java 中的关键字,是一种同步锁(对方法或者代码块中存在共享数据的操作)。同步锁可以是任意对象

具体修饰的对象有3种方式

  • 修饰代码块
    被修饰的代码块称为同步语句块,作用的范围是大括号{ }内的内容
  • 修饰方法
    作用范围为整个方法,作用对象是调用该代码块的对象(虽然可以修饰方法,但 synchronized 并不属于方法定义的一部分,因此synchronized 关键字不能被继承)
  • 修饰静态方法
    作用的范围是整个静态方法,作用的对象是这个类的所有对象(修饰一个类也同理)
public class synchronizedTest implements Runnable{
    //共享资源(临界资源)
    static int i=0;
    /**
     * synchronized 修饰实例方法
     * 对静态方法加锁,锁是当前类的class对象锁
     */
    public static synchronized void add(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<10;j++){
            add();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        synchronizedTest b1=new synchronizedTest();
        synchronizedTest b2=new synchronizedTest();
        Thread m1=new Thread(b1);
        Thread m2=new Thread(b2);
        m1.start();
        m2.start();
        m1.join();
        m2.join();
        System.out.println(i);
    }
}

2、Lock接口

2.1 介绍

Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition

2.2 lock常用方法

lock()方法用来获取锁

  • 如果锁已被其他线程获取,则进行等待
  • 发生异常不会自动解锁,需用在 try{}catch{}块中进行

Condition 类也可以实现等待/通知模式

关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式;Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式

  • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行
  • signal()用于唤醒一个等待的线程
  • signalAll()用于唤醒所有等待的线程(推荐)

unlock()方法用于解锁(此方法必须在finally中,否则会造成死锁)

public interface Lock {
  //上锁
  void lock();
  void lockInterruptibly() throws InterruptedException;
  //尝试获取锁
  boolean tryLock();
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  //解锁
  void unlock();
  //可实现等待/通知模式
  Condition newCondition();
}

2.3 Lock实战

public class LockTest {
    //票数量
    private int number = 30;
    //创建可重入锁
    private final ReentrantLock lock = new ReentrantLock(true);

    //卖票方法
    public void sale() {
        //lock不能使用try-with-resource方法
        //上锁
        lock.lock();
        try {
            //判断是否有票
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockTest ticket = new LockTest();

            new Thread(()-> {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            },"shawn1").start();

            new Thread(()-> {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            },"shawn2").start();

            new Thread(()-> {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            },"shawn3").start();
    }
}

3、线程间的通信

对于synchronized 来说,自定义同步通信

public class LockTest {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    void test() {
        synchronized (lock1) {
            try {
                lock1.wait();
                //TODO
            } catch (InterruptedException e) {
            }finally {
                lock2.notify();
            }
        }
    }

}

对于Lock来说,可自定义同步通信

public class LockTest {
    //创建Lock锁
    private Lock lock = new ReentrantLock();

    //创建三个condition
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    
    //打印5次,参数第几轮
    public void print5(int loop) throws InterruptedException {
        //上锁
        lock.lock();
        try {
            c1.await();
            //TODO
            c2.signal(); //通知第二个线程
        }finally {
            //释放锁
            lock.unlock();
        }
    }

}

4、synchronized与lock的异同

  • synchronized是java关键字,内置;而lock不是内置,是一个类,可以实现同步访问且比synchronized中的方法更加丰富
  • synchronized不会手动释放锁,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁)
  • lock等待锁的线程会相应中断,而synchronized不会相应,只会一直等待
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
  • Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候)

三、多线程锁与并发

1、多线程锁结论

  • 对于普通同步方法,锁的是当前实例对象
    所有的非静态的同步方法用的都是同一把锁—实例对象本身。一个对象里面如果有多个synchronized方法,某个时刻内,只要一个线程去调用其中一个synchronized 方法,其他的线程都要等待。换句话说,在某个时刻内,只能有唯一一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法
  • 对于静态同步方法,锁的是当前的Class对象
    所有的静态同步方法用的也是同一把锁—类对象本身
  • 对于同步方法块,锁是synchronized括号里面的配置对象

2、并发同步

2.1 JMM介绍

JMM即为JAVA 内存模型(java memorymodel)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
JMM规定了内存主要划分为主内存工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性;多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

2.2 volatile

volitile 是 Java 虚拟机提供的轻量级的同步机制,三大特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

volatile 实现了禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

而使用volatile的另一好处是相比synchronized来说,volatile更加轻量,运行速度更快

2.3 JUC原子类工具

java.util.concurrent.atomic包下有很多使用了高效的机器级指令(而没有使用锁)来保证其他操作的原子性。例如AtomicInteger类提供了incrementAndGet()方法,它以原子方式将一个整数进行自增。具体查看JDK8文档

3、公平锁与非公平锁

  • 公平锁:效率相对低
  • 非公平锁:效率高,但是线程容易饿死
//查看源码可知ReentrantLock默认是非公平锁
public ReentrantLock() {
        sync = new NonfairSync();
    }
  
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

4、可重入锁

synchronized和ReentrantLock都是可重入锁

  • sychronized是隐式锁,不用手工上锁与解锁,而ReentrantLock为显示锁,需要手工上锁与解锁
  • 可重入锁也叫递归锁
  • 可以有效避免死锁

5、自旋锁(spinlock)

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU;

自旋锁的底层是CAS,CAS 的全称为 Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,整个过程是原子的。

CAS并发原语体现在JAVA语言中就是 sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于 sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

6、读写锁

独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLock和 Synchronized 而言都是独占锁

共享锁(读锁):该锁可被多个线程所持有

ReentrantReadWriteLock其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效的

7、死锁

产生死锁主要原因:

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当


验证是否是死锁:

  • jps -l类似于linux中的ps -ef查看进程号,定位进程号
  • jstack 进程号自带的堆栈跟踪工具死锁查看

8、JUC三大辅助类

减少计数CountDownLatch

循环栅栏CyclicBarrier

信号灯Semaphore

8.1 CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句

构造方法

CountDownLatch(int count)构造一个用给定计数初始化的CountDownLatch

两个常用的主要方法
await() 使当前线程在锁存器倒计数至零之前一直在等待,除非线程被中断
countDown()递减锁存器的计数,如果计数达到零,将释放所有等待的线程

public class LockTest {
    //6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {

        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);
        //6个同学陆续离开教室之后
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
                //计数  -1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        //等待
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
    }
}

8.2 CyclicBarrier

CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

构造方法
CyclicBarrier(int parties,Runnable barrierAction)创建一个新的CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程操作

常用的方法
await()在所有的参与者都已经在此barrier上调用await方法之前一直等待

public class LockTest {
    //创建固定值
    private static final int NUMBER = 7;

    public static void main(String[] args) {
        //创建CyclicBarrier
        CyclicBarrier cyclicBarrier =
                new CyclicBarrier(NUMBER,()->{
                    System.out.println("集齐7颗龙珠就可以召唤神龙");
                });

        //集齐七颗龙珠过程
        for (int i = 1; i <=7; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
                    //等待
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

8.3 Semaphore

一个计数信号量,从概念上将,信号量维护了一个许可集,如有必要,在许可可用前会阻塞每一个acquire(),然后在获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动

构造方法
Semaphore(int permits)创建具有给定的许可数和非公平的公平设置的Semapore

具体方法
acquire()从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
release()释放一个许可,将其返回给信号量

设置许可数量Semaphore semaphore = new Semaphore(3);一般acquire()都会抛出异常,release在finally中执行

public class LockTest {
    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);
        //模拟6辆汽车
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    //抢占
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+" 抢到了车位");
                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName()+" ------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

9、线程局部变量

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

线程间共享变量有风险,而使用ThreadLocal辅助类可以为各个线程提供各自的实例,例如时间格式化类SimpleDateFormat是线程不安全的,并发访问会出现混乱,而使用同步锁开销又很大,这时候就可以使用ThreadLocal是最方便高效的

/**
 * 用ThreadLocal处理simplDateFormat线程不安全
 * SimpleDateFormat在多线程情况下会出现线程不安全的情况,故用ThreadLoacl 处理
 */
public class LockTest1 {
    public static void main(String[] args) throws InterruptedException {
        // 使用了JUC辅助类
        CountDownLatch countDownLatch = new CountDownLatch(100);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
                String format = dateFormat.get().format(new Date());
                System.out.println(format);
                countDownLatch.countDown();
            }
            ).start();
        }

        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

四、线程安全的集合

1、线程不安全集合

平时使用的ArrayList、HashSet、HashMap等方法虽然运行速度快,但是都是现成不安全的集合,在多线程运行时,可能会产生java.util.ConcurrentModificationException并发修改异常

public static void main(String[] args) {
        //创建ArrayList集合
        List<String> list = new ArrayList<>();

        for (int i = 0; i <30; i++) {

            new Thread(()->{
                //向集合添加内容
                //故障原因add方法没有加锁
                list.add(UUID.randomUUID().toString().substring(0,8));
                //从集合获取内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }

2、集合的线程安全类

2.1 Vector和Hashtable

通过list和map实现的线程安全类,通过查看源码可发现是在方法中添加了synchronized关键字,现在用的较少(不推荐)

2.2 Collections

Collections类中的很多方法都是static静态,其中有一个方法是返回指定列表支持的同步(线程安全的)列表为synchronizedList(List <T> list),通过包装获得线程安全

List<String> list = Collections.synchronizedList(new ArrayList<>());

此方法也比较古老,很少使用

2.3 CopyOnWriteArrayList

List<String> list = new CopyOnWriteArrayList<>();

涉及的底层原理为写时复制技术,支持读多写少的并发情况

  • 读的时候并发(多个线程操作)
  • 写的时候独立,先复制相同的空间到某个区域,将其写到新区域,旧新合并,并且读新区域(每次加新内容都写到新区域,覆盖合并之前旧区域,读取新区域添加的内容)

2.4 CopyOnWriteArraySet

该类是HashSet的实现类,同样使用HashSet类,也会出现线程不安全

Set<String> set = new CopyOnWriteArraySet<>();

2.5 ConcurrentHashMap

ConcurrentHashMap类是HashMap的实现类,保证线程安全以及效率

3、阻塞队列

3.1 介绍

与普通队列不同,阻塞队列是共享队列(多线程操作),一端输入一端输出,不能无限放队列,满了之后就会进入阻塞,取出也同理,在多线程环境下,程序员不必自己去控制这些细节。阻塞队列有以下几点特点:

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

3.2 阻塞队列种类

  • ArrayBlockingQueue

    基于数组的阻塞队列,由数组结构组成的有界阻塞队列

    ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,无法并行

  • LinkedBlockingQueue

    基于链表的阻塞队列,由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列

    能够高效的处理并发数据的原因,是因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能

  • DelayQueue

    使用优先级队列实现的延迟无界阻塞队列

    DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞

  • PriorityBlockingQueue

    基于优先级的阻塞队列,支持优先级排序的无界阻塞队列

    不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者

  • SynchronousQueue

    一种无缓冲的等待队列,相对于有缓冲的 BlockingQueue 来说,少了一个缓冲区不存储元素的阻塞队列,也即单个元素的队列

    • 公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞
    多余的生产者和消费者,从而体系整体的公平策略;
    • 非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者

  • LinkedTransferQueue

    由链表结构组成的无界阻塞 TransferQueue 队列,由链表组成的无界阻塞队列

    预占模式,意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,生成一个节点(节点元素为 null)入队,消费者线程被等待在这个节点上,生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回

  • LinkedBlockingDeque

    由链表组成的双向阻塞队列

    • 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException异常
    • 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数

3.3 阻塞队列常用API

方法类型抛出异常特殊值阻塞超时
插入方法add(e)offer(e)put(e)offer(e,time.unit)
移除方法remove()poll()take()poll(time.unit)
检查方法element()peek()不可用不可用

抛出异常

  • 当阻塞队列满时,再往队列里add插入元素会抛llegalstateException.Queue full
  • 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException

特殊值

  • 插入方法,成功ture失败false
  • 移除方法,成功返回出队列的元素,队列里没有就返回null

阻塞

  • 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出
  • 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用

超时退出

  • 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出
//举例
public static void main(String[] args) {
        // 队列大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        System.out.println(blockingQueue.element()); // 检测队列队首元素!
        // public E remove()  返回值E,就是移除的值
        System.out.println(blockingQueue.remove());  //a
        System.out.println(blockingQueue.remove());  //b
        System.out.println(blockingQueue.remove());  //c 
        // java.util.NoSuchElementException
        System.out.println(blockingQueue.remove());
    }

五、线程池与异步计算

1、Java线程池

1.1 Java线程池概述

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度

特点:

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

具体架构:
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类。Java提供了一个工厂类来构造我们需要的线程池,这个工厂类就是 Executors 。这里主要讲4个创建线程池的方法,即

  • newCachedThreadPool()
  • newFixedThreadPool(int nThreads)
  • newScheduledThreadPool(int corePoolSize)
  • newSingleThreadExecutor()

1.2 newCachedThreadPool()

创建缓存线程池。缓存的意思就是这个线程池会根据需要创建新的线程,在有新任务的时候会优先使用先前创建出的线程。线程一旦创建了就一直在这个池子里面了,执行完任务后后续还有任务需要会重用这个线程,若是线程不够用了再去新建线程,60秒线程空闲就会被回收

ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int index = i;

    // 每次发布任务前根据奇偶不同等待一段时间,如1s,这样就会创建两个线程
    if (i % 2 == 0) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 执行任务
    cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + ":" + index));

注意这里的线程池是无限大的,并没有规定他的大小

1.3 newFixedThreadPool(int nThreads)

创建定长线程池,参数是线程池的大小。也就是说,在同一时间执行的线程数量只能是 nThreads 这么多,这个线程池可以有效的控制最大并发数从而防止占用过多资源。超出的线程会放在线程池的一个无界队列里等待其他线程执行完。

ExecutorService executorService = Executors.newFixedThreadPool(5);

1.4 newScheduledThreadPool(int corePoolSize)

第3个坏处线程池的坏处就是缺乏定时执行功能,这个Scheduled代表是支持的,这个线程池也是定长的,参数 corePoolSize 就是线程池的大小,即在空闲状态下要保留在池中的线程数量。而要实现调度需要使用这个线程池的 schedule() 方法

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
// 三秒后执行
scheduledExecutorService.schedule(() -> System.out.println(Thread.currentThread().getName() + ": 我会在3秒后执行。"),
                3, TimeUnit.SECONDS);

1.5 newSingleThreadExecutor()

创建单线程池,只使用一个线程来执行任务。但是它与 newFixedThreadPool(1, threadFactory) 不同,它会保证创建的这个线程池不会被重新配置为使用其他的线程,也就是说这个线程池里的线程始终如一。

ExecutorService executorService = Executors.newSingleThreadExecutor();

1.6 线程池的拒绝策略

RejectedExecutionHandler rejected = null;
rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务,抛出异常
rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务,不抛出异常[如果允许任务丢失这是最好的]
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务,回退

1.7 自定义线程池

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

ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

1.8 线程池的关闭

线程池启动后需要手动关闭,否则会一直不结束

  • shutdown() : 将线程池状态置成 SHUTDOWN,此时不再接受新的任务等待线程池中已有任务执行完成后结束
  • shutdownNow() : 将线程池状态置成 SHUTDOWN,将线程池中所有线程中断(调用线程的 interrupt() 操作),清空队列,并返回正在等待执行的任务列表

并且它还提供了查看线程池是否关闭和是否终止的方法,分别为 isShutdown()isTerminated()

2、Fork与Join分支

从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任 务,最终汇总每个小任务的结果得到这个大任务的结果。**工作窃取(work-stealing)**算法是指某个线程从其他队列里窃取任务来执行,率先完成任务的线程会去未完成任务的线程对应的队列里窃取一个任务来执行,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

核心类

ForkJoinPool:WorkQueue是一个ForkJoinPool中的内部类,它是线程池中线程的工作队列的一个封装,支持任务窃取。ForkJoinTask 需要通过 ForkJoinPool 来执行

主要方法

  • fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。
  • join() 当任务完成的时候返回计算结果。
  • invoke()开始执行任务,如果必要,等待计算完成。

子类Recursive:递归

  • RecursiveAction:用于没有返回结果的任务
  • RecursiveTask:用于有返回结果的任务
class MyTask extends RecursiveTask<Integer> {

    //拆分差值不能超过10,计算10以内运算
    private static final Integer 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() {
        //判断相加两个数值是否大于10
        if((end-begin)<=VALUE) {
            //相加操作
            for (int i = begin; i <=end; i++) {
                result = result+i;
            }
        } else {//进一步拆分
            //获取中间值
            int middle = (begin+end)/2;
            //拆分左边
            MyTask task01 = new MyTask(begin,middle);
            //拆分右边
            MyTask task02 = new MyTask(middle+1,end);
            //调用方法拆分
            task01.fork();
            task02.fork();
            //合并结果
            result = task01.join()+task02.join();
        }
        return result;
    }
}

public class TaskTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyTask对象
        MyTask myTask = new MyTask(0,100);
        //创建分支合并池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
        //获取最终合并之后结果
        Integer result = forkJoinTask.invoke();
        System.out.println(result);
        //关闭池对象
        forkJoinPool.shutdown();
    }
}

3、异步回调

3.1 函数式接口简介

函数式接口可以参考java8常用新特性,这里主要介绍几种函数式接口,Callable、Runnable、Future、CompletableFuture和FutureTask

3.2 Callable和Runnable异同

两个接口的定义

@FunctionalInterface
public interface Runnable {
    
    public abstract void run();
}

@FunctionalInterface
public interface Callable<V> {
    
    V call() throws Exception;
}

相同点

都是接口,都可以编写多线程程序,都可以通过线程池启动线程

不同点

Runnable没有返回值,Callable可以返回执行结果,是个泛型;Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

public class Test1 {

    static class Min implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            Thread.sleep(1000);
            return 4;
        }
    }
    static class Max implements Runnable{
        @SneakyThrows
        @Override
        public void run() {
            Thread.sleep(1000);
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new Min());
        executorService.submit(new Max());
    }

3.3 Future类

//Since:1.5
public interface Future<V> {
    //取消任务的执行,参数指定是否立即中断任务执行,或者等等任务结束
    boolean cancel(boolean mayInterruptIfRunning);
    //任务是否已经取消,任务正常完成前将其取消,则返回 true
    boolean isCancelled();
    //任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
    boolean isDone();
    //等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
    V get() throws InterruptedException, ExecutionException;
    //同上面的get功能一样,多了设置超时时间。超时会抛出TimeoutException
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

一般情况下,我们会结合Callable和Future一起使用,通过ExecutorService的submit方法执行Callable,并返回Future。

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
       ExecutorService executor = Executors.newCachedThreadPool();
       //Lambda 是一个 callable, 提交后便立即执行,这里返回的是 FutureTask 实例
       Future<String> future = executor.submit(() -> {
           System.out.println("running task");
           Thread.sleep(10000);
           return "return task";
       });
       future.get(2, TimeUnit.SECONDS);
   }

当然Future模式也有它的缺点,它没有提供通知的机制,我们无法得知Future什么时候完成。如果要在future.get()的地方等待future返回的结果,那只能通过isDone()轮询查询。

3.4 CompletableFuture类

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html

CompletableFuture能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。它避免了传统回调最大的问题,那就是能够将控制流分离到不同的事件处理器中。CompletableFuture弥补了Future模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。

CompletableFuture的静态工厂方法,其中runAsyncsupplyAsync 方法的区别是runAsync返回的CompletableFuture是没有返回值的。

方法名描述
runAsync(Runnable runnable)使用ForkJoinPool.commonPool()作为它的线程池执行异步代码
runAsync(Runnable runnable, Executor executor)使用指定的thread pool执行异步代码
supplyAsync(Supplier supplier)使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作有返回值
supplyAsync(Supplier supplier, Executor executor)使用指定的thread pool执行异步代码,异步操作有返回值
allOf(CompletableFuture<?>… cfs)等待所有任务完成,构造后CompletableFuture完成
anyOf(CompletableFuture<?>… cfs)只要有一个任务完成,构造后CompletableFuture就完成

对于变量的方法,常用有这几种方法

方法名描述
complete(T t)完成异步执行的话返回执行结果,若不是返回设置的值
completeExceptionally(Throwable ex)完成异步执行的话返回执行结果,若不是返回一个异常
thenApply(Function<? super T,? extends U> fn)返回一个新的CompletionStage,当这个阶段正常完成时,它将以这个阶段的结果作为所提供函数的参数执行
thenAccept(Consumer<? super T> action)返回一个新的CompletionStage,当这个阶段正常完成时,返回为void
handle(BiFunction<? super T, Throwable, ? extends U> fn)处理结果或错误,生成一个新结果返回
whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)处理结果或错误,返回为void
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
        //在这里执行返回值为World
        // future.complete("World");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //在这里执行结果为Hello
        future.complete("World");

        try {
            //get() 方法会抛出经检查的异常,可被捕获,自定义处理或者直接抛出。join() 会抛出未经检查的异常。
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
//thenApplyhello shawn
// thenAccept shawn
public static void main(String[] args) throws InterruptedException, ExecutionException {
    //thenApply
    CompletableFuture<String> cfuture =
            CompletableFuture.supplyAsync(() -> "shawn").thenApply(data -> "hello "+ data);
    System.out.println("thenApply" + cfuture.get());
    //thenAccept
    CompletableFuture.supplyAsync(() -> "thenAccept shawn").thenAccept(System.out::println);
}

3.5 FutureTask

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/FutureTask.html

Future是一个接口,是无法生成一个实例的,所以又有了FutureTask。FutureTask实现了RunnableFuture接口,RunnableFuture接口又实现了Runnable接口和Future接口。所以FutureTask既可以被当做Runnable来执行,也可以被当做Future来获取Callable的返回结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值