admin管理员组

文章数量:1547925

大家好,我是Oldou,今天又到了我们的学习时间了,本文介绍的是多线程相关的知识,文中的内容可能不是很全,但是学习完一定会让自己掉发升级,内容比较多,但是我们千万别放弃,不懂的地方一定要主动花时间去理解,毕竟学习是一辈子的事,不懂的东西不可能一直放在那里吃灰。正所谓:只要学不死,就往死里学。让我们一起进入正题吧。

目录

  • 简介
  • 基础篇
    • 进程和线程是什么?
    • 进程和线程的区别是什么?
    • 有了进程为什么还需要线程?
    • Java默认有几个线程?Java可以开启线程吗?
    • 创建线程的四种方式
      • 方式一:继承Thread类
      • 方式二:实现Runnable接口(代理模式)
      • 方式三:实现Callable接口
      • 方式四:使用Executors工具类创建线程池
      • 灵魂拷问
      • 继承Thread类和实现Runnable接口有什么区别?
      • Runnable和Callable这两种创建线程的方式有什么区别?
      • 线程的run()方法和start()方法有什么区别?
      • 为什么我们调用start()方法时会执行run()方法,为什么不直接调用run()方法?
    • 守护(deamon)线程
      • 什么是守护线程?
      • 守护线程的作用
    • 线程的状态
      • 新建状态-NEW
      • 运行状态-RUNNABLE
      • 阻塞状态-BLOCKED
      • 等待状态-WAITING
      • 超时等待-TIMED_WAITING
      • 线程终止-TERMINATED
      • 线程之间的切换
    • 线程的方法
      • 线程休眠-sleep()
      • JUC:TimeUnit替换 sleep()
      • Thread.sleep(0)的作用
      • 线程礼让-yield()
      • 线程的优先级-priority()
      • 获取线程ID-getId()
      • 获取当前线程-currentThread()
      • 线程中断-Interrupt()
      • 线程强制执行-join()
      • 如何关闭一个线程
  • 多线程并发篇【往后都是】
    • 什么是JUC?
    • 并发和并行
    • 多线程的初体验
    • 使用Jconsole工具观察线程
    • start()的源码分析
    • 线程安全与数据同步
      • 数据不一致问题的引入
    • 深入浅出Synchronized
      • Synchronized的简介
      • Synchronized的使用场景
      • Synchronized的作用
      • Synchronized的对象锁(monitor)机制
      • synchonized的优化【锁升级】
    • Lock显示锁
      • Lock锁的介绍
      • Lock锁的方法
      • ReentrantLock锁的构造方法
      • Lock和Synchronized的区别
    • 线程间通信
      • wait、notify和notifyAll的介绍
      • 生产者和消费者问题
      • Synchronized实现生产者和消费者问题
      • 解决虚假唤醒问题
      • JUC方式的生产者消费者问题
      • Condition 精准的通知和唤醒线程
      • wait() 和 sleep() 的区别
    • 八种锁现象理解锁
      • 情况一
      • 情况二
      • 情况三
      • 情况四
      • 情况五
      • 情况六
      • 情况七
      • 情况八
  • 由浅入深-同步容器CopyOnWrite
    • 同步容器之CopyOnWriteArrayList
      • 容器不安全问题的引入
      • 迭代过程中的异常 —— ConcurrentModificationException*
      • 如何防止迭代过程出现异常?
      • 线程不安全的解决方案
      • CopyOnWriteArrayList的详述
      • 总结
      • 应用场景
    • 同步容器之CopyOnWriteArraySet
      • HashSet的源码分析
      • CopyOnWriteArraySet的详述
      • 总结
    • 同步容器之ConcurrentHashMap
      • HashMap的简介
      • HashMap的线程不安全问题
      • HashMap的线程不安全解决方案
      • 深入分析ConcurrentHashMap
  • Callable详解【创建线程的第三种方式】
    • Callable的简述
    • 通过Callable创建线程
    • Callable 与Future 的叙述
    • FutureTask是什么?为什么我们可以通过Thread与Callable 产生联系?
    • FutureTask 的get()为什么会产生阻塞?
  • 并发辅助类【必会知识点】
    • 闭锁-CountDownLatch
    • 栅栏-CyclicBarrier
    • 信号量-Semaphore
    • 总结
    • CountDownLatch和CycliBarrier的区别?
  • 浅谈读写锁-ReentrantReadWriteLock
    • ReadWriteLock的介绍
    • ReentrantReadWriteLock的介绍
    • 写锁的获取与释放
    • 读锁的加锁与释放
    • 锁降级
    • 使用ReentrantReadWriteLock实现一个简单的缓存
    • 总结
  • 阻塞队列-BlockingQueue
    • BlockingQueue的介绍
    • BlockingQueue的四组API
    • BlockingQueue的实现类【转】
  • 线程池技术(重点)
    • 池化技术的简介
    • 什么是线程池?
    • 什么要用线程池?
    • 线程池的作用
    • 线程池的四大方法
    • 线程池的七大参数【超重要】
    • 线程池执行流程
    • 线程池的四大拒绝策略
    • 自定义线程池
    • 举例说明
    • 线程池的状态
    • 线程池的异常处理
    • CPU密集型和密集型 IO的介绍【线程池调优】
  • 四大函数式接口【必须掌握】
  • Stream流式计算
  • ForkJoin详解
  • 异步回调
  • Java锁机制
    • 锁的概念
    • JAVA 内置锁
    • 锁的分类
  • Volatile关键字
  • ThreadLocal由浅入深
  • CAS的学习
  • 可重入锁
  • 自旋锁
  • 死锁排查
  • 多线程设计模式
  • 结束语
  • 参考资料

简介

本文主要介绍了:线程的基础、创建线程的方式、守护线程、线程的状态、线程的方法、多线程的认识以及进阶、JUC下的各种类的介绍以及使用、Synchronized、Lock锁、线程间的通信、八锁情况、同步容器、并发辅助类、读写锁、阻塞队列、线程池技术、ThreadLocal、Volatile关键字等等知识点,看到这里,还等什么呢?赶紧点赞关注一波,让我们一起遨游知识的海洋【0.0】。

基础篇

进程和线程是什么?

  • 进程:进程作为资源分配的基本单位,进程是在内存中运行的一个应用程序【例如我们的QQ就算作一个进程,我们打开的IDEA就是一个进程】。每个进程都有着自己独立的一块内存空间,一个进程可以有多个线程。在windows系统中,一个运行的xx.exe就是一个进程;

  • 线程:线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。

进程和线程的区别是什么?

  • 基本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度和执行的基本单元;

  • 包含关系:一个进程包含一个或者多个线程(至少一个),线程是进程的一部分,因此线程被称为轻量级进程;

  • 资源开销: 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销,线程可以看作为轻量级进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换开销小。

  • 内存分配:同归进场的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是互相独立的;

  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃了整个进程就会全部死掉,所以多进程要比多线程健壮。

有了进程为什么还需要线程?

  • 进程是系统进行资源分配和调度的独立单位。每一个进程都有它自己的内存空间和系统资源,线程就是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。

  • 进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销,而线程在相同的环境下耗费资源少。

  • 引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。

总的来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能**【调度】**)。

Java默认有几个线程?Java可以开启线程吗?

  • 2个,一个主函数main线程、一个垃圾回收GC线程。

  • Java自己开启不了线程,它底层通过调用本地方法中的C++去操作硬件,源码如下所示:

    public synchronized void start() {
    ......
            try {
                start0(); //注意这里
                started = true;
            } finally {
    ......
        }
    	// 使用native关键字修饰了该方法,说明Java能力范围达不到了,需要使用底层的本地方法去调用C++
        private native void start0();
    

    如果这里不懂的话,请看我的JVM文章关于Native关键字的介绍。

创建线程的四种方式

创建线程有四种方式,分别是:

  • 继承Thread类;
  • 实现Runnable接口;
  • 实现Callable接口;
  • 使用Executors工具类创建线程池来创建线程(这个后面介绍);

方式一:继承Thread类

  • 第一步:自定义线程类MyThread继承Thread类;

  • 第二步:重写run方法,编写线程执行体run,run方法中的代码就是线程要执行的业务逻辑;

    public class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"   Run方法正在执行。。。。。");
        }
    }
    
  • 第三步:创建线程对象,调用start()方法来启动线程;

    public class CreateThreadWaysOne {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
            System.out.println(Thread.currentThread().getName()+"   main方法正在执行........");
        }
    }
    
  • 输出结果:

main   main方法正在执行........
Thread-0   Run方法正在执行。。。。。

注意:线程开启不一定立即执行,需要由CPU分配资源调度执行,并且直接调用run方法不会开启另一个线程,只有main线程运行,只有调用了start方法才是开辟了一个新的线程,和main线程交替执行。

方式二:实现Runnable接口(代理模式)

  • 第一步:定义一个Runnable接口实现类MyRunnable,并且重写run方法;

    public class MyRunnable implements Runnable{
        @Override
        public void run() {
       	 System.out.println(Thread.currentThread().getName()+"  run()方法正在执行。。。");
        }
    } 
    
  • 第二步:创建MyRunnable的实例,以myRunnable为target创建Thread对象,该Thread对象才是真正的线程对象;

  • 第三步:调用线程对象的start()方法启动线程;

    public class CreateThreadWaysTwo {
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();
       		 System.out.println(Thread.currentThread().getName()+"  main()方法正在执行");
   	    }
     }
  • 输出结果:
main  main()方法正在执行
Thread-0  run()方法正在执行。。。

方式三:实现Callable接口

关于Callable接口后面详细介绍。

  • 第一步:创建实现Callable接口的类MyCallable,并且重写call()方法,书写返回值类型;
public class MyCallable implements Callable<String>{
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"  call()方法的执行");
        return "你好啊";
    }
}
  • 第二步:以MyCallable为参数创建FutureTask对象;
  • 第三步:将FutureTask作为参数创建Thread对象;
  • 第四步:调用线程对象的start()方法;
public class CreateThreadWaysThree {
    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask<String>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            Thread.sleep(1000);
            System.out.println("返回结果:"+futureTask.get());
        }catch (InterruptedException e){
            e.printStackTrace();
        }catch (ExecutionException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"  main方法执行完毕");
    }
}
  • 输出结果:
Thread-0  call()方法的执行
返回结果:你好啊
main  main方法执行完毕

方式四:使用Executors工具类创建线程池

Executors工具类提供了一系列的工厂方法用于创建线程池,返回的线程池都实现了ExecutorsService接口。
四种线程池主要有:

  • newSingleThreadExecutors:创建一个单线程的线程池
  • newScheduledThreadPool:创建一个大小无限的线程池
  • newFixedThreadPool:创建固定大小的线程池
  • newCachedThreadPool:创建一个可缓存的线程池

方式四后面会给出详细的介绍,这里不过多介绍,见后文。

灵魂拷问

以上介绍了几种创建线程的方式,但是这里要问一个问题就是:创建线程的方式到底有几种?

答案是只有一种,

继承Thread类和实现Runnable接口有什么区别?

  • 前者的子类通过继承Thread类具备多线程能力,后者实现Runnable具备多线程能力
  • 前者启动线程通过子类对象.start(),后者传入目标对象+Thread对象.start()
  • 由于Java是单继承具有一定的局限性,因此没有后者灵活性高

Runnable和Callable这两种创建线程的方式有什么区别?

相同点:都是接口,都可以用于编写多线程程序,并且都是采用Thread.start()方法来启动线程;
区别如下

  • 返回值:Runnable接口的run()方法无返回值,Callable接口的call()方法有返回值,并且是个泛型,可以通过Future、FutureTask配合来获取异步执行的结果;
  • 异常信息是否能捕获:Runnable接口的run()方法只能抛出运行时的异常,且无法捕捉异常,而Callable接口的call()方法运行抛出异常可以获取异常信息;

线程的run()方法和start()方法有什么区别?

  • 每个线程都是通过某个特定的R=Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程;
  • start()方法用于启动线程,run()用于执行线程运行时的代码;
  • run()可以重复调用,而start()方法只能被调用一次;
  • start()方法用于启动一个线程,真正实现了多线程运行,调用start()方法无需等待run()方法体的代码执行完毕,可以直接继续执行其他代码,此时线程处于等待状态,并没有运行。通过此Thread类调用方法run()来完成其运行状态,run()结束,此线程就终止了,然后CPU再调用其他线程;
  • run()方法是在本线程里的,只是线程里的一个函数,而不是多线程,如果直接调用run(),其实只是相当于调用了一个普通函数而已,直接调用run()方法必须等到run()方法执行完毕后才能执行下面的代码,所以执行路径只有一条,所以多线程执行时要是使用start()方法而不是run()方法。

为什么我们调用start()方法时会执行run()方法,为什么不直接调用run()方法?

  • 当我们去new一个Thread时,线程进入了一个新建状态,调用start()方法时,会启动一个线程并且使线程进入就绪状态,这个时候只要分配到时间片【系统资源】之后就开始运行了。start()方法会执行线程的相应准备工作,然后自动执行run()方法,当调用到run()方法时就相当于线程被分配到时间片了,这个时候就会从就绪状态变为运行状态;
  • 而如果直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中去执行它。

守护(deamon)线程

线程分为用户线程和守护线程,守护线程是一种比较特殊的线程,一般用于处理一些后台的工作,比如 JDK 的垃圾回收线程就是守护线程。那到底什么是守护线程呢?为什么要有守护线程,以及何时需要守护线程?下面我们探究一下。

什么是守护线程?

首先我们先通过一个简单的程序来认识一下守护线程的特点::

/**
 * 测试守护线程
 */
public class DaemonThread {

    public static void main(String[] args) throws InterruptedException {
        // 创建子线程
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000L);
                    System.out.println(" 子线程...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
     // t1.setDaemon(true); // 是否设置为守护线程
        t1.start(); // 启动子线程
        Thread.sleep(2000L);
        // main 父线程结束
        System.out.println("main thread finished lifecycle.");
    }
}
  • 上面的代码存在两个线程,一个是由 JVM 启动的 main 线程,另外一个则是我们自己创建的线程 Thread,运行上面的这段代码,你会发现 JVM 进程永远不会退出,即使 main 线程正常结束了自己的生命周期,原因就是因为在 JVM 进程中还存在一个非守护线程在运行。

  • 如果打开是否设置为守护线程的注释t1.setDaemon(true);,将自己创建的 Thread 设置为了守护线程,那么main 进程结束生命周期后,JVM 也会随之退出运行,当然 Thread 线程也会结束。

【举栗子:JVM中如果没有一个非守护线程,JVM才会退出,当我们去创建一个类,类执行完结束,main函数终止后垃圾回收还要去回收它的。假如垃圾回收是一个非守护的线程,那么就会出现main函数已经结束了但是JVM无法退出,因为这里假设的是垃圾回收是非守护的,它会像上面的例子一样一直运行导致JVM无法退出。而我们现在之所以运行之后JVM能够退出是因为内部有很多像垃圾回收这样的守护线程,而守护线程就是会跟随着某一个线程的结束而结束,就拿我们上面的示例代码来说,main控制着它下面所有的子线程,子线程设置守护线程后,它们就会跟随者main线程的结束而结束】

守护线程的作用

通过上面的分析,如果一个 JVM 进程中没有一个非守护线程,那么 JVM 会退出,也就是说守护线程具备自动结束生命周期的特性而非守护线程则不具备这个特点,试想一下如果 JVM 进程的垃圾回收线程是非守护线程,如果 main 线程完成了工作,则 JVM 无法退出,因为垃圾回收线程还在正常的工作。再比如有一个简单的游戏程序,其中有一个线程正在与服务器不断交互以获取玩家最新的信息,若希望在退出游戏客户端的时候,这些数据同步的工作也能够立即结束,等等。

使用场景:守护线程经常用作与执行一些后台任务,因此有时它也被称为后台线程,当你希望关闭某些线程的时候,或者退出 JVM 进程的时候,一些线程能够自动关闭,此时就可以考虑用守护线程为你完成这样的工作。例如:后台记录操作日志,监控内存,垃圾回收等待…

如何设置守护线程:通过Thread.setDaemon(true)将用户线程设置为守护线程,默认是false,正常的线程都是用户线程。

线程的状态

要问到线程的状态,网上大部分人说是五种状态(新建、就绪、运行、阻塞、死亡),但是我看了一下线程的源码,Thread类中关于线程状态有一个枚举类,里面说明线程有6种状态,如下所示:

public enum State {
		//新建状态
        NEW,
		//可运行状态
        RUNNABLE,
		//阻塞状态
        BLOCKED,
		//等待状态
        WAITING,
    	//超时等待状态
        TIMED_WAITING,
		//终止状态
        TERMINATED;
    }
  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

新建状态-NEW

概念:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。顾名思义,这个状态,只存在于线程刚创建,未start之前,例如:

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        System.out.println(myThread.getState());
    }

此时打印出来的就是NEW。

运行状态-RUNNABLE

这个状态的线程,其正在JVM中执行,但是这个"执行",不一定是真的在运行, 也有可能是在等待CPU资源。当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。因此它分为一下两个状态:

就绪状态

包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系统资源,如等待CPU为它分配时间片。就绪状态有资格运行,但是要等到调度程序选到,不选到永远都是就绪状态。

进入就绪状态的几种方式:

  1. 新建的线程调用start()方法进入就绪状态。
  2. 运行中线程时间片用完了,调用该线程的yield()方法进入就绪状态。
  3. 等待锁资源的线程拿到对象锁后进入就绪状态。
  4. 当前线程sleep()结束、其他线程join()结束、等待用户输入完毕、某个线程拿到对象锁,这些线程也将进入就绪状态。

运行中状态

线程调度程序从可运行线程池中选择一个线程作为当前线程时,该线程所处的状态。

阻塞状态-BLOCKED

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。、
  • 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

比较经典的就是synchronized关键字,这个关键字修饰的代码块或者方法,均需要获取到对应的锁,在未获取之前,其线程的状态就一直未BLOCKED,如果线程长时间处于这种状态下,我们就是当心看是否出现死锁的问题了。

public class MyThread2 extends Thread {
    private byte[] lock = new byte[0];//一个资源
    public MyThread2(byte[] lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){//给这个资源加锁
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("done");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        byte[] lock = new byte[0];
        MyThread2 thread1 = new MyThread2(lock);
        thread1.start();
        MyThread2 thread2 = new MyThread2(lock);
        thread2.start();
        Thread.sleep(1000);//等一会再检查状态
        System.out.println(thread2.getState());
    }
}

等待状态-WAITING

一个线程会进入这个状态,一定是执行了如下的一些代码,

  • Object.wait()

  • Thread.join()

  • LockSupport.park()

当一个线程执行了Object.wait()的时候,它一定在等待另一个线程执行Object.notify()或者Object.notifyAll()
或者一个线程thread,其在主线程中被执行了thread.join()的时候,主线程即会等待该线程执行完成。当一个线程执行了LockSupport.park()的时候,其在等待执行LockSupport.unpark(thread)。当该线程处于这种等待的时候,其状态即为WAITING。需要关注的是,这边的等待是没有时间限制的,当发现有这种状态的线程的时候,若其长时间处于这种状态,也需要关注下程序内部有无逻辑异常。例如
LockSupport.park()

public class MyThread3 extends Thread {

    private byte[] lock = new byte[0];

    public MyThread3(byte[] lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        LockSupport.park();
    }

    public static void main(String[] args) throws InterruptedException {
        byte[] lock = new byte[0];
        MyThread3 thread1 = new MyThread3(lock);
        thread1.start();
        Thread.sleep(100);
        System.out.println(thread1.getState());
        LockSupport.unpark(thread1);
        Thread.sleep(100);
        System.out.println(thread1.getState());
    }

}

会输出WAITINGTERMINATED

Object.wait()

public class MyThread4 extends Thread{
    private byte[] lock = new byte[0];

    public MyThread4(byte[] lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            try {
                lock.wait(); //wait并允许其他线程同步lock
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args)
            throws InterruptedException {
        byte[] lock = new byte[0];
        MyThread4 thread1 = new MyThread4(lock);
        thread1.start();
        Thread.sleep(100);
        System.out.println(thread1.getState()); //这时候线程状态应为WAITING
        synchronized (lock){
            lock.notify(); //notify通知wait的线程
        }
        Thread.sleep(100);
        System.out.println(thread1.getState());
    }

}

会输出WAITINGTERMINATED

Thread.join()

public class MyThread5 extends Thread {

    private byte[] lock = new byte[0];

    public MyThread5(byte[] lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            //让线程睡一会
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyThread6 extends Thread {

    Thread thread;

    public MyThread6(Thread thread) {
        this.thread = thread;
    }

    @Override
    public void run() {
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Test{
    public static void main(String[] args)
            throws InterruptedException {
        byte[] lock = new byte[0];
        MyThread5 thread5 = new MyThread5(lock);
        thread5.start();
        MyThread6 thread6 = new MyThread6(thread5);
        thread6.start();
        Thread.sleep(100);
        System.out.println(thread6.getState());
    }
}

输出为WAITING

超时等待-TIMED_WAITING

这个状态和WAITING状态的区别就是,这个状态的等待是有一定时效的,即可以理解为WAITING状态等待的时间是永久的,即必须等到某个条件符合才能继续往下走,否则线程不会被唤醒。但是TIMED_WAITING,等待一段时间之后,会唤醒线程去重新获取锁。当执行如下代码的时候,对应的线程会进入到TIMED_WAITING状态

  • Thread.sleep(long)

  • Object.wait(long)

  • Thread.join(long)

  • LockSupport.parkNanos()

  • LockSupport.parkUntil()

这里举一个例子Object.wait(long):

public class MyThread8 extends Thread {
    private Object lock;

    public MyThread8(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            try {
                //注意,此处1s之后线程醒来,会重新尝试去获取锁,如果拿不到,后面的代码也不执行
                lock.wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("lock end");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        byte[] lock = new byte[0];
        //先创建线程对象
        MyThread8 thread = new MyThread8(lock);
        //执行线程
        thread.start();
        //让线程睡100ms
        Thread.sleep(100);
        System.out.println(thread.getState());
        Thread.sleep(2000);
        System.out.println(thread.getState());
    }
}

输出:

TIMED_WAITING
lock end
TERMINATED

其他的方法类似。

线程终止-TERMINATED

这个状态很好理解,一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程之间的切换

线程的方法

关于线程的方法,还是挺重要的,而且笔试也经常会出多选题让你选出哪些是线程的方法【别问我为什么】,其实这个部分我们完全可以看JDK的开发手册,这里我给出几个常见的:

  • static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠

  • final String getName(): 返回线程的名称

  • final synchronized void setName():设置线程的名字

  • Thread.getState():获取线程的状态

  • boolean isAlive():测试线程是否出于活动状态

  • setPriority(int newPriority):更改线程的优先级

  • inal int getPriority():获取线程优先级

  • void join():等待该线程终止,调用该方法的线程强制执行,其他线程阻塞,该线程执行完之后其他线程再执行

  • static void yield():暂停当前正在执行的线程对象,并且执行其他线程,当前正在执行的线程暂停一次,允许其他线程执行,
    不阻塞,线程进入就绪状态,如果没有其他等待执行的线程,这个时候当前线程就会马上恢复执行。

  • void interrupt():中断线程,别种这种方式

  • static Thread currentThread() :返回当前正在执行的线程

  • synchronized void start():用于启动一个线程

线程休眠-sleep()

sleep 是一个静态方法,【用的是static关键字修饰的,意味着我们可以直接Thread.sleep()调用,而不用去new对象】,其有两个重载方法,其中一个需要传入毫秒数,另外一个既需要毫秒数也需要纳秒数。

public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos)

sleep 方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是其不会放弃 monitor 锁的所有权(后文关于线程同步和锁的时候会重点介绍 monitor)。

sleep()和wait()的区别

  • 来源于不同的类:sleep()是Thread类中的方法,wait()是Object类中的方法这里特别要注意
  • 关于锁的释放:wait()会释放锁,sleep()只是让出CPU而不会释放同步资源锁;
  • 使用范围:wait()只能在同步代码块或同步方法中使用,而sleep()可以在任何地方使用;

而我们平时虽然测试代码的时候时常使用sleep()但是在工作中却不用它,而是使用TimeUnit()去替代sleep()方法,下面我们就来介绍一下这个TimeUnit()方法。

JUC:TimeUnit替换 sleep()

TimeUnit()方法是java.util.concurrent包下的一个枚举类,在 JDK1.5 以后,JDK 引入了一个枚举TimeUnit,其对 sleep 方法提供了很好的封装,使用它可以省去时间单位的换算步骤,比如线程想休眠 3 小时 24 分 17 秒 88 毫秒,使用TimeUnit 来实现就非常简便优雅了,如下所示:

Thread.sleep(12257088L);

TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(24);
TimeUnit.SECONDS.sleep(17);
TimeUnit.MILLISECONDS.sleep(88);

同样的时间表达,TimeUnit 显然清晰很多,强烈建议在使用 Thread.sleep 的地方,完全使用 TimeUnit 来代替,因为 sleep 能做的事情,TimeUnit 全部都能完成,并且可以做的更好。【详细的可以查看API】:

    • DAYS时间单位代表二十四小时
      HOURS时间单位代表六十分钟
      MICROSECONDS时间单位代表千分之一毫秒
      MILLISECONDS时间单位为千分之一秒
      MINUTES时间单位代表60秒
      NANOSECONDS时间单位代表千分之一千分之一
      SECONDS时间单位代表一秒

Thread.sleep(0)的作用

  • Thread.sleep(0)表示挂起 0 毫秒,你可能觉得没作用。其实 Thread.sleep(0)并非是真的要线程挂起 0 毫秒,意义在于这次调用 Thread.sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread.sleep(0) 是你的线程暂时放弃cpu,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作
  • 在线程中,调用sleep(0)可以释放 cpu 时间,让线程马上重新回到就绪队列而非等待队列,sleep(0)释放当前线程所剩余的时间片(如果有剩余的话),这样可以让操作系统切换其他线程来执行,提升效率。

线程礼让-yield()

yield 方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的 CPU 资源,如果 CPU 的资源不紧张,则会忽略这种提醒。

调用 yield 方法会使当前线程从 Running 状态切换到 Runnable 状态,一般这个方法不太常用。

在 JDK1.5 以前的版本中 yield 的方法事实上是调用了 sleep(0),但是他们之间存在着本质的区别,具体如下:

  • sleep 会导致当前线程暂停指定的时间,没有 CPU 时间片的消耗;

  • yield 只是对 CPU 调度器的一个提示,如果 CPU 调度器没有忽略这个提示,它会导致线程上下文的切换;

  • sleep 会使线程短暂 blocked,会在给定的时间内试放 CPU 资源;

  • yield 会使 Running 状态的 Thread 进入 Runnable 状态(如果 CPU 调度器没有忽略这个提示的话);

  • sleep 几乎百分之百的完成了给定时间的休眠,而 yield 的提示并不能一定保证。

线程的优先级-priority()

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

线程的优先级用数字表示,默认范围从1~10,如果超过了这个范围就会跑出参数异常IllegalArgumentException

  • Thread.MIN_PRIORITY= 1; //最低的优先级
  • Thread.MAX_PRIORITY = 10; //最高的优先级
  • Thread.NORM_PRIORITY= 5; //默认的优先级

使用以下方式改变或获取优先级

  • public final void setPriority(int newPriority)
  • public final int getPriority()

先设置优先级再启动!
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度。

获取线程ID-getId()

public long getId() 获取线程的唯一 ID,线程的 ID 在整个 JVM 进程中都会是唯一的,并且是从 0 开始逐次递增。如果你在 main 线程(main 函数)中创建了一个唯一的线程,并且调用 getId()后发现其并不等于 0,也许你会纳闷,不应该是从 0 开始的吗?之前已经说过了在一个 JVM 进程启动的时候,实际上是开辟了很多个线程,自增序列已经有了一定的消耗,因此我们自己创建的线程绝非第 0 号线程。下面我们来测试一下获取线程这个方法:

public class ThreadId {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while(true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        System.out.println(thread.getId());
    }
}

我的JDK版本是8,所以我的线程ID为11,这个会根据版本的不同而输出的ID也不同,JDK11的输出14。为啥不是0而是11呢?那是因为我们在启动的时候,虚拟机已经开启了很多的线程了【守护线程之类的】,我们可以通过我们后面介绍的工具去查看。

获取当前线程-currentThread()

public static Thread currentThread()用于返回当前执行线程的引用,这个方法虽然很简单,但是使用非常广泛,我们在后面的内容中会大量的使用该方法,例如获取当前线程的名字,下面我们来看一段测试代码:

public class CurrentThread {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread() == this);
            }
        };
        t1.start();
        String name = Thread.currentThread().getName();
        System.out.println(name);//输出main
    }
}

线程中断-Interrupt()

线程 interrupt,是一个非常重要的 API,也是经常使用的方法,与线程中断相关,相关的 API 有以下几个,在本节中我们也将 Thread 深入源码对其进行详细的剖析。

  • public void interrupt()
  • public static boolean interrupted()
  • public boolean isInterrupted()

interrupt

以下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的 interrupt 方法,就可以打断阻塞。

  • Object 的 wait 方法;
  • Object 的 wait(long)方法;
  • Object 的 wait(lOng, int)方法;
  • Object 的 sleep(long)方法;
  • Thread 的 sleep(long)方法;
  • Thread 的 join 方法;
  • Thread 的 join(long)方法;
  • Thread 的 join(long, int)方法;
  • InterruptIbleChannel 的 io 操作;
  • Selector 的 wakeup 方法。

上述若干方法都会使得当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的interrupt 方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,记住,打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态
一旦线程在阻塞的情况下被打断,都会抛出一个称为 InterruptedException的异常,这个异常就像一个 signal(信号)一样通知当前线程被打断了,下面我们来看一个例子:

public class ThreadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                 TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                 System.out.println(" 阻塞状态被中断");
                 e.printStackTrace();
            }
        });
        t1.start();
        // 短暂的阻塞是为了保证 t1 线程已启动
        TimeUnit.MILLISECONDS.sleep(100);
        // 中断 t1 线程的阻塞状态
        t1.interrupt();
    }
}

上面的代码创建了一个线程,并且企图休眠 1 分钟的时长,不过很可惜,大约在 100毫秒之后就被主线调用 interrupt 方法打断,程序的执行结果就是”阻塞状态被中断“。interrupt 这个方法到底做了什么样的事情呢?在一个线程内部存在着名为interrupt flag 的标识,如果一个线程被 interrupt,那么它的 flag 将被设置,但是如果当前线程正在执行可中断方法被阻塞时,调用 interrupt 方法将其中断,反而会导致flag 被清除,关于这点我们在后面还会做详细的介绍。另外有一点需要注意的是,如果一个线程已经是死亡状态,那么尝试对其的 interrupt 会直接被忽略。

isInterrupted

isInterrupted 是 Thread 的一个成员方法,它主要判断当前线程是否被中断,该方法仅仅是对 interrupt 标识的一个判断,并不会影响标识发生任何改变,这个与我们即将学习到的 interrupted 是存在差别的

public class ThreadInterrupt02 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
        @Override
        public void run() {
            while (true) {
            }
        }
        };
        //设置为守护线程,不设置也行,只是测试的时候程序一直在运行
        t1.setDaemon(true);
        t1.start();//启动线程进入就绪状态
        TimeUnit.MILLISECONDS.sleep(100);//让线程睡100ms
        System.out.printf("Thread is interrupted ? %s\n",t1.isInterrupted()); //false
        t1.interrupt();//中断线程
        System.out.printf("Thread is interrupted ? %s\n",t1.isInterrupted()); //true
    }
}

上面的代码中定义了一个线程,并且在线程的执行单元中(run 方法)写了一个空的死循环,为什么不写 sleep 呢?因为 sleep 是可中断方法,会捕获到中断信号,从而干扰我们程序的结果。程序运行的结果我写在输出后面了,记得手动结束上面的程序运行,或者你也可以将上面定义的线程指定为守护线程,这样就会随着主线程的结束导致 JVM 中没有非守护线程而自动退出。

中断方法捕获到了中断信号(signal)之后,也就是捕获了 InterruptedException异常之后会擦除掉 interrupt 的标识,为了不影响线程中其他方法的执行,将线程的 interrupt 标识复位是一种很合理的设计。

interrupted

interrupted 是一个静态方法,虽然其也用于判断当前线程是否被中断,但是它和成员方法 isInterrupted 还是又很大的区别,调用该方法会直接擦除掉线程的 interrupt标识,需要注意的是,如果当前线程被打断了,那么第一次调用 interrupted 方法会返回true,并且立即擦除了 interrupt 标识;第二次包括以后的调用永远都会返回 false,除非在此期间线程又一次被打算,下面设计了一个简单的例子,来验证我们的说法:

public class ThreadInterrupt04 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
        @Override
        public void run() {
            while (true) {
                //获取是够被中断的状态
                System.out.println(Thread.interrupted());
            }
        }
        };
        t1.setDaemon(true);
        t1.start();
        // 短暂的阻塞是为了保证 t1 线程已启动
        TimeUnit.MILLISECONDS.sleep(2);
        // 中断 t1 线程的阻塞状态
        t1.interrupt();
        System.out.println(t1.isInterrupted());
    }
}
...
false
false
true
false
false
false
...

interrupted的源码

我们打开Thread,找到interruptedisInterrupted,发现它们两个底层都调用了同一个本地方法:

private native boolean isInterrupted(boolean ClearInterrupted);

其中参数 ClearInterrupted 主要用来控制是否擦除线程 interrupt 的标识。

  • interrupted的源码中【如下所示】,我们可以看见参数ClearInterrupted为true,表示想要擦除线程的interrupt的标识。
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
  • isInterrupted的源码中,我们可以看见参数ClearInterrupted为false,表示不想擦除线程的interrupt的标识。
public boolean isInterrupted() {
    return isInterrupted(false);
}

线程强制执行-join()

Thread 的 join 方法同样是一个非常重要的方法,使用它的特性可以实现很多比较强大的功能,Thread 的 API 为我们提供了三个不同的 join 方法,具体如下:

  • public final void join() throws InterruptedException

  • public final void join (long millis) throws InterruptedException

  • public final void join (long millis, int nanos) throws InterruptedException

join()的使用

当当前线程B正在运行期间,某个线程 A调用join(),那么会使当前线程 B 进入等待,直到线程 A 结束生命周期,或者到达给定的时间,那么在此期间 B 线程是处于 Blocked 的,而不是 A 线程,下面就来通过一个简单的实例解释一下 join 方法的基本用法:

public class ThreadJoin {
    public static void main(String[] args) throws InterruptedException {
        // 1. 定义两个线程
        Thread t1 = new Thread(() -> printNum());
        Thread t2 = new Thread(() -> printNum());
        // 2. 启动这两个线程
        t1.start();
        t2.start();
        // 3. 执行这两个线程的 join 方法
        t1.join();
        t2.join();
        // 4. main 线程循环输出
        printNum();
    }
    private static void printNum() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "#" + i);
        }
    }
}

上面的代码首先创建了两个线程,分别启动,并且调用了每个线程的join 方法(注意:join 方法是被主线程调用的,因此在第一个线程还没结束生命周期的时候,第二个线程的 join 不会得到执行,但是此时,第二个线程也已经启动了),运行上面的程序,你会发现线程一和线程二会交替的输出直到他们结束生命周期,main 线程的循环才会开始运行,程序输出如下:

...
Thread-0#8
Thread-0#9
Thread-1#6
Thread-1#7
Thread-1#8
Thread-1#9
main#0
main#1
main#2
...

join 方法会使当前线程永远的等待下去,直到期间被另外的线程中断,或者 join 的线程执行结束,当然你也可以使用 join 的另外两个重载方法,指定毫秒数,在指定的时间到达之后,当前线程也会退出阻塞。
同样思考一个问题,如果一个线程已经结束了生命周期,那么调用它的 join 方法的当前线程会被阻塞吗?

【答案是:一直阻塞。】

如何关闭一个线程

JDK 有一个 Deprecated 方法 stop,但是该方法存在一个问题,JDK 官方早已经不推荐使用,其在后面的版本中有可能会被移除,根据官网的描述,该方法在关闭线程时可能不会释放掉 monitor 的锁,所以强烈建议不要使用该方法结束线程,下面介绍几种关闭线程的方法。

正常关闭

  • 1、线程结束生命周期正常结束

线程运行结東,完成了自己的使命之后,就会正常退出,如果线程中的任务耗时比较短,或者时间可控,那么放任它正常结束就好了。

  • 2、 捕获中断信号关闭线程
    我们通过 new Thread 的方式创建线程,这种方式看似很简单,其实它的派生成本是比较高的,因此在一个线程中往往会循环地执行某个任务,比如心跳检査,不断地接收网络消息报文等,系统决定退出的时候,可以借助中断线程的方式使其退出,示例代码如下:
public class InterruptThreadExit {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread() {
        @Override
        public void run() {
            System.out.println("I will start work.");
            while (!isInterrupted()) {
                 // working.
            }
            System.out.println("I will be exiting.");
        }
        };
        t.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("System will be shutdown.");
        t.interrupt();
    }
}

上面的代码是通过检査线程 interrupt 的标识来决定是否退出的,如果在线程中执行某个可中断方法,则可以通过捕获中断信号来决定是否退出。

  • 3、使用 volatile 开关控制
    由于线程的 interrupt 标识很有可能被擦除,或者逻辑单元中不会调用任何可中断方法,所以使用 volatile 修饰的开关 flag 关闭线程也是一种常用的做法,具体如下:
public class FlagThreadExit {
    public static void main(String[] args) throws InterruptedException {
        MyTask t = new MyTask();
        t.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("System will be shutdown.");
        t.close();
    }
}

class MyTask extends Thread {
    private volatile boolean closed = false;
    @Override
    public void run() {
        System.out.println("I will start work.");
        while (!closed && !isInterrupted()) {
          // working.
        }
        System.out.println("I will be exiting.");
    }
    public void close() {
        this.closed = true;
        this.interrupt();
    }
}

异常退出

在一个线程的执行单元中,是不允许抛出 checked 异常的,不论 Thread 中的 run 方法,还是 Runnable 中的 run 方法,如果线程在运行过程中需要捕获 checked 异常并且判断是否还有运行下去的必要,那么此时可以将 checked 异常封装成 unchecked 异常(RuntimeException)抛出进而结束线程的生命周期。

进程假死

所谓假死就是进程虽然存在,但没有日志输出,程序不进行任何的作业,看起来就像死了一样,但事实上它是没有死的,程序之所以出现这样的情况,绝大部分的原因就是某个线程阻塞了,或者线程出现了死锁的情况。这个时候我们需要借助一些工具来帮助诊断,比如 jstack、 jconsole、 jvisualvm 等工具,

IntelliJ IDEA 其实也是一个 Java 进程,打开 jvisualvm,选择 IntelliJ IDEA进程,如下图所示,将右侧的 Tab 切换到【线程】。

多线程并发篇【往后都是】

什么是JUC?

所谓JUC就是java.util.concurrent在并发编程中使用的工具类。

并发和并行

首先我们都知道,无论是并发还是并行,在用户看来都是同时运行的,不管是进程还是线程,都只是一个任务而已,但是真正干活的是CPU,同时需要记住的是:一个CPU在同一时刻只能执行一个任务

并发的概念:系统具有处理多个任务的能力,伪并行,看起来像是同时运行,实际上是单个CPU处理多个任务的能力,举个渣男例子:

你是一个 cpu,你同时谈了三个女朋友,每一个都可以是一个恋爱任务,你被这三个任
务共享,要玩出并发恋爱的效果,应该是你先跟女友 1 去看电影,看了一会说:不好,我
要拉肚子,然后跑去跟第二个女友吃饭,吃了一会说:那啥,我去趟洗手间,然后跑去跟女
友 3 喝下午茶。

这个例子懂了么?还不懂的话整个图:

并行的概念:系统具有同时处理多个任务的能力。同时运行,只有具备多个CPU才能实现,单核下,可以利用多道技术,多个核,每个核也都可以利用多道技术(多道技术是针对单核而言的)。

并行一定是并发,而并发不一定是并行。多个线程操作同一个资源类的时候,把资源丢入到线程中。

由上可得:

并行:

  • 并行性是指同一时刻内发生两个或多个事件。
  • 并行是在不同实体上的多个事件

并发:

  • 并发性是指同一时间间隔内发生两个或多个事件。
  • 并发是在同一实体上的多个事件

并行是针对进程的,并发是针对线程的

多线程的初体验

在计算机的世界里,当我们探讨并行的时候,实际上是指一系列的任务在计算机中同时运行,比如在浏览网页的时候还能打开音乐播放器,在撰写邮件的时候,收件箱还能接收新的邮件。在单 CPU 的计算机中,其实并没有真正的并行,它只不过是 CPU 时间片轮转机制带给你的错觉,而这种错觉让你产生了它们真的在同一时刻同时运行。当然如果是多核CPU,那么并行运行还是真实存在的。

案例:假设你想在浏览网页看新闻的同时还想听听音乐

public class Demo01 {

    public static void main(String[] args) {
        //JDK1.8之前的匿名内部类
       /* new Thread(){
            @Override
            public void run() {
                browseNews ();
            }
        };*/

        //JDK1.8以后 lambda编码方式
        //new Thread(()->{browseNews ();}).start();

        //JDK1.8后对象方法引用
        new Thread(Demo01::browseNews).start();
        enjoyMusic();
    }

    // 浏览新闻
    private static void browseNews () {
        while ( true ) {
            System. out .println ( "Uh huh. The good news." ) ;
            try {
                Thread. sleep( 1000L ) ;
            } catch ( InterruptedException e ) {
                e.printStackTrace () ;
            }
        }
    }
    // 欣赏音乐
    private static void enjoyMusic () {
        while ( true ) {
            System. out .println ( "Uh huh. The nice music." ) ;
            try {
                Thread. sleep( 1000L ) ;
            } catch ( InterruptedException e ) {
                e.printStackTrace () ;
            }
        }
    }
}

使用Jconsole工具观察线程

我们上面给出了一个多线程的简单例子,那么我们运行这个实例的时候JVM中有多少个线程呢?我们可以借助JDK自身提供的JVM工具Jconsole或者Jstack来进行查看。

  • 首先我们将代码先停掉,然后Win+R输入CMD打开电脑的命令窗口,然后输入jps查看一下现在运行的线程

  • 然后我们再启动我们刚刚的代码,再使用jps查看一下线程,多出来的那个线程就是我们代码的线程,我们使用``jconsole + 线程号去连接工具,点击不安全连接进入。

我们就可以在这里查看一下我们启动的线程之类的信息,像我们的main和Thread-0就是我们的用户线程,至于为什么还会有其他那么多的线程,基本上都是一些守护线程,上图中有一个线程Finalizer就是GC相关的线程。

如果觉得我们自己的线程名字太过普通不太好认出来,我们可以通过设置线程的名字的方法去设置,方法的介绍都在上面。

start()的源码分析

/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * 使此线程开始执行;Java虚拟机调用这个线程的run()方法
     
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * 多次调用start()方法启动一个线程是非法的,这里说明start方法只能调用不超过一次
     
     * In particular, a thread may not be restarted once it has completed
     * execution.
     * 特别是,线程在执行完成后不能重新启动
     
     * @exception  IllegalThreadStateException  if the thread was already
     * 如果已经启动的线程再次调用start()方法就会抛出线程非法状态异常
     *   
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
        /**
         * A zero status value corresponds to state "NEW".
         	0状态值对应着NEW状态
         */
        if (threadStatus != 0) //线程的状态校验,0表示NEW新建状态
            throw new IllegalThreadStateException();

        group.add(this); //将此线程添加进线程组

        boolean started = false;
        try {
            start0();// 调用 native 方法执行线程的 run 方法
            started = true;
        } finally {
            try {
                if (!started) { 
                    group.threadStartFailed(this); // 启动失败,从线程组中移除当前前程
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
	 native 方法,C++ 程序执行
    private native void start0();

我们从以上可以知道,一个线程的start方法只能被调用一次,如果已经启动的线程再次调用start()方法就会抛出线程非法状态异常IllegalThreadStateException

总结:start 方法的源码特别简单,其实最核心的部分就是 start0 这个本地方法,也就是 JNI方法:private native void start0 ();也就是说在 start 方法中会调用 start0 方法,那么重写的那个 run 方法何时被调用了呢?

JDK 官方文档是这样说的:Causes this thread to begin execution; the Java VirtualMachine calls the run method of this thread.

意思是:在开始执行这个线程时;Java 虚拟机将会调用这个线程的 run方法。换言之,run 方法就是被 JNI 方法 start0 调用的。仔细阅读源码会总结出以下几点:

  • Thread 被构造后的 NEW 状态,threadStatus 这个内部属性为 0;
  • 不能两次启动 Thread,否则就会出现IllegalThreadStateException 异常;
  • 线程启动后将会被加入到一个 ThreadGroup 中,后文中我们会详细介绍它;
  • 一个线程生命周期结束,也就是到了 Terminated 状态,再次调用 start 方法是非法的,也就是说 Terminated 状态是没有办法回到 Runnable/Running 状态的。

线程安全与数据同步

在串行化的任务执行过程中,由于不存在资源的共享,线程安全的问题几乎不用考虑,但是串行化的程序,运行效率低下,不能最大化地利用 CPU 的计算能力,随着 CPU核数的增加和计算速度的提升,串行化的任务执行显然是对资源的极大浪费,比如 B 客户提交了一个业务请求,只有等到 A 客户处理结束才能开始,这样的体验显然是用户无法忍受的。

无论是互联网系统,还是企业级系统,在追求稳定计算的同时也在追求更高的系统吞吐量,这也对系统的开发者提出了更高的要求,如何开发高效率的程序成了每个程序员必须掌握的技能,并发或者并行的程序并不意味着可以满足越多的 Thread, Thread 的多少对系统的性能来讲是一个抛物线,同时多线程的引入也带来了共享资源安全的隐患。在本章中,我们主要来探讨如何在安全的前提下高效地共享数据。

线程安全是在多线程编程中,有可能会出现同时访问同一个 共享、可变资源 的情况,始终都不会导致数据破坏以及其他不该出现的结果。这种资源可以是一个变量、一个对象、一个文件等。

共享:多个线程可以同时访问该共享变量。
可变:数据在生命周期中可以被改变。

数据不一致问题的引入

我们写一个简单的营业大厅叫号机程序,当我们设定的最大号码是50时,多个线程操作的时候会出现问题,如下所示:

/**
 * 具体策略类 叫号机
 */
public class CounterWindowRunnable implements Runnable {
    // 最多受理 50 笔业务
    private static final int MAX = 50;
    // 起始号码,不做 static 修饰
    private int index = 1;
    @Override
    public void run() {
        while (index <= MAX) {
            try {
                //让线程睡1毫秒
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.format(" 请【%d 】号到【%s 】办理业务\n", index++,
                    Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        final CounterWindowRunnable task = new CounterWindowRunnable();
        new Thread(task, " 一号窗口").start();
        new Thread(task, " 二号窗口").start();
        new Thread(task, " 三号窗口").start();
        new Thread(task, " 四号窗口").start();
    }
}

运行输出:

....
 请【23 】号到【 二号窗口 】办理业务
 请【24 】号到【 四号窗口 】办理业务
 请【25 】号到【 一号窗口 】办理业务
 请【24 】号到【 三号窗口 】办理业务
 请【26 】号到【 二号窗口 】办理业务
....
 请【48 】号到【 三号窗口 】办理业务
 请【49 】号到【 四号窗口 】办理业务
 请【50 】号到【 二号窗口 】办理业务
 请【51 】号到【 一号窗口 】办理业务
 请【52 】号到【 三号窗口 】办理业务
 请【53 】号到【 四号窗口 】办理业务

多次运行上述程序,每次都会有不一样的发现,但是总结起来主要有三个问题,具体如
下。

  • 第一,某个号码被略过没有出现。
  • 第二,某个号码被多次显示。
  • 第三,号码超过了最大值 50。

原因分析

  • 某个号码被略过没有出现。

线程的执行是由 CPU 时间片轮询调度的,假设此时线程 1 和 2 都执行到了 index=35 的位置,其中线程 2 将 index 修改为 36 之后未输出之前,CPU 调度器将执行权利交给了线程 1,线程 1 直接将其累加到了 37,那么36 就被忽略了。

  • 某个号码被多次显示。

线程 1 执行 index+1,然后 CPU 执行权落入线程 2 手里,由于线程 1 并没有给 index赋予计算后的结果24,因此线程 2 执行 index+1 的结果仍然是 24,所以会出现重复号码的情况。

  • 号码超过了最大值 50。

当 index=49 的时候,线程 1 、线程 2、线程3、线程4 都看到条件满足,线程 2、线程3、线程4 短暂停顿,线程 1 将 index 增加到了 50,线程 2 恢复运行后又将 50增加到了 51,线程 3 恢复运行后又将 51增加到了 52,线程 4 恢复运行后又将 52增加到了 53,此时就出现了超过最大值的情况。

解决方式

我们使用synchronized关键字进行加锁处理

/**
 * 具体策略类 叫号机
 */
public class CounterWindowRunnable implements Runnable {
    // 最多受理 50 笔业务
    private static  final int MAX = 50;
    // 起始号码,不做 static 修饰
    private int index = 1;
    @Override
    public void run() {
        synchronized (this){
            while (index <= MAX) {
                try {
                    //让线程睡1毫秒
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.format(" 请【%d 】号到【%s 】办理业务\n", index++,
                        Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        final CounterWindowRunnable task = new CounterWindowRunnable();
        new Thread(task, " 一号窗口").start();
        new Thread(task, " 二号窗口").start();
        new Thread(task, " 三号窗口").start();
        new Thread(task, " 四号窗口").start();
    }
}

问题解决,那么synchronized是啥呢?加一个这玩意儿就好了,我们下面就介绍以下synchronized关键字。

深入浅出Synchronized

Synchronized的简介

  • synchronized是Java的一个关键字,它能够将代码块或者方法锁起来,它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能。

  • synchronized是一种互斥锁,一次只能允许一个线程进入被锁住的代码块;

  • synchronized是一种内置锁/监视器锁,在Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用**对象的内置锁(监视器)**来将代码块(方法)锁定的!

Synchronized的使用场景

我们使用该关键字一般会有以下几种场景:

  • 修饰实例方法,对当前实例对象this加锁
public class Demo {
    public synchronized void test(){

    }
}
  • 修饰静态方法,对当前类的Class对象加锁
public class Demo {
    public void test(){
        synchronized(Synchronized.class){

        }
    }
}
  • 修饰代码块,指定一个加锁的对象,给对象加锁
public class Demo {
    public void test(){
        synchronized(new A()){

        }
    }
}

synchronized关键字可以锁方法、锁代码块、锁对象。这里需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系

Synchronized的作用

  • synchronized保证了线程的原子性,被synchronized保护的代码块是一次被执行的,没有任何线程会同时进行访问;

  • synchronized保证了可见性,当执行完synchronized之后,修改后的变量对其他线程是可见的。

Java中的synchronized通过使用内置锁来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

Synchronized的对象锁(monitor)机制

首先我们编写一个简单的SynchronizedDemo,分别有锁方法和锁代码块,代码如下所示:

public class SynchronizedDemo {
    public synchronized void test(){
        synchronized (this){

        }
    }
    public static void main(String[] args) {
    }
}

编译之后【就是运行之后】,打开对应的目录找到SynchronizedDemo.class文件的位置,然后可以使用电脑的命令窗口去反编译一下这个文件,【javap -v SynchronizedDemo.class】查看字节码文件:


  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter			#看这里
         4: aload_1
         5: monitorexit				#看这里
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit				#看这里
        12: aload_2
        13: athrow
        14: return
....
  • 如上所示,我们添加了标记的地方就是使用Synchronized关键字之后独有的地方,执行同步代码块后首先要先执行monitorenter命令,退出的时候执行monitorexit命令。

  • 通过分析可以知道,使用Synchronized进行同步,关键的就是将对象的监视器monitor进行获取,当线程获取到monitor后才能继续往下执行,否则只能等待。而这个获取的过程是互斥的【即同一时刻只有一个线程能够获取到monitor】。

  • 从上面的demo中就可以看出来,在执行同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一

也就是说当我们进入一个人方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1,同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。所有的互斥,其实在这里,就是看你能否获得monitor的所有权,一旦你成为owner就是获得者。

  • 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态

下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:

​ 对象,对象监视器,同步队列和线程状态的关系

该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

synchronized底层通过monitor对象,该对象有自己的对象头,存储了很多信息,其中一个信息标识是被哪个线程持有

Synchronized:底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorentermonitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。

synchonized的优化【锁升级】

Lock显示锁

Lock锁的介绍

Lock显式锁是JDK1.5之后才有的,它是一个接口,我们可以看看这个接口下面的实现类有哪些。【我们打开JDK1.8的手册】

Lock是java.util.concurrentLocks包下的一个接口,它有三个实现类分别是:RenntrantLock(可重入锁)、ReadLock(读锁)、WriteLock(写锁)。【其中RenntrantLock是最常用的】,我们需要手动的加锁和解锁,这样给我们带来了很好的灵活性,但是我们必须手动释放锁【同时这个操作需要加入try-catch,必须在finally中释放锁,不然会造成死锁】。同时Lock锁支持Condition条件对象,允许多个读线程同时访问共享资源。

查看Lock的源码发现底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。

Lock锁的方法

  • Modifier and TypeMethod and Description
    voidlock():获得锁。 如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。
    voidlockInterruptibly():获取锁定,除非当前线程是 interrupted 。
    ConditionnewCondition():返回一个新Condition绑定到该实例Lock实例。
    booleantryLock():尝试获取锁,只有在调用时才可以获得锁。
    booleantryLock(long time,TimeUnit unit):如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁。
    voidunlock():释放锁。

ReentrantLock锁的构造方法

首先我们创建一个对象,然后点进去查看一下源码。

Lock lock = new ReentrantLock();

在该源码中我们可以发现,默认的是创建一个非公平锁,如果传入一个true值就创建一个公平锁。【关于这两个锁我在后文介绍】

    /**
	 * 无参构造就创建一个非公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync(); //创建了一个非公平锁
    }
    /**
	 * 有参构造传入一个boolean值,为true就创建一个公平锁,false就创建一个非公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

Lock和Synchronized的区别

  • 【本质区别】:Synchronized是Java的关键字,由内置语言实现,而Lock是接口。

  • 【异常是否释放锁】:Synchronized在线程发生异常的时候会自动释放锁,因此不会发生异常死锁,而Lock异常时不会自动释放锁,所以需要在finally中实现手动释放锁。

  • 【是否响应中断】:Lock可以中断锁,Synchronized是非中断锁,必须等待线程执行完后释放锁。【后文有该锁的介绍】

  • 【效率上比较】Lock可以使用读锁提高多线程读效率。

  • 【能否获取锁的状态】Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁。

  • 【作用范围】Synchronized 可以锁对象、类、代码块,而Lock只能锁代码块。

线程间通信

与网络通信等进程间通信方式不一样,线程间通信又称为进程内通信,多个线程实现互斥访问共享资源时会互相发送信号或等待信号,比如线程等待数据到来的通知,线程收到变量改变的信号等。下面我们通过案例来学习 Java 提供的原生通信 API,以及这些通信机制背后的内幕。

wait、notify和notifyAll的介绍

首先,wait、notify和notifyAll 这三个都是Object类里的方法,可以用来控制线程的状态;

  • 如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态

  • 如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。

  • 如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

wait()的介绍

  • public final void wait() throws InterruptedException导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法。 换句话说,这个方法的行为就好像简单地执行呼叫wait(0)public final native void wait(long timeout) throws InterruptedException;】。

  • 当前的线程必须拥有该对象的监视器。 该线程释放此监视器的所有权,并等待另一个线程通知等待该对象监视器的线程通过调用notify方法或notifyAll方法。 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。

  • Java开发书记中建议开发者永远都要把wait()放到循环语句里面,否则很可能会发生线程虚假唤醒的问题,这就是为什么一个是wait()方法外面为什么是while循环而不是if判断

为什么wait()方法必须要写在synchronized中?

wait是要释放对象锁,进入等待池。既然是释放对象锁,那么肯定是先要获得锁。所以wait必须要写在synchronized代码块中,否则会报异常。

notify的介绍

  • public final void notify(),唤醒正在等待对象监视器的单个线程。如果任何线程正在等待这个对象,其中一个被选择被唤醒。 线程通过调用wait方法之一等待对象的监视器。

  • 唤醒的线程将无法继续,直到当前线程放弃此对象上的锁定为止。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步。

notifyAll的介绍

  • public final void notifyAll(),唤醒正在等待对象监视器的所有线程。 线程通过调用wait方法之一等待对象的监视器。

  • 唤醒的线程将无法继续,直到当前线程释放该对象上的锁。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步

我们应该尽量使用notifyAll()的原因就是,notify()非常容易导致死锁

notify()也需要写在synchronized代码块中,调用对象的这两个方法也需要先获得该对象的锁。
notify,notifyAll,唤醒等待该对象同步锁的线程,并放入该对象的锁池中。
对象的锁池中线程可以去竞争得到对象锁,然后开始执行, 如果是通过notify来唤起的线程,那进入wait的线程会被随机唤醒;

如果是通过notifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即FIFO的策略;

notify()或者notifyAll()调用时并不会真正释放对象锁, 必须等到synchronized方法或者语法块执行完才真正释放锁.

以上三个方法都是Object类中的方法,它们组成一个典型的生产者消费者模型,下面我们来实现一下经典的生产消费问题。

生产者和消费者问题

线程之间的通信问题:生产者和消费者问题, 等待唤醒,通知唤醒。

我们通过一个案例来实现一下线程交替执行,A、B线程操作同一个变量num,A线程负责将num++,B线程负责将num–。

Synchronized实现生产者和消费者问题

public class Demo {
    public static void main(String[] args) {
        Data data = new Data();
		//开启两个线程进行操作
        new Thread(()->{
            for (int i=0;i<10;i++){
                try {
                    data.increment();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"A").start();

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

    }
}
/** 资源类
 * 判断等待,业务、通知。
 */
class Data{
    //共同操作的资源
    private int num = 0;
    
    //+1
    public synchronized void increment() throws InterruptedException {
        if(num!=0){  //0
            //等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"------>"+num);
        //通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if (num == 0){ //1
            //等待
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"------>"+num);
        //通知其他线程,我-1完毕了
        this.notifyAll();
    }
}

我们允许可以发现,没什么问题,两个线程操作同一个资源。但是如果四个线程操作同一个资源呢?开四个线程我们测试,发现数据变化了,也就是说现在是一个数据不安全的状态,那为什么会出现这样的状况呢?我们打开我们的JDK手册去查看一下wait()方法。

注意点:防止虚假唤醒的问题,也就是说我们使用Object的wait方法时,wait方法须一直在循环中,而我们上面的代码中是放在了if语句中,只判断一次就完事了,因此我们将代码中的if改为while,这就是为什么一个是wait()方法外面为什么是while循环而不是if判断

解决虚假唤醒问题

我们将代码中的if改为while。

public class Demo {
    public static void main(String[] args) {
        Data data = new Data();

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

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

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

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

    }
}

/** 资源类
 * 判断等待,业务、通知。
 */
class Data{
    //共同操作的资源
    private int num = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while(num!=0){  //0
            //等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"------>"+num);
        //通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        while (num == 0){ //1
            //等待
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"------>"+num);
        //通知其他线程,我-1完毕了
        this.notifyAll();
    }
}

根据输出可以发现,虚假唤醒的问题已经解决了。

总结:我们以上代码使用的是Object的wait方法设置当前线程进行等待,然后通过notifyAll()方法唤醒正在等待对象的监视器的单个线程,这两个方法都是Object中的方法,那么有没有什么方法可以替换掉这两个方法呢?不知道上面我给出的Lock的方法中还记不记得这么一个方法newCondition(),下面我们来介绍一下JUC下的几个方法。

JUC方式的生产者消费者问题

我们通过Lock找到Condition,如下图所示:

我们可以通过Condition接口下的await方法和signal方法替换调Object的wait方法和notifyAll方法,方法介绍大家可以看JDK的手册。

public class Demo02 {
    public static void main(String[] args) {
        Data1 data = new Data1();

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

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

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

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

    }
}
/** 资源类
 * 判断等待,业务、通知。
 */
class Data1{
    //共同操作的资源
    private int number = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    /**
     * 等待     condition.await();
     * 唤醒全部 condition.signalAll();
     */

    //+1
    public void increment() throws InterruptedException {

        lock.lock();
        try {
            // 业务代码
            while (number!=0){ //0
                // 等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            // 通知其他线程,我+1完毕了
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    //-1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number==0){ // 1
                // 等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            // 通知其他线程,我-1完毕了
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

从输出可以发现,上面四个线程没有按照顺序执行,这不是我们想要的,我们想要A执行完了去通知B执行,B执行完了去通知C执行…以此类推,下面我们再去修改一下。

Condition 精准的通知和唤醒线程

我们上面使用的是condition.signalAll();唤醒全部,接下来我们实现精准的通知,可以使用condition.signal();方法实现。

public class Demo03 {
    public static void main(String[] args) {

        Data3 data3 = new Data3();
        new Thread(()->{
            for(int i=0;i<5;i++){
                data3.printA();
            }
        },"A").start();
        new Thread(()->{
            for(int i=0;i<5;i++){
                data3.printB();
            }
        },"B").start();
        new Thread(()->{
            for(int i=0;i<5;i++){
                data3.printC();
            }
        },"C").start();
    }
}

/** 资源类
 * 判断等待,业务、通知。
 */
class Data3{
    //共同操作的资源
    private int number = 1; //1-->A  2-->B  3--C

    Lock lock = new ReentrantLock();
    //创建三个同步监视器,分别监视三个线程
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    public void printA(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number!=1){
                // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
            // 唤醒,唤醒指定的人,B
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number!=2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
            // 唤醒,唤醒指定的人,c
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number!=3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>CCCCCCCCCCC");
            // 唤醒,唤醒指定的人,c
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}
A=>AAAAAAA
B=>BBBBBBBBB
C=>CCCCCCCCCCC
A=>AAAAAAA
B=>BBBBBBBBB
C=>CCCCCCCCCCC
...

这样我们就能实现精准的通知和唤醒线程了。

wait() 和 sleep() 的区别

  • 首先从本质上来说,sleep()是Thread中的方法,而wait()是Object中的方法;
  • 其次从使用范围上来说,sleep()方法可以在任何地方使用,而wait()方法只能在synchronized块或者synchronized修饰的方法中使用;
  • 使用sleep()方法不需要被唤醒,而使用wait()方法需要使用notify()或者notifyAll()唤醒;
  • Thread.sleep只会让出CPU,不会导致锁行为的改变,如果当前线程拥有锁,那么Thread.sleep()不会让线程释放锁。
  • Object.wait不仅让出CPU,还会释放已经占有的同步资源锁。

Thread.sleep和Object.wait都会暂停当前的线程,对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间。

八种锁现象理解锁

看下面的代码,分析先输出哪个。

情况一

正常情况下,下面两个线程哪个先打印?先打印发短信还是打电话?

/** 八锁情况一
 * 问题:正常情况下,下面两个线程哪个先打印?先打印发短信还是打电话?
 * 答案:打电话
 * 分析:被 synchronized 修饰的方法,锁的对象是方法的调用者也就是实际new的对象,所以说这里两个方法调用的对象是同一个,因此先调用的先执行!
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone{
    //synchronized锁的对象是方法的调用者
    public synchronized void sendSms(){
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:

发短信
打电话

情况二

sendSms()延迟4秒的情况下,两个线程哪个先打印?先打印发短信还是打电话?

/** 八锁情况二
 * sendSms()延迟4秒的情况下,两个线程哪个先打印?先打印发短信还是打电话?
 * 答案:打电话
 * 被 synchronized 修饰的方法,锁的对象是方法的调用者,所以说这里两个方法调用的对象是同一个先调用的先执行!执行sleep()方法的线程并不会释放锁。
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone{
     //synchronized锁的对象是方法的调用者
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:

发短信
打电话

分析:synchronized锁的对象就是方法的调用者,以上并不是因为程序是由上往下执行而导致的结果,而是因为锁的存在,因为两个方法调用的都是同一把锁【对象的锁phone】,而对象的锁在代码中只有一把,谁先拿到就谁先执行,以上睡一秒保证A线程先拿到锁。

情况三

增加一个普通方法,请问先打印那个 发短信还是 hello

/**八锁情况三
 * 问题:增加一个普通方法,请问先打印那个 发短信还是 hello
 * 结果:hello
 * 新增加的这个方法没有 synchronized 修饰,不是同步方法,不受锁的影响!
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}

class Phone{
     //synchronized锁的对象是方法的调用者
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }
// 这里没有锁,不是同步方法,不受锁的影响
    public void hello(){
        System.out.println("hello");
    }
}

情况四

两个对象,两个同步方法, 先输出发短信还是打电话?

/** 八锁情况一
 * 问题:正常情况下,下面两个线程哪个先打印?先打印发短信还是打电话?
 * 结果:打电话
 * 分析:被synchronized 修饰的不同方法 锁的对象是调用者,这里锁的是两个不同的调用者,这里有两把锁,所以互不影响,这里由于发短信那里有延迟,所以先执行打电话
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone{
     //synchronized锁的对象是方法的调用者
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

情况五

增加两个镜静态的同步方法, 先输出发短信还是打电话?

/** 八锁情况五
 * 问题:增加两个镜静态的同步方法, 先输出发短信还是打电话? 
 * 结果:发短信
 * 分析:只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!所以说这里是同一个锁,并不是因为synchronized,这里程序会从上往下依次执行。
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone{
    //synchronized锁的对象是方法的调用者
    //static修饰的在类加载的时候就初始化了 锁的对象是Class对象
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }
}

情况六

两个对象,两个被static修饰的同步方法,是先打印发短信还是打电话?

/** 八锁情况六
 * 问题:两个对象,两个被static修饰的同步方法,是先打印发短信还是打电话?
 * 结果:发短信
 * 分析:只要方法被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一,虽然下面是创建了两个对象,但是它们的Class对象是同一个,因此还是先执行的先调用
 */
public class Demo1 {
    public static void main(String[] args) {
        //这两个对象的Class类模板只有一个
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone{
    //synchronized锁的对象是方法的调用者
     //static修饰的在类加载的时候就初始化了 锁的对象是Class对象
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }
}

情况七

一个对象,有两个同步方法,一个被static关键字修饰,一个没有被static修饰,先输出哪个?发短信还是打电话?

/** 八锁情况七
 * 问题:一个对象,有两个同步方法,一个被static关键字修饰,一个没有被static修饰,先输出哪个?发短信还是打电话?
 * 结果:打电话
 * 分析:只要被static修饰锁的是class模板, 而synchronized 锁的是调用的对象,这里是两个锁互不影响,按时间先后执行。
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone{
    //静态的同步方法,锁的是Class类模板
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    //普通的同步方法,锁的是调用者
    public synchronized void call(){
        System.out.println("打电话");
    }
}

情况八

两个对象,一个静态的同步方法,一个普通的同步方法,先输出哪个?发短信还是打电话?

/** 八锁情况八
 * 问题:两个对象,一个静态的同步方法,一个普通的同步方法,先输出哪个?发短信还是打电话?
 * 结果:打电话
 * 分析:只要被static 修饰的锁的就是整个class模板,这里一个锁的是class模板 一个锁的是调用者, 所以锁的是两个对象,互不影响
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone1.call();
        },"B").start();
    }
}
class Phone{
    //静态的同步方法,锁的是Class类模板
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    //普通的同步方法,锁的是调用者
    public synchronized void call(){
        System.out.println("打电话");
    }
}

由浅入深-同步容器CopyOnWrite

Copy-On-Write简称COW,是计算机程序设计领域中的一种优化策略。从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayListCopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

同步容器之CopyOnWriteArrayList

容器不安全问题的引入

我们平时使用List容器中,使用最频繁的莫过于ArrayList,而我们都知道,它是线程不安全的,在多个线程同时操作的情况下,无法保证add操作的原子性从而会出现数据不安全的现象,下面我们就举个例子,我们开10条线程往ArrayList中插入数据:

public class ListTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for(int i=1; i<=10; i++){
            new Thread(()->{
                //插入随机数
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }
}

运行报错【多运行几次】java.util.ConcurrentModificationException,这个错误要牢牢的记住,主要在并发下在集合里面基本上都会出现这个错误,这个异常名为【并发修改异常】。

迭代过程中的异常 —— ConcurrentModificationException*

在ArrayList的实现中,其迭代器实现了一个方法checkForComodification 这个方法会检查迭代期间是否有其他线程修改了集合,如果有,则抛出ConcurrentModificationException

而抛出这个异常主要与expectedModCount(来自ArrayList的迭代器Itr)和modCount(来自ArrayList的父类AbstractList,初始为0)这两个关键字有关。

因为ArrayList实现中,每执行一次添加操作,都会让modCount+1

【addAll也是让modCount+1,与添加的元素个数无关。remove和set操作不算,其不会让modCount有所改变。】。

ArrayList的迭代器中,expectedModCount的初始值被设定为modCount

迭代器在每次遍历时,会调用checkForComodification 检查状态,如果此过程中集合发生了改动,则直接抛出异常。

:如果迭代期间需要修改集合,只能通过迭代器的方法修改集合,这些方法不会触发异常。因为其会重置expectedModCount的值为当前modCount。

如何防止迭代过程出现异常?

所以,在使用这样的ArrayList时,如果需要对其进行迭代,则需要对容器进行加锁(或者拷贝一份),使当前线程对其独占访问,以保证其迭代过程能够正常运行。如下:

public class SomeClass {
    List<E> list;

    public SomeClass(List<E> list) {
        this.list = list;
    }

    //如果这个方法会被多线程访问,那么最好对list的访问进行加锁
    public void function() {
        synchronized(list) {
            for(E e:list) {
                ....
            }
        }
    }
}

那这个线程不安全问题该如何解决呢?

线程不安全的解决方案

  • 方案一使用Vector代替ArrayList,因为Vector是线程安全的,底层使用了synchronized关键字进行修饰,但是不建议使用这个。
List<String> list =  new Vector<>();
  • 方案二:使用Collections工具类中的synchronizedList方法将ArrayList转为线程安全的集合。【包装】
List<String> list = Collections.synchronizedList(new ArrayList<>());

Collections实用类(注意,不是Collection接口)提供同步容器包装,将普通的集合包装成线程安全的集合。当synchronizedList传入的参数类型是ArrayList时, 因为ArrayList实现了RandomAccess接口,所以synchronizedList会构建一个SynchronizedRandomAccessList对象,当我们使用add()方法添加对象的时候,synchronizedList的底层在执行add()等方法的时候是加了synchronized关键字的,因此添加数据的时候线程同步,但是在底层中,迭代listIterator()iterator()却没有加synchronized关键字,因此我们在对synchronizedList进行遍历的时候一定不要忘了在外部也加上synchronized(list),以保证线程安全【如上介绍的防止迭代过程出现异常】。

这里其实是一个装饰器模式的应用,参数集合List将被装饰为SynchronizedList。通过对每个方法调用都进行同步加锁,使得多个线程读写ArrayList只能按序进行。这样的话,数据的安全性和一致性都得到了保证。同时缺点也很明显,每个线程读写ArrayList都需进行同步,开销大。

以上两种方式是比较常见的,下面我们介绍我们的重点,java.util.concurrent包下的CopyOnWriteArrayList这种解决方案。

  • 方案三使用CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList的详述

CopyOnWriteArrayList同样是线程安全的ArrayList,但是与SynchronizedList不同的是,它只对写操作加锁,对读操作不加锁。关键是,其在迭代期间不需要对容器进行加锁或复制。这一切都与"写入时复制"有关。

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

	/** 
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //【1】定义一个Lock锁,写操作的时候要上锁
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            Object[] elements = getArray();//【2】获取容器数组
            int len = elements.length;//【2】获取容器数组长度
            //【3】创建一个新的存储空间,容量+1,并将元素复制出新数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 【4】把新元素添加到新的存储空间里
            newElements[len] = e;
            // 【5】修改当前容器数组的引用,把原数组引用指向新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();//【6】解锁
        }
    }
	//setArray()的作用是给array赋值;其中,array是volatile transient Object[]类型,即array是“volatile数组”。
    final void setArray(Object[] a) {
        array = a;
    }

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;//容器数组的引用,指向存储当前元素的数组
//volatile能让变量变得可见,即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。正在由于这种特性,每次更新了“volatile数组”之后,其它线程都能看到对它所做的更新。transient关键字,它是在序列化中才起作用,transient变量不会被自动序列化。

写过程详述

第一,在”添加操作“开始前,获取独占锁(lock),若此时有需要线程要获取锁,则必须等待;在操作完毕后,释放独占锁(lock),此时其它线程才能获取锁。通过独占锁,来防止多线程同时修改数据!

第二,操作完毕时,会通过setArray()来更新”volatile数组“。而且,前面我们提过”即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入“;这样,每次添加元素之后,其它线程都能看到新添加的元素。

当add操作完成后,array的引用就已经指向另一个存储空间了。 这里也暴露了一个缺点:如果此容器的写操作比较频繁,那么其开销就比较大。

读过程详述

读的时候没有加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。get(int index)的实现很简单,就是返回”volatile数组“中的第index个元素。

public E get(int index) {
    return get(getArray(), index);
}

删除过程

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    // 获取“锁”
    lock.lock();
    try {
        // 获取原始”volatile数组“中的数据和数据长度。
        Object[] elements = getArray();
        int len = elements.length;
        // 获取elements数组中的第index个数据。
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        //如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组。
        //否则,新建数组,然后将”volatile数组中被删除元素之外的其它元素“拷贝到新数组中;最后,将新数组赋值给”volatile数组“。
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        // 释放“锁”
        lock.unlock();
    }
}
  • remove(int index)的作用就是将”volatile数组“中第index个元素删除。它的实现方式是,如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组。否则,新建数组,然后将”volatile数组中被删除元素之外的其它元素“拷贝到新数组中;最后,将新数组赋值给”volatile数组“。
  • 和add(E e)一样,remove(int index)也是”在操作之前,获取独占锁;操作完成之后,释放独占是“;并且”在操作完成时,会通过将数据更新到volatile数组中“。

迭代器的实现

CopyOnWriteArray有自己的迭代器,该迭代器不会检查修改状态,也无需检查状态。因为迭代的数组是可以说是只读的,不会有其他线程能够修改它。

迭代器引用的数组变量名就叫snapshot(快照)。也从另一个角度说明,在迭代器迭代过程中,其使用的是容器的过去一个版本,一个快照。不能保证是当前容器的状态。COWIterator不支持修改元素的操作。例如,对于remove(),set(),add()等操作,COWIterator都会抛出异常!另外,需要提到的一点是,CopyOnWriteArrayList返回迭代器不会抛出ConcurrentModificationException异常,即它不是fail-fast机制的!

这里也暴露了一个缺点不能保证数据的瞬时一致性

但是,其有一个显著的优点,那就是读操作和遍历操作不需要同步。多线程访问的时候,速度较高。

总结

说明

  1. CopyOnWriteArrayList实现了List接口,因此它是一个队列。
  2. CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了CopyOnWriteArrayList的互斥访问。
  3. CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。

“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。

  • CopyOnWriteArrayList的“动态数组”机制 :它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。

  • CopyOnWriteArrayList的“线程安全”机制 – 是通过volatile和互斥锁来实现的。(01) CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证。(02) CopyOnWriteArrayList通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。

应用场景

由以上的优缺点可得,CopyOnWriteArrayList应用的场景,最好是读操作多写操作相对较少的场景(“读多写少”)。也就是说,集合内容不会经常变动的。例如,网上常说的"黑名单"这类东西。

同步容器之CopyOnWriteArraySet

根据以上的测试方法测试HashSet在多线程的情况下插入输入,同样也会抛出ConcurrentModificationException异常,如下代码所示:

public class SetTest {

    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        for(int i=1; i<=30; i++){
            new Thread(()->{
                //插入随机数
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

模拟三十个线程同时插入数据,一样会抛出ConcurrentModificationException异常,因为Set集合本身也是不安全的,解决方案有以下两种:

  • 方案一:使用使用Collections工具类中的synchronizedSet方法将HashSet转为线程安全的集合。
Set<String> set = Collections.synchronizedSet(new HashSet<>());
  • 方案二:使用JUC下的CopyOnWriteArraySet集合
Set<String> set = new CopyOnWriteArraySet<>();

HashSet的源码分析

HashSet我们在学习集合的时候就已经学习过了【看我之前的文章】,下面我们再来回顾一下:

HashSet是基于HashMap实现的,底层数据结构是哈希表,主结构数组,HashSet的值存放在HashMap的key上,HashMap的Value统一为PRESENT,因此HashSet是一个无序集合且里面的元素唯一【因为HashMap的key是无序且唯一的】,允许插入一个null元素,但不允许有重复的值。HashSet基本上都是直接调用底层的HashMap的相关方法来实现的。下面我们对它的源码进行探究。

	//感觉超级懒,直接New一个HashMap,我们往下走,看它的add方法
	public HashSet() {
        map = new HashMap<>();
    }
	
	//add方法中我们可以看见,它使用map的put方法添加,添加的位置为key的位置,难怪说HashSet是一个无序、元素不重复的容器
	//然后我们发现再map的Value处有一个PRESENT,这个东西是什么呢?我们继续往下走
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
//我们可以看见,PRESENT就是一个不变的值,使用final修饰的,如果问为啥是不变的值的话,就需要好好的补一下final的作用

关于HashSet的源码分析,请见我总结的集合文章,虽然写的不是很全,但是看完还是有收获的。

为什么这里介绍HashSet的源码呢?因为后面会用到。下面进入主题。

CopyOnWriteArraySet的详述

CopyOnWriteArraySet是线程安全的无序集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。,CopyOnWriteArraySet底层使用一个CopyOnWriteArrayList来做代理,它的所有api都是依赖于CopyOnWriteArrayList来实现的,下面的代码也展示了这种代理的事实:

下面来分析一下CopyOnWriteArraySet的写操作实现,比如add方法:


    public boolean add(E e) {
        return al.addIfAbsent(e);
    }
    
    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
        //indexOf() 这个方法的作用是判断需要添加的元素在集合中是否存在,若存在则返回该元素的索引,主要用于去重使用;
		//如果集合中没有重复元素,则返回-1
    }
    
    private boolean addIfAbsent(E e, Object[] snapshot) {
        //定义一个Lock锁,写操作的时候要上锁
        final ReentrantLock lock = this.lock;
        //加锁操作
        lock.lock();
        try {
            //当线程并发写入时 获取最新的数组值,这里主要是为了获取最新的数组
            Object[] current = getArray();
            //获取当前容器数组长度
            int len = current.length;
            //判断两个数组对象是否相等,说明这里并发操作
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation:针对与另一个addXXX操作的竞争而优化
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            //建立一个新的容器数组,容量+1,并做拷贝处理
            Object[] newElements = Arrays.copyOf(current, len + 1);
            //将元素添加到新的容器中
            newElements[len] = e;
            //将引用指向新的容器
            setArray(newElements);
            return true;
        } finally {
            //解锁
            lock.unlock();
        }
    }    

set是一种不允许有重复元素的简单数据结构,所以和CopyOnWriteArrayList不同,CopyOnWriteArraySet需要add在插入新元素的时候多做一些判断,而CopyOnWriteArraySet在实现上使用了CopyOnWriteArrayListaddIfAbsent方法,这个方法的意思就是如果存在就不再插入,如果不存在再进行插入。

关于CopyOnWriteArraySet,它是通过CopyOnWriteArrayList实现的,它的API基本上都是通过调用CopyOnWriteArrayList的API来实现的。因此理解我上面总结的CopyOnWriteArrayList之后对CopyOnWriteArraySet也会有深刻的理解,后面不在叙述。

总结

  • 它最适合于集合大小通常保持较小,只读操作大大超过突变操作的应用程序,并且需要防止遍历期间线程之间的干扰。

  • 它是线程安全的。

  • 可变操作( addsetremove ,等)是昂贵的,因为它们通常意味着复制整个底层数组。

  • 迭代器不支持突变remove操作。

  • 遍历遍历迭代器是快速的,不能遇到来自其他线程的干扰。 迭代器构建时迭代器依赖于数组的不变快照。

同步容器之ConcurrentHashMap

看到这个东东是不是特别熟悉,没错,这就是面试的时候从HashMap到ConcurrentHashMap的连环炮,说实话学习容器的时候只是仅仅的听说过ConcurrentHashMap同时网上也有很多的关于它的描述等等,但那个时候压根没有用到过,现在不一样了,让我们从前往后一步步的学习ConcurrentHashMap吧。

HashMap的简介

在我们分析ConcurrentHashMap之前,我们先回顾一下关于HashMap的知识。

Map是一个键值对集合,里面存储着Key-Value之间的映射,Key无序且唯一,Value不要求有序且允许重复。

  • HashMap的结构

HashMap是我们常见的数据结构,在JDK1.7之前HashMap的底层数据结构采用的是数组+链表的数据结构,数组中存储着Key-Value这样的实例,它的数据节点是一个Entry节点;但是在JDK1.8之后,它的数据结构就变成了数组+链表+红黑树,把原来的一个Entry节点变成了一个Node节点,当链表长度大于8且数组长度大于64时会自动转化为红黑树。

  • 插入元素

当我们使用HashMap去put添加一个元素的时候,HashMap会利用哈希算法将插入元素的Key的HashCode进行重新哈希,并且计算出当前对象元素放在数组的下标是多少,然后将元素存储进去;当元素进行存储时出现Hash值相同的Key,那么这个时候就会使用equals方法去比较它们的Key是否相同,如果Key相同就进行值的覆盖,如果Key不同就将当前的元素放入到链表中去。

  • 插入元素的方式

说到插入元素到链表的方式,在JDK1.7之前采用的是头插法,意思就是新来的值会取代原有的值的位置,原有的值就被顺推到链表中去。

【头插法导致的问题:在进行数组扩容的时候,容易造成链表成环】

而JDK1.8之后采用的是尾插法,意思就是新来的值会往后追加到链表中,当链表长度大于8且数组长度大于64的时候,链表就会自动转化为红黑树。

  • 扩容机制

HashMap的底层采用的是数组进行存储的,而数组容量是有限的,数据的容量达到一定程度就会进行扩容,而扩容的两个重要因素就是LoadFactor(负载因子,默认0.75)和 Capacity(数组长度,默认是16),举例子当数组长度为100,当我们插入第76个元素的时候就需要进行扩容了,而HashMap的扩容就是先创建一个新的Entry空数组,长度为原数组的两倍,然后遍历原来的数组,将原数组中的元素重新Hash到新数组中。

	//HashMap的默认长度为16,注意这里使用的是位运算,同时为什么是16大家可以去查阅资料
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	//HashMap的负载因子为0.75f
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

【以上关于HashMap的介绍我就不多说了,大家可以看我的这篇文章,毕竟本文是有关多线程的,扯集合扯多了不太好】

HashMap的线程不安全问题

以上说了那么多,但是HashMap不是一个线程安全的容器,在并发情况下往HashMap中插入元素同样也会报java.util.ConcurrentModificationException的异常,如下所示:

public class MapTest {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        for(int i=1; i<=30; i++){
            new Thread(()->{
                //插入随机数
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

HashMap的线程不安全解决方案

  • 方案一:我们可以使用Hashtable代替HashMap,不建议使用

因为Hashtable基本上已经不建议使用了,具体原因www.baidu

  • 方案二:使用使用Collections工具类中的synchronizedSet方法将HashSet转为线程安全的集合。
Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
  • 方案三:使用JUC下的ConcurrentHashMap容器推荐
Map<String,String> map = new ConcurrentHashMap<>();

出于线程并发度的原因,一般会舍弃前两种方案而选择最后的ConcurrentHashMap,他的性能和效率明显高于前两者。下面我们就开始进入正题。

深入分析ConcurrentHashMap

待更新…

Callable详解【创建线程的第三种方式】

Callable的简述

创建线程我们前面介绍了几种方式,【1、继承Thread类】【2、实现Runnable接口】【3、实现Callable接口】,前面两种方式我这里就不介绍了,下面我们来介绍一下创建线程的第三种方式【3、实现Callable接口】。

Callable是java.util.concurrent包下的一个接口,在JDK1.5之后才有新的创建线程的方式,支持泛型、并且有返回值、可以抛出异常,并且与前面两种创建线程方式的区别是:Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

通过Callable创建线程

下面我们就通过代码来简单实现一下通过Callable来创建线程:

  • 第一步:创建实现Callable接口的类MyCallable,并且重写call()方法,指定返回值类型【我这里是String】;
//1.创建一个线程类,实现Callable接口,
//2.Callable会报一个黄色的警告,原因:可以加个泛型,这个泛型,是返回值对应的类型。
//3.我们这里是返回一个字符串,所以加个泛型String
public class MyCallable implements Callable<String>{
    //4.一旦上面的泛型确定了,那么这个重写的方法的返回值类型就是String了。
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"  call()方法的执行");
        return "你好啊";
    }
}
  • 第二步:以MyCallable为参数创建FutureTask对象;
  • 第三步:将FutureTask作为参数创建Thread对象;
  • 第四步:调用线程对象的start()方法;
public class TestCallable {
    //5.写main方法测试
    public static void main(String[] args) {
        //6.创建一个线程对象,但是Callable的实现对象不能直接传入Thread,需要借助FutureTask模板
        FutureTask<String> futureTask = new FutureTask<String>(new MyCallable());
        //FutureTask实现了Runnable接口所以可以传入Thread
        Thread thread = new Thread(futureTask);
        thread.start();//8.上面已经将线程启动了,直接运行是没有结果的
        try {
            TimeUnit.SECONDS.sleep(2);
            //9.我们必须要对返回值处理,那么如何接收返回值,get方法中可以加入sleep,验证get是个阻塞方法
            System.out.println("返回结果:"+futureTask.get());
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"  main方法执行完毕");
    }
}

Callable 与Future 的叙述

Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。

Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。

FutureTask是什么?为什么我们可以通过Thread与Callable 产生联系?

FutureTask 表示一个异步运算的任务,它是Runnable接口中的一个实现类;

FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

因此,Callable接口本身与Thread是无法联系的,但是它可以通过FutureTask 与Runnable接口产生联系,再通过Runnable与Thread产生联系。

FutureTask 的get()为什么会产生阻塞?

这种线程方式,最根本的实现类就是FutureTask:因此我们看FutureTask 的源码,如下所示:

在看run方法前,你要明白,这个线程走完,最终state的数值,从1(new)变为 2(completing) 变为 3(normal) 那么线程就执行完了。

接下来我们去看一下构造器:

再去看FutureTask的run方法:

接下来我们再去看一下get()方法的源码:

现在知道为啥get()会阻塞了么?如果还不懂的话,可能我的说明不够给力【多看几遍】,这个时候还请另寻资源了,hhhh~

并发辅助类【必会知识点】

在JUC下包含了一些常用的辅助工具类,其中最常用的三个就是CountDownLatch、CyclicBarrier、Semaphore,下面我们就来学习一下这三个的使用方法以及它们之间的区别。

闭锁-CountDownLatch

介绍CountDownLatch是一个同步辅助器,允许一个或多个线程一直等待,直到一组在其他线程执行的操作全部完成。

(1)构造方法

它的构造方法中会传入一个count值,用于锁存器计数,如下所示:

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

(2)常用API

它常用的两个API就是:await()countDown(),两个方法的作用介绍如下所示:

  • public void countDown():减少锁存器的计数,如果计数达到零,释放所有等待的线程。 如果当前计数大于零,则它将递减。 如果新计数为零,则所有等待的线程都将被重新启用以进行线程调度。如果当前计数等于零,那么没有任何反应。

  • public void await() throws InterruptedException:导致当前线程阻塞直到锁存器计数到零,除非线程是interrupted ,否则会一直阻塞。 如果当前计数为零,则此方法立即返回。如果当前计数大于零,则当前线程将被禁用以进行线程调度,并处于休眠状态。直至发生两件事情之一才会: 由于countDown()方法的调用,计数达到零,要么 一些其他线程interrupts当前线程。

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  

(3)使用说明

首先count初始化CountDownLatch,然后需要等待的线程调用await方法。await方法会使线程一直受阻塞直到count=0。而其它线程完成自己的操作后,调用countDown()使计数器count减1。当count减到0时,所有在等待的线程均会被释放,说白了就是通过count变量来控制等待,如果count值为0了(其他线程的任务都完成了),那就可以继续执行。

下面我们使用代码示例来说明:

(4)代码示例一

/**
 * 并发辅助类-CountDownLatch:倒计时器
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6,必须要执行任务的时候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for(int i=1;i<=6;i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"-Go Out...");
                countDownLatch.countDown();//数量-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await();//等待计数器归0,当计数器为0后再往下执行
        System.out.println("Close door!!!");
    }
}

输出

1-Go Out...
2-Go Out...
4-Go Out...
3-Go Out...
5-Go Out...
6-Go Out...
Close door!!!

上述代码中我创建了一个countDownLatch示例,计数量为6,这表示需要有6个线程来完成任务【这里的计数量初始值不一定是线程的数量,一个线程也可以countDown多次,看你怎么用】,其他线程需要等待在CountDownLatch上的线程执行完才能继续执行【这里主要就是主线程等待】。然后我通过for循环6次分别创建6个线程来完成任务,每一个线程完成一个任务计数量就减一,countDownLatch.countDown()方法作用是通知CountDownLatch有一个线程已经准备完毕,倒计数器可以减一了,当计数量减为0的时候,主线程就会被释放执行,并且随后对wait()方法的调用会立即返回【一次性,count不会被重置,如果想要count被重置,考虑使用CyclicBarrier】,从而输出后面的关门。countDownLatch.await()方法要求主线程等待所有6个检查任务全部准备好才一起并行执行。

(5)代码示例二

以上的例子可能说得不太清楚,下面我们再使用一个代码示例来说明一下,例子是:oldou在图书馆自修室学习,已经晚上10点半了,oldou看着自修室还有6个同学没有走,oldou想等这6个同学走了之后再走。

public class CountDownLatchDemo2 {
    public static void main(String[] args) {
        //创建一个CountDownLatch示例,计数量为6,表示还有6位同学没走
        final CountDownLatch latch = new CountDownLatch(6);
        System.out.println("现在已经是晚上10点半了");

        //oldou线程启动成功
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();//oldou看到其他同学还没有走,于是就阻塞等待,等他们先走
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("其他同学走完了,oldou终于可以走了");
            }
        }).start();

        //启动其他同学的线程
        for(int i=1; i<=6; i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"--走了...");
                latch.countDown();//走一个同学数量就减一
            }).start();
        }
    }
}

输出:

现在已经是晚上10点半了
Thread-1--走了...
Thread-2--走了...
Thread-4--走了...
Thread-3--走了...
Thread-5--走了...
Thread-6--走了...
其他同学走完了,oldou终于可以走了

通过这个例子懂了没?结合例子1的说明理解一下这里的流程。

(6)总结

1、CountDownLatch countDownLatch= new CountDownLatch(count); //构造对象时候 需要传入参数count,表示计数量

2、countDownLatch.await() 能够阻塞线程,直到调用countDownLatch.countDown() 方法将count减为0才释放线程

3、countDownLatch.countDown() 可以在多个线程中调用 ,计算调用次数是所有线程调用次数的总和

栅栏-CyclicBarrier

A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point . The barrier is called cyclic because it can be re-used after the waiting threads are released.

介绍:CyclicBarrier是一种同步辅助工具,它允许一组线程互相等待以到达某个公共屏障点。屏障称为循环屏障,因为它可以在释放等待线程之后重新使用。叫做cyclic是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用(对比于CountDownLatch是不能重用的)。CyclicBarrier可以将其看作是加法计数器,到达一定数量的时候就会执行某操作,不然之前的线程会阻塞等待。

java.util.concurrent.CyclicBarrier类是一个同步机制。它可以通过一些算法来同步线程处理的过程。换言之,就是所有的线程必须等待对方,直到所有的线程到达屏障,然后继续运行。之所以叫做“循环屏障”,是因为这个屏障可以被重复使用。原理如下图所示【图来源于网络】:

(1)构造方法

CyclicBarrier 提供了两种构造方法:

    public CyclicBarrier(int parties) {
        this(parties, null);
    }
    public CyclicBarrier(int parties, Runnable barrierAction) {
        //如果线程数小于等于0就会抛出异常IllegalArgumentException
        if (parties <= 0) throw new IllegalArgumentException();
        // parties表示“必须同时到达barrier的线程个数”
        this.parties = parties;
         // count表示“处在等待状态的线程个数”。
        this.count = parties;
        // barrierCommand表示“parties个线程到达barrier时,会执行的动作”。
        this.barrierCommand = barrierAction;
    }

第一个构造方法的参数,指的是需要几个线程一起到达,才可以使所有线程取消等待。创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。

第二个构造方法,额外指定了一个参数,用于在所有线程达到屏障时,优先执行 barrierAction。创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。

(2)让线程在CyclicBarrier中等待

有两个方法可以让线程在CyclicBarrier处等待:

  • barrier.await();

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen;
        }
    }
    

    说明await()是通过dowait()实现的。

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        // 获取“独占锁(lock)”
        lock.lock();
        try {
            // 保存“当前的generation”
            final Generation g = generation;
    
            // 若“当前generation已损坏”,则抛出异常。
            if (g.broken)
                throw new BrokenBarrierException();
    
            // 如果当前线程被中断,则通过breakBarrier()终止CyclicBarrier,唤醒CyclicBarrier中所有等待线程。
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
    
           // 将“count计数器”-1
           int index = --count;
           // 如果index=0,则意味着“有parties个线程到达barrier”。
           if (index == 0) {  // tripped
               boolean ranAction = false;
               try {
                   // 如果barrierCommand不为null,则执行该动作。
                   final Runnable command = barrierCommand;
                   if (command != null)
                       command.run();
                   ranAction = true;
                   // 唤醒所有等待线程,并更新generation。
                   nextGeneration();
                   return 0;
               } finally {
                   if (!ranAction)
                       breakBarrier();
               }
           }
    
            // 当前线程一直阻塞,直到“有parties个线程到达barrier” 或 “当前线程被中断” 或 “超时”这3者之一发生,
            // 当前线程才继续执行。
            for (;;) {
                try {
                    // 如果不是“超时等待”,则调用awati()进行等待;否则,调用awaitNanos()进行等待。
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    // 如果等待过程中,线程被中断,则执行下面的函数。
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        Thread.currentThread().interrupt();
                    }
                }
    
                // 如果“当前generation已经损坏”,则抛出异常。
                if (g.broken)
                    throw new BrokenBarrierException();
    
                // 如果“generation已经换代”,则返回index。
                if (g != generation)
                    return index;
    
                // 如果是“超时等待”,并且时间已到,则通过breakBarrier()终止CyclicBarrier,唤醒CyclicBarrier中所有等待线程。
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            // 释放“独占锁(lock)”
            lock.unlock();
        }
    }
    

    说明:dowait()的作用就是让当前线程阻塞,直到“有parties个线程到达barrier” 或 “当前线程被中断” 或 “超时”这3者之一发生,当前线程才继续执行。

    (01)generation是CyclicBarrier的一个成员遍历,它的定义如下:

    private Generation generation = new Generation();
    private static class Generation {
        boolean broken = false;
    }
    

​ 在CyclicBarrier中,同一批的线程属于同一代,即同一个Generation;CyclicBarrier中通过generation对象,记录属于哪一代。当有parties个线程到达barrier,generation就会被更新换代。

​ (02)如果当前线程被中断,即Thread.interrupted()为true;则通过breakBarrier()终止CyclicBarrier。breakBarrier()的源码如下:

private void breakBarrier() {
    //设置当前中断标记broken为true,意味着“将该Generation中断”;
    generation.broken = true;
    //设置count=parties,即重新初始化count;
    count = parties;
    //通过signalAll()唤醒CyclicBarrier上所有的等待线程。
    trip.signalAll();
}

​ (03)将“count计数器”-1,即–count;然后判断是不是“有parties个线程到达barrier”,即index是不是为0。
当index=0时,如果barrierCommand不为null,则执行该barrierCommand,barrierCommand就是我们创建CyclicBarrier时,传入的Runnable对象。然后,调用nextGeneration()进行换代工作,nextGeneration()的源码如下:

private void nextGeneration() {
    //调用signalAll()唤醒CyclicBarrier上所有的等待线程;
    trip.signalAll();
    //重新初始化count;
    count = parties;
    //更新generation的值。
    generation = new Generation();
}

​ (04) 在for(;;)循环中。timed是用来表示当前是不是“超时等待”线程。如果不是,则通过trip.await()进行等待;否则,调用awaitNanos()进行超时等待。

  • barrier.await(10, TimeUnit.SECONDS);

此方法指线程等待的超时时间,当出现等待超时的时候,当前线程会被释放,但会像其他线程传播出BrokenBarrierException异常。

所有线程在CyclicBarrier等待指的是:

• 最后一个线程到达(调用await方法)

• 一个线程被被另外一个线程中断(另外一个线程调用了这个现场的interrupt()方法)

• 其中一个等待的线程被中断

• 其中一个等待的线程超时

• 一个外部的线程调用了CyclicBarrier.reset()方法。

(3)代码示例一:收集七颗龙珠召唤神龙

public class CyclicBarrierDemo {
    /**
     * 集齐七颗龙珠召唤神龙
     */
    public static void main(String[] args) {
        //召唤神龙的线程
        CyclicBarrier barrier = new CyclicBarrier(7,()->{
            System.out.println(Thread.currentThread().getName()+"---->召唤神龙成功...Success!");
        });

        for(int i=1;i<=7;i++){
            //由于lambda表达式不能操作到i,因此需要借助一个final修饰的变量
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集了第"+temp+"颗龙珠!");
                try {
                    barrier.await();//等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

输出:

Thread-0收集了第1颗龙珠!
Thread-1收集了第2颗龙珠!
Thread-2收集了第3颗龙珠!
Thread-3收集了第4颗龙珠!
Thread-4收集了第5颗龙珠!
Thread-5收集了第6颗龙珠!
Thread-6收集了第7颗龙珠!
Thread-6---->召唤神龙成功...Success!

首先创建一个CyclicBarrier,将一定数量【参与者】线程在到达屏障之前处于等待状态【这里是7个】,7个线程到达之后才能取消其他线程的等待,同时当7个线程到达屏障时,会优先执行给定的屏障操作【输出语句】,该操作由最后一个进入 barrier 的线程执行

思考:在以上代码示例中,为什么for当中的i在lambda表达式中取不到值?大家可以在评论当中说明噢~

(4)Barrier被破坏情况

如果在参与者(线程)在等待的过程中,Barrier被破坏,就会抛出BrokenBarrierException。可以用isBroken()方法检测Barrier是否被破坏。

  • 情况一:如果有线程已经处于等待状态,调用reset方法会导致已经在等待的线程出现BrokenBarrierException异常。并且由于出现了BrokenBarrierException,将会导致始终无法等待。

  • 情况二:如果在等待的过程中,线程被中断(某个线程调用了interrupt方法),也会抛出BrokenBarrierException异常,并且这个异常会传播到其他所有的线程。

  • 情况三:如果在执行屏障操作过程中发生异常,则该异常将传播到当前线程中,其他线程会抛出BrokenBarrierException,屏障被损坏。

  • 情况四:如果超出指定的等待时间,当前线程会抛出 TimeoutException 异常,其他线程会抛出BrokenBarrierException异常。

OK,这个辅助类就介绍到这里了,其他的东西大家可以去网上查看资料,这里由于能力有限,就介绍这么多了。

信号量-Semaphore

Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource.

介绍:Semaphore是一个计数信号量,它的作用是限制访问特定资源的线程数目,它维护了一组**“许可证”**。一般可用于流量的控制。【限流的作用】

Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。

以一个停车场是运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这是如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。
  在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
  更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait等待)操作时,它要么通过然后将信号量减一,要么一自等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是应为加操作实际上是释放了由信号量守护的资源。

(1)构造方法

  • 如下所示,Semaphore的一个构造函数【Semaphore(int permits)】,可以传入一个 int 型整数permits表示某段代码最多只有permits个线程可以访问,如果超出了permits,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果往Semaphore 构造函数中传入的 int 型整数 permits = 1,相当于变成了一个 synchronized 了。
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

  • Semaphore还有一个构造函数【public Semaphore(int permits, boolean fair)】,可以创建具有给定的许可数permits和给定的公平设置的Semaphore,默认是false【非公平锁】,可以传入 true 来设置为公平锁。【何为公平锁/非公平锁,见后文】。 当设置为false时,此类不会保证线程获取许可的顺序。 也就是说,一个线程调用acquire()可以提前获取已经等待线程分配的许可证,也就是说自己可以插队。 当设置为true时,信号量保证调用acquire方法的线程被选择以按照它们调用这些方法的顺序获得许可(先进先出; FIFO),设置为true时,下次执行的线程会是等待最久的线程。
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

(2)常用API介绍

  • acquire() 获取许可,假设如果已经满了,那么其他线程就进行阻塞等待,直到某个线程被释放为止!

    从此信号量获取一个许可,在提供一个许可前线程将一直阻塞,否则线程被中断。获取一个许可(如果提供了一个)并立即返回,将可用的许可数减 1。如果没有可用的许可,则在发生以下两种情况之一,禁止将当前线程用于线程安排目的并使其处于休眠状态:

    【1】某些其他线程调用此信号量的 release() 方法,并且当前线程是下一个要被分配许可的线程;或者其他某些线程中断当前线程。

    【2】如果当前线程:被此方法将其已中断状态设置为 on ;或者在等待许可时被中断。则抛出 InterruptedException,并且清除当前线程的已中断状态。

  • release() 表示释放许可,会将当前的信号量释放 + 1,然后唤醒等待的线程!

    释放一个许可,将其返回给信号量。释放一个许可,将可用的许可数增加 1。如果任意线程试图获取许可,则选中一个线程并将刚刚释放的许可给予它。然后针对线程安排目的启用(或再启用)该线程。
    不要求释放许可的线程必须通过调用 acquire() 来获取许可。通过应用程序中的编程约定来建立信号量的正确用法。

(3)代码示例

我们就用上面说到的抢车位的例子来说明,代码中给出了三个车位6位大佬来抢,看以下谁能抢到。

/**
 * Semaphore 计数信号量
 * 实现抢车位
 */
public class SemaphoreDemo {

    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(2);//睡两秒
                    System.out.println(Thread.currentThread().getName()+"-->离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //释放一个许可证
                    semaphore.release();
                }
            },"大佬"+String.valueOf(i)+"号").start();
        }
    }
}

输出:

大佬1号-->抢到了车位
大佬2号-->抢到了车位
大佬3号-->抢到了车位
大佬1号-->离开了车位
大佬4号-->抢到了车位
大佬2号-->离开了车位
大佬3号-->离开了车位
大佬5号-->抢到了车位
大佬6号-->抢到了车位
大佬4号-->离开了车位
大佬6号-->离开了车位
大佬5号-->离开了车位

说明:首先创建Semaphore对象,并给出三个许可证,也就是车库里的三个车位,守门大爷就在车库门口守着,之后创建6个大佬去抢这三个车位,先进入车库的三位大佬【1,2,3】就用着车位,这个时候守门大爷将门关了,其他大佬进不去车库就只能在外面等着【阻塞】,当其中一位大佬【1号】将车开走了【释放了一个许可证】,这个时候门卫大爷又将门打开了,但是只能进入一个,你们想进去停车的自己抢,4号牛批抢先开车溜进去了【拿到了这个许可证】,这个时候门卫大爷又将门关了。。。以此类推。。

(4)使用场景:可用于流量控制,限制最大的并发访问数。

好了,关于Semaphore就介绍到这里了【能力有限】,下面还有一大波僵尸正在来临。。。。

总结

  1. CountDownLatch 是一个线程等待其他线程, CyclicBarrier 是多个线程互相等待。
  2. CountDownLatch 的计数是减 1 直到 0,CyclicBarrier 是加 1,直到指定值。
  3. CountDownLatch 是一次性的, CyclicBarrier 可以循环利用。
  4. CyclicBarrier 可以在最后一个线程达到屏障之前,选择先执行一个操作。
  5. Semaphore 会限制访问特定资源的线程数目,需要拿到许可才能执行,没有许可证就阻塞,并可以选择公平和非公平模式。
  • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

CountDownLatch和CycliBarrier的区别?

CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:

  • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

  • CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。

  • 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;

  • CountDownLatch的方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;

  • CountDownLatch是不能复用的,而CyclicLatch是可以复用的。

浅谈读写锁-ReentrantReadWriteLock

ReadWriteLock的介绍

ReadWriteLock是java.util.concurrent.locks包下的一个接口,它管理着一组锁,一个用于只读的锁【读锁ReadLock】,一个用于写的锁【写锁WriteLock】,读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占的,写的时候只能有一个线程去写。也就是说:读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞

  • 所有读写锁的实现必须确保写操作对读操作的内存影响。也就是一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。

  • 读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。

ReadWriteLockd 接口中的方法只有两个:readLock()writeLock

public interface ReadWriteLock {
    /**
     * 返回读锁
     */
    Lock readLock();

    /**
     * 返回写锁
     */
    Lock writeLock();
}

ReadWriteLock的实现类只有一个,那就是ReentrantReadWriteLock,没错,这就是我们这部分介绍的重点,下面进入正题。

ReentrantReadWriteLock的介绍

ReentrantReadWriteLock读写锁,与ReentrantLock一样默认非公平,内部定义了读锁ReadLock()和写锁WriteLock(),在同一时间允许被多个读线程访问,但在写线程访问时,所有读线程和写线程都会被阻塞。读写锁主要特性:

  • 公平性选择:支持非公平性(默认)和公平的锁获取方式。
  • 可重入性:允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。
  • 锁降级:允许写锁降低为读锁。
  • 中断锁的获取:在读锁和写锁的获取过程中支持中断。
  • 支持Condition:写锁提供Condition实现。
  • 监控:提供确定锁是否被持有等辅助方法。

构造方法

    //默认的构造方法使用的是非公平模式
	public ReentrantReadWriteLock() {
        this(false);
    }
	//默认的构造方法使用的是非公平模式
    public ReentrantReadWriteLock(boolean fair) {
        //创建的Sync是NonfairSync对象,然后初始化读锁和写锁。
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
	//一旦初始化后,ReadWriteLock接口中的两个方法就有返回值了
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

在ReentrantReadWriteLock中定义了两个内部类ReadLockWriteLock,分别来实现读锁和写锁。ReentrantReadWriteLock底层是通过AQS来实现锁的获取与释放的【啥是AQS后面解释】,因此ReentrantReadWriteLock内部还定义了一个继承了AQS类的同步组件Sync,同时ReentrantReadWriteLock还支持公平与非公平性,因此它内部还定义了两个内部类FairSync、NonfairSync,它们继承了Sync。下面我们来详细的介绍一下写锁和读锁。

写锁的获取与释放

(1)写锁获取

WriteLock也是独占锁【一次只能被一个线程占有】,那么他和ReentrantLock有什么区别呢?最大的区别就在获取锁时WriteLock不仅需要考虑是否有其他写锁占用,同时还要考虑是否有其他读锁,而ReentrantLock只需要考虑自身是否被占用就行了。来看下WriteLock获取锁的源代码:


		protected final boolean tryAcquire(int acquires) {
             // 获取当前线程
            Thread current = Thread.currentThread();
            // 获取写锁当前的同步状态
            int c = getState();
            // 获取写锁获取的次数
            int w = exclusiveCount(c);
             //存在读锁或者写锁
            if (c != 0) {
               // 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话,当前线程获取写锁失败
                //写锁为0(证明有读锁),或者持有写锁的线程不为当前线程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 最多65535次重入,若超过报错  
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //当前线程持有写锁,为重入锁,+acquires即可
                setState(c + acquires);
                return true;
            }
           // 写锁未被任何线程获取,当前线程可获取写锁
            if (writerShouldBlock() ||
                 //CAS操作失败,多线程情况下被抢占,获取锁失败。CAS成功则获取锁成功
                !compareAndSetState(c, c + acquires))
            
                return false;
              // 设置获取锁的线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }

从源码中我们可以发现getState()获取的是读锁与写锁总同步状态,再通过exclusiveCount()方法来获取是否存在写锁,然后通过c != 0和w == 0判断了是否存在读锁。

    static final int SHARED_SHIFT   = 16;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
        
    static int exclusiveCount(int c) {
        return c & EXCLUSIVE_MASK; 
    }

其中EXCLUSIVE_MASK为: static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;如上所示,EXCLUSIVE _MASK为1左移16位然后减1,即为0x0000FFFF。而exclusiveCount方法是将同步状态(state为int类型)与0x0000FFFF相与,即取同步状态的低16位。那么低16位代表什么呢?根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论同步状态的低16位用来表示写锁的获取次数。其示意图如下图所示:

现在我们回过头来看写锁获取方法tryAcquire,其主要逻辑为:当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写锁同步状态

(2)写锁释放

protected final boolean tryRelease(int releases) {
    // 判断是否是当前线程持有锁,若释放的线程不为锁的持有者
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
	// 同步状态减去写状态【state-releases】, 重新设置同步状态
    int nextc = getState() - releases;
	// 当前写状态是否为0,若新的写锁持有线程数为0,则将锁的持有线程置为null
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
	// 不为0则更新同步状态
    setState(nextc);
    return free;
}

写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。只需要用当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的

读锁的加锁与释放

这里参考:https://zhuanlan.zhihu/p/91408261

或者:https://juejin.im/post/6844903650133803021#heading-1

或者后面我专门再总结一下关于锁的介绍以及源码分析,这里不过多介绍!

锁降级

读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,但不支持锁升级。锁降级指的是写锁降级成为读锁,即先获取写锁、获取读锁在释放写锁的过程,目的为了保证数据的可见性。假设有两个线程A、B,若线程A获取到写锁,不获取读锁而是直接释放写锁,这时线程B获取了写锁并修改了数据,那么线程A无法知道线程B的数据更新。如果线程A获取读锁,即遵循锁降级的步骤,则线程B将会被阻塞,直到线程A使用数据并释放读锁之后,线程B才能获取写锁进行数据更新。

使用ReentrantReadWriteLock实现一个简单的缓存

为了实现简单,不考虑缓存过期策略等复杂因素。

  • 缓存主要提供两个功能:读和写。

  • 读时如果缓存中存在数据,则立即返回数据。

  • 读操作时如果缓存中不存在数据,则需要从其他途径获取数据,同时写入缓存。

  • 在写入缓存的同时,为了避免其他线程同时获取这个缓存中不存在的数据,需要阻塞其他读线程。 下面我们就来通过ReentrantReadWriteLock实现上述功能:

未加锁的问题

public class TestReentrantReadWriteLock {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //开5个线程写数据
        for(int i=1; i<=5; i++){
            final int temp = i;
            new Thread(()->{
                myCache.put(temp+"",temp+"");
            }).start();
        }
        //开5个线程读数据
        for(int i=1; i<=5; i++){
            final int temp = i;
            new Thread(()->{
                myCache.get(temp+"");
            }).start();
        }
    }
}

/**
 * 自定义缓存,不加锁,不安全
 */
class MyCache{
    private volatile Map<String,Object> map = new HashMap<>();
    //存数据,写操作
    public void put(String Key,Object Value){
        System.out.println(Thread.currentThread().getName()+"写入数据-->"+Key);
        map.put(Key,Value);
        System.out.println(Thread.currentThread().getName()+"写入数据OK!");
    }

    //取数据,读操作
    public void get(String Key){
        System.out.println(Thread.currentThread().getName()+"读取数据-->"+Key);
        map.get(Key);
        System.out.println(Thread.currentThread().getName()+"读取OK!!!");
    }
}

Thread-0写入数据-->1
Thread-2写入数据-->3
Thread-2写入数据OK!
Thread-1写入数据-->2
Thread-1写入数据OK!
Thread-0写入数据OK!
Thread-3写入数据-->4
Thread-3写入数据OK!
Thread-4写入数据-->5
Thread-4写入数据OK!
Thread-6读取数据-->2
Thread-6读取OK!!!
Thread-7读取数据-->3
Thread-5读取数据-->1
Thread-7读取OK!!!
Thread-5读取OK!!!
Thread-8读取数据-->4
Thread-9读取数据-->5
Thread-8读取OK!!!
Thread-9读取OK!!!

我们没有加锁时,在多线程情况下,写入数据时被其他线程进行了插队,有干扰,不是那种我插入数据插入成功之后才能读我的数据或者我数据还没插入成功就被其他线程进行下一次数据的插入了,因此下面我们使用读写锁来简单实现一下。

加锁成功解决

public class TestReentrantReadWriteLock {

    public static void main(String[] args) {
        MyCacheLock myCacheLock = new MyCacheLock();
        //开5个线程写数据
        for(int i=1; i<=5; i++){
            final int temp = i;
            new Thread(()->{
                myCacheLock.put(temp+"",temp+"");
            }).start();
        }
        //开5个线程读数据
        for(int i=1; i<=5; i++){
            final int temp = i;
            new Thread(()->{
                myCacheLock.get(temp+"");
            }).start();
        }
    }
}

/**
 * 加锁缓存
 */
class MyCacheLock{
    private volatile Map<String,Object> map = new HashMap<>();
    // 读写锁: 更加细粒度的控制
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //存数据,写操作
    public void put(String Key,Object Value){
        //加写锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"写入数据-->"+Key);
            map.put(Key,Value);
            System.out.println(Thread.currentThread().getName()+"写入数据OK!");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //解写锁
            readWriteLock.writeLock().unlock();
        }
    }
    //取数据,读操作
    public void get(String Key){
        //加读锁
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"读取数据-->"+Key);
            map.get(Key);
            System.out.println(Thread.currentThread().getName()+"读取OK!!!");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //解读锁
            readWriteLock.readLock().unlock();
        }
    }
}

输出:

Thread-0写入数据-->1
Thread-0写入数据OK!
Thread-1写入数据-->2
Thread-1写入数据OK!
Thread-3写入数据-->4
Thread-3写入数据OK!
Thread-2写入数据-->3
Thread-2写入数据OK!
Thread-4写入数据-->5
Thread-4写入数据OK!
Thread-5读取数据-->1
Thread-5读取OK!!!
Thread-6读取数据-->2
Thread-7读取数据-->3
Thread-8读取数据-->4
Thread-8读取OK!!!
Thread-6读取OK!!!
Thread-9读取数据-->5
Thread-9读取OK!!!
Thread-7读取OK!!!

加入读写锁以后,写数据只有一个线程去完成,其他线程阻塞【独占锁】,我们读数据可以有多个线程一起去读【共享锁】。

总结

当有线程获取读锁时,不允许再有线程获得写锁
当有线程获得写锁时,不允许其他线程获得读锁和写锁
写锁能降级为读锁,读锁无法升级成写锁

阻塞队列-BlockingQueue

BlockingQueue的介绍

BlockingQueue【阻塞队列】是java.util.concurrent包下的一个接口,它是一个支持两个附加操作的队列,这两个附加的操作支持阻塞的插入和移除方法。

  • 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列元素被取出,有空位。

  • 支持阻塞的移除方法:当队列为空时,获取元素的线程会阻塞等待,直到队列中有元素。

BlockingQueue很好的帮助我们解决在多线程中如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。

最常用的"生产者-消费者"问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。

BlockingQueue的四组API

BlockingQueue基本操作都是基于下面四组API来进行操作的,在实际使用时根据自己需求选择下面的某组API,下面开始介绍:

方法/处理方式抛出异常返回特殊值阻塞、等待超时退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove(e)poll()take()poll(time,unit)
检查方法element()peek()--
  • 抛出异常:当队列满了以后,如果再往队列中插入元素,就会抛出IllegalStateException: Queue full异常。当队列为空时,从队列中获取元素就会抛出NoSuchElementException异常。

  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果队列中没有元素,调用移除方法就返回null

  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。

  • 超时退出:当阻塞队列满时,如果生产者线程往队列里offer添加元素,队列会等待一段时间【时间通过参数设置】,时间到了之后就会退出等待。同理当队列空时,如果消费者从队列里poll元素,队列会阻塞住消费者线程一段时间,时间到了之后退出等待。

注意:如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true。

这里我给出其中的2组API的测试代码

/**
* 抛出异常
*/
public static void test1(){
    // 队列的大小设置为3
    ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.add("a"));
    System.out.println(blockingQueue.add("b"));
    System.out.println(blockingQueue.add("c"));
    // IllegalStateException: Queue full 抛出异常!
    // System.out.println(blockingQueue.add("d"));
    System.out.println("=-===========");
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());
    // java.util.NoSuchElementException 抛出异常!
    // System.out.println(blockingQueue.remove());
}
/**
 * 有返回值,没有异常
 */
public static void test2(){
    // 队列的大小设置为3
    ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("a"));
    System.out.println(blockingQueue.offer("b"));
    System.out.println(blockingQueue.offer("c"));//这个进队列之后就满了
    // System.out.println(blockingQueue.offer("d")); // false 不抛出异常!
    System.out.println("============================");
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());//取出第三个了,队列中已经没有了
    System.out.println(blockingQueue.poll()); //再取就返回 null,但 不抛出异常!
}

其它API的介绍:

public interface BlockingQueue<E> extends Queue<E> {

    //将给定元素设置到队列中,如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
    boolean add(E e);

    //将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
    boolean offer(E e);

    //将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
    void put(E e) throws InterruptedException;

    //将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    //从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
    E take() throws InterruptedException;

    //在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法,为null则直接返回null。
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    //获取队列中剩余的空间。
    int remainingCapacity();

    //从队列中移除指定的值。
    boolean remove(Object o);

    //判断队列中是否拥有该值。
    public boolean contains(Object o);

    //将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection<? super E> c);

    //指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection<? super E> c, int maxElements);
}

BlockingQueue的实现类【转】

BlockingQueue 的实现类有很多,有ArrayBlockingQueueDelayQueueLinkedBlockingDequeLinkedBlockingQueueLinkedTransferQueuePriorityBlockingQueueSynchronousQueue。而这几种常见的阻塞队列也是在实际编程中会常用的,如下表所示:

队列有界性数据结构
ArrayBlockingQueuebounded(有界)加锁ArrayList
LinkedBlockingQueueoptionally-bounded(可选)加锁LinkedList
PriorityBlockingQueueunbounded(无界)加锁heap
DelayQueueunbounded(无界)加锁heap
SynchronousQueuebounded(有界)加锁
LinkedTransferQueueunbounded(无界)加锁heap
LinkedBlockingDequeunbounded(无界)无锁heap

下面我们简单介绍一下这些实现类。

ArrayBlockingQueue- 数组阻塞队列

  • ArrayBlockingQueue是一个用数组实现的有界阻塞队列,支持公平锁和非公平锁。默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue。,如果保证公平性,通常会降低吞吐量。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁。】如果需要获得公平性的ArrayBlockingQueue,可采用如下代码:

    private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
    
  • 此队列按照先进先出(FIFO)的原则对元素进行排序。因此,对头元素时队列中存在时间最长的数据元素,而对尾数据则是当前队列最新的数据元素。

  • ArrayBlockingQueue可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue一旦创建,容量不能改变。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。

  • 参数以及构造方法如下所示:

    // 存储队列元素的数组
    final Object[] items;
    
    // 拿数据的索引,用于take,poll,peek,remove方法
    int takeIndex;
    
    // 放数据的索引,用于put,offer,add方法
    int putIndex;
    
    // 元素个数
    int count;
    
    // 可重入锁
    final ReentrantLock lock;
    // notEmpty条件对象,由lock创建
    private final Condition notEmpty;
    // notFull条件对象,由lock创建
    private final Condition notFull;
    
    //capacity表示最多允许三个线程入队列
    public ArrayBlockingQueue(int capacity) { 
         //会调用下面的构造方法,默认是非公平锁
        this(capacity, false);
    }
    /** 
    * 构造函数。capacity设置数组大小 ,fair设置是否为公平锁 
    */  
    public ArrayBlockingQueue(int capacity, boolean fair) {
        //如果允许入队列的数小于等于0就抛出IllegalArgumentException异常
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        //是否为公平锁,如果是的话,那么先到的线程先获得锁对象。
        lock = new ReentrantLock(fair);
        //否则,由操作系统调度由哪个线程获得锁,一般为false,性能会比较高  
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    /** 
     *构造函数,带有初始内容的队列 
     */ 
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);
    
        final ReentrantLock lock = this.lock;
        //要给数组设置内容,先上锁  
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;//依次拷贝内容  
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;//如果putIndex大于数组大小 ,那么从0重新开始  
        } finally {
            lock.unlock();//最后一定要释放锁 
        }
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LEZcaOf9-1603457218531)(F:\Typora文件存储位置\多线程并发笔记\【学习笔记】学习多线程,看完这篇超详细的教程就够了.assets\1241406-20180412190457941-1980149826.png)]

    这里的add方法和offer方法最终调用的是enqueue(E x)方法,其方法内部通过putIndex索引直接将元素添加到数组items中,这里可能会疑惑的是当putIndex索引大小等于数组长度时,需要将putIndex重新设置为0,这是因为当前队列执行元素获取时总是从队列头部获取,而添加元素从中从队列尾部获取所以当队列索引(从0开始)与数组长度相等时,下次我们就需要从数组头部开始添加了,如下图演示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gzNcZqy0-1603457218532)(F:\Typora文件存储位置\多线程并发笔记\【学习笔记】学习多线程,看完这篇超详细的教程就够了.assets\1241406-20180412190657414-1245108796.png)]

  • put(E e)方法详解

    public void put(E e) throws InterruptedException {
        checkNotNull(e);//非空检查
        final ReentrantLock lock = this.lock;
         // 加锁,如果线程中断了抛出异常
        lock.lockInterruptibly();
        try {
            //如果当前队列已满,将线程移入到notFull等待队列中
            while (count == items.length)// notFull等待的意思是说现在队列满了,只有取走一个元素后,队列才不满
                notFull.await();
            //满足插入数据的要求,直接进行入队操作
            enqueue(e);
        } finally {
            lock.unlock();//解锁
        }
    }
    

    该方法的逻辑很简单,当队列已满时(count == items.length)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用enqueue(e)插入数据元素。enqueue方法源码为:

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        //插入数据
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //通知消费者线程,当前队列中有数据可供消费
        notEmpty.signal();
    }
    

    enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。

  • take方法详解

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JH77qTcO-1603457218533)(F:\Typora文件存储位置\多线程并发笔记\【学习笔记】学习多线程,看完这篇超详细的教程就够了.assets\1241406-20181203223152973-1707252984.png)]

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 加锁,如果线程中断了抛出异常
        lock.lockInterruptibly();
        try {
            //如果队列为空,没有数据,将消费者线程移入等待队列中
            while (count == 0)
                notEmpty.await();
            //获取数据
            return dequeue();
        } finally {
            lock.unlock();//解锁
        }
    }
    

    take方法也主要做了两步:1. 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;2. 若队列不为空则获取数据,即完成出队操作dequeue。dequeue方法源码为:

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //获取数据
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        //通知被阻塞的生产者线程
        notFull.signal();
        return x;
    }
    

    dequeue方法也主要做了两件事情:

    1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);
    2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。

    可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。在理解ArrayBlockingQueue后再去理解LinkedBlockingQueue就很容易了。【其他方法同理,理解了一组之后,一通百通,这里不过多介绍了】

LinkedBlockingQueue - 链表阻塞队列

LinkedBlockingQueue 的底层是基于单向链表实现的阻塞队列,可以当做无界队列,也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增长,通常在创建 LinkedBlockingQueue 对象时会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE。

【关于构造、插入、移除方法这里不过多介绍,理解了ArrayBlockingQueue之后这里也差不多一样的理解方式】

PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来进行排序。需要注意的是不能保证同优先级元素的顺序。

PriorityBlockingQueue 并发控制采用的是 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。

SynchronousQueue - 同步队列

SynchronousQueue每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue也可以通过构造器参数来为其指定公平性。

  • 代码示例

    /**
    * 同步队列
    * 和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素
    * put了一个元素,必须从里面先take取出来,否则不能在put进去值!
    */
    public class SynchronousQueueDemo {
        public static void main(String[] args) {
            BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队
    
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+" put 1");
                    blockingQueue.put("1");
                    System.out.println(Thread.currentThread().getName()+" put 2");
                    blockingQueue.put("2");
                    System.out.println(Thread.currentThread().getName()+" put 3");
                    blockingQueue.put("3");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"T1").start();
            new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"T2").start();
        }
    }
    

LinkedTransferQueue

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

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

  • ryTransfer(E e)
    tryTransfer方法如果当前有消费者线程(调用take方法或者具有超时特性的poll方法)正在消费数据的话,该方法可以将数据立即传送给消费者线程,如果当前没有消费者线程消费数据的话,就立即返回false。因此,与transfer方法相比,transfer方法是必须等到有消费者线程消费数据时,生产者线程才能够返回。而tryTransfer方法能够立即返回结果退出。

  • tryTransfer(E e,long timeout,imeUnit unit)
    与transfer基本功能一样,只是增加了超时特性,如果数据才规定的超时时间内没有消费者进行消费的话,就返回false

DelayQueue - 延迟队列

DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityBlockingQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

DelayQueue运用在以下应用场景

  • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  • 任务超时处理:比如下单后15分钟内未付款,自动关闭订单。

LinkedBlockingDeque

LinkedBlockingDeque是 一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirstaddLastofferFirstofferLastpeekFirstpeekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双向队列的最后一个元素。

详细可以查看:https://wwwblogs/WangHaiMing/p/8798709.html

这篇文章说明得很清楚。下面继续掉发学习,线程池技术。

线程池技术(重点)

本部分主要介绍线程池的四大方法、七大参数、四种拒绝策略。

池化技术的简介

在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。例如:通过复用对象来减少创建对象、垃圾回收开销的对象池;还有通过复用TCP连接来减少创建和释放连接的时间的连接池(数据库连接池、Redis连接池和HTTP连接池等)。当然,我们线程这里也有线程池,而线程池就是通过复用线程提升性能。简单来说,池化技术就是通过复用来提升性能。

线程、内存、数据库的连接对象都是资源,在程序中,当你创建一个线程或者在堆上申请一块内存的时候都涉及到很多的系统调用,也是非常消耗CPU的。如果你的程序需要很多类似的工作线程或者需要频繁地申请释放小块内存,在没有对这方面进行优化的情况下,这部分代码很可能会成为影响你整个程序性能的瓶颈。

什么是线程池?

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程(提高线程复用,减少性能开销)。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。然而,增加可用线程数量是可能的。线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中然后等待下一次分配任务。

什么要用线程池?

  • 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗。

  • 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;

    过于频繁的创建/销毁线程,会很大程度上影响处理效率。例如:

    创建线程消耗时间T1,执行任务消耗时间T2,销毁线程消耗时间T3

    如果T1+T3>T2,那么是不是说开启一个线程来执行这个任务太不划算了!

  • 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。

    线程并发数量过多,抢占系统资源从而导致阻塞,我们知道线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况,运用线程池能有效的控制线程最大并发数,避免以上的问题

  • 提供更强大的功能,延时定时线程池。

    比如:延时执行、定时循环执行的策略等

    运用线程池都能进行很好的实现

线程池的作用

线程池作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

线程池的四大方法

Java里面线程池的顶级接口是 Executor,不过真正的线程池接口是 ExecutorService, ExecutorService 的默认实现是 ThreadPoolExecutor;普通类 Executors 里面调用的就是 ThreadPoolExecutor。在阿里巴巴的规范中,线程池不允许使用Executors 去创建,而是通过ThreadPoolExecutor的方式去创建,这样的处理方式为了让使用者更清楚线程池的允许规则,同时为了规避资源耗尽的风险。而Executors 返回的线程池对象的弊端如下所示:

  • FixedThreadPoolSingleThreadPool允许的请求队列长度为Integer.MAX_VALUE【约为21亿】,可能会堆积大量的请求,从而导致OOM。【啥是OOM请看我的JVM文章】。
  • CachedThreadPoolScheduledThreadPool允许的创建线程数量为Integer.MAX_VALUE【约为21亿】,可能会创建大量的线程,从而导致OOM。【啥是OOM请看我的JVM文章】。

因此,阿里巴巴不允许使用Executors 去创建线程池,而是建议通过ThreadPoolExecutor的方式去创建。emmmm,这里我为了方便,还是先使用Executors 去创建线程池,后文再介绍如何使用ThreadPoolExecutor的作用。

Executors 提供了四种线程池,下面我们分别介绍一下

newSingleThreadExecutor

  • 介绍:创建是一个单线程池,也就是该线程池只有一个线程在工作,所有的任务是串行执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  • 创建方式ExecutorService singleThreadPool = Executors.newSingleThreadPool();

  • 代码示例

    public class SingleThreadExecutorDemo {
        public static void main(String[] args) {
            //创建单个线程的线程池
            ExecutorService threadPool = Executors.newSingleThreadExecutor();
            try {
                //整10个任务让线程池去跑
                for(int i=0; i<10; i++){
                    //执行一个任务,没有返回值。
                    threadPool.execute(()->{
                        System.out.println(Thread.currentThread().getName()+"-->OK");
                    });
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //线程池用完,一定要记得关闭线程池
                threadPool.shutdown();
            } 
        }
    }
    

    通过输出可以发现,线程池中有且仅有一个工作线程执行任务,所有任务按照指定顺序执行,即遵循队列的入队出队规则。

    附带方法解释:execute()表示执行一个任务,没有返回值。

newFixedThreadPool(int nThread)

  • 介绍:创建固定大小的线程池,nThread为固定数量的线程,如果是在服务器上使用线程池,建议使用这种方式,能够获得更好的性能。

  • 创建方式ExecutorService threadPool = Executors.newFixedThreadPool(nThreads),nThreads表示创建多少个线程。

  • 代码示例

    public class FixedThreadPoolDemo {
        public static void main(String[] args) {
            //创建5个线程的线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(5);
    
            try {
                for(int i=0; i<10; i++){
                    //使用线程池创建线程
                    threadPool.execute(()->{
                        System.out.println(Thread.currentThread().getName()+"-->OK");
                    });
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //线程池用完,一定要记得关闭线程池
                threadPool.shutdown();
            }
    
        }
    }
    

    从以上代码输出可知,通过newFixedThreadPool可以创建固定数量的线程,我上面是创建了5个,所以十个任务5个线程交替执行。

newCachedThreadPool

  • 介绍:创建一个线程池,线程池中的线程数量可以动态改变,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

  • 创建方式ExecutorService threadPool = Executors.newCachedThreadPool();创建线程数量可以动态改变的线程池

  • 代码示例

    public class newCachedThreadPoolDemo {
        public static void main(String[] args) {
            //创建线程数量可以动态改变的线程池
            ExecutorService threadPool = Executors.newCachedThreadPool();
    
            try {
                for(int i=0; i<100; i++){
                    //使用线程池创建线程
                    threadPool.execute(()->{
                        System.out.println(Thread.currentThread().getName()+"-->OK");
                    });
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //线程池用完,一定要记得关闭线程池
                threadPool.shutdown();
            }
    
        }
    }
    

    以上代码我创建了100个任务,但是实际上电脑测试创建的线程没有100个【我这里只有46个】,具体原因其实与我们电脑的配置有关。

newScheduledThreadPool

  • 介绍:创建一个无限大小的线程池,此线程池支持定时以及周期性执行任务的需求。

  • 创建方式ExecutorService threadPool = Executors.newScheduledThreadPool();

  • 代码示例:略…

线程池的七大参数【超重要】

我们上面介绍了通过Executors工具类创建线程池的四种方法,下面我们先看一下这四个方法的源码:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1, //这里核心线程数和线程总数为1,表示线程池中只有一个线程
                                    0L, TimeUnit.MILLISECONDS,//线程池中非核心线程闲置超时时长为0,单位是毫秒
                                    new LinkedBlockingQueue<Runnable>()));//使用的是链表阻塞队列
    }

	public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, //核心线程数和线程总数可自定义
                                      0L, TimeUnit.MILLISECONDS, //线程池中非核心线程闲置超时时长为0毫秒时销毁
                                      new LinkedBlockingQueue<Runnable>());//使用链表阻塞队列
    }

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,//核心线程数为0,线程总数最大为21亿
                                      60L, TimeUnit.SECONDS,//闲置线程超过一分钟就会被销毁
                                      new SynchronousQueue<Runnable>());//使用同步阻塞队列
    }
	
	//创建无限大小的线程池底层调用了下面这个方法,参数我附带上去了
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

从以上四个方法的源码我们可以知道,虽然是使用Executors工具创建的线程池,但是底层都是调用了ThreadPoolExecutor,我们看一下ThreadPoolExecutor的源码:

public ThreadPoolExecutor(    int corePoolSize,  	// 核心线程池大小
                              int maximumPoolSize,	// 最大核心线程池大小
                              long keepAliveTime,	// 超时了没有人调用就会释放
                              TimeUnit unit,		// 超时单位
                              BlockingQueue<Runnable> workQueue, // 阻塞队列
                              ThreadFactory threadFactory,// 线程工厂:创建线程的,一般不用动
                              RejectedExecutionHandler handler) {    // 拒绝策略
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

我们发现,这个构造方法中竟然有七个参数,这七个参数代表着什么呢?有什么作用呢?下面我们进入正题,学习一下这七个参数,非常重要!!

  • (1)int corePoolSize该线程池中核心线程数最大值

    线程池新建线程的时候,如果当前线程总数小于 corePoolSize ,则新建的是核心线程;如果超过corePoolSize,则新建的是非核心线程。核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。如果指定ThreadPoolExecutor的 allowCoreThreadTimeOut 这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间( keepAliveTime),就会被销毁掉

  • (2)int maximumPoolSize线程池中线程总数的最大值

    线程总数计算公式 = 核心线程数 + 非核心线程数。

  • (3)long keepAliveTime线程池中非核心线程闲置超时时长

    一个非核心线程,如果不干活(闲置状态)的时长,超过这个参数所设定的时长,就会被销毁掉。

    但是,如果设置了 allowCoreThreadTimeOut = true,则会作用于核心线程。

  • (4)TimeUnit unit时间单位

    TimeUnit是一个枚举类型,翻译过来就是时间单位,我们最常用的时间单位包括:

    【MILLISECONDS : 1毫秒 、SECONDS : 秒、MINUTES : 分、HOURS : 小时、DAYS : 天】

  • (5)BlockingQueue workQueue阻塞队列

    该线程池中的任务队列:维护着等待执行的Runnable对象。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。关于阻塞队列的核心方法在上文中有介绍。

    常用的workQueue类型:

    1. **SynchronousQueue:**这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
    2. **LinkedBlockingQueue:**这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
    3. **ArrayBlockingQueue:**可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
    4. **DelayQueue:**队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
  • (6)ThreadFactory threadFactory创建线程的方式。

    这是一个接口,new它的时候需要实现他的Thread newThread(Runnable r)方法,一般用不上,不用管

  • (7)RejectedExecutionHandler handler拒绝策略

    这玩意儿就是抛出异常专用的,比如上面提到的两个错误发生了,就会由这个handler抛出异常,你不指定他也有个默认的

    当线程池和队列都满了,再加入线程会执行此策略。有四种策略,我后面会详细描述。

新建一个线程池的时候,一般只用5个参数的构造函数。

线程池执行流程

我们上面学习了new一个ThreadPoolExecutor对象,同时也学习了各个参数是干嘛的, 那么我们怎么向线程池提交一个要执行的任务呢?不要急,向线程池提交要执行的任务就是通过一下方式:

通过ThreadPoolExecutor.execute(Runnable command)方法即可向线程池内添加一个任务。

结合上面参数说明,当一个任务通过execute()想要添加到线程池的时候,需要考虑以下几种情况:

  • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  • 如果此时线程池中的数量等于corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满了,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

  • 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

execute()和submit()方法

1、execute(),执行一个任务,没有返回值。
2、submit(),提交一个线程任务,有返回值。

  • submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。

  • submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。

  • submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

  • Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。

关于拒绝策略handler有四种选择,下面我们再介绍一下线程池的拒绝策略。

线程池的四大拒绝策略

上面介绍参数的时候其实已经说到了ThreadPoolExecutor执行的策略,首先这里有四种拒绝策略,下面我们逐一介绍一下。

当队列满了以后,再往线程池中添加一个或者多个任务时,处理方式:

  • AbortPolicy:假如队列满了,对添加的任务不做处理,同时抛出RejectedExecutionException异常。

  • DiscardPolicy:假如队列满了,就会抛弃想要添加进队列的任务,同时也不会抛出异常。

  • DiscardOldestPolicy:假如队列满了,添加的任务和会最早入队列的任务竞争, 竞争成功就会丢弃队列里最老的任务,将当前这个任务继续提交给线程池,竞争失败就会将当前任务丢弃。

  • CallerRunsPolicy:假如队列满了,添加的任务就会交给线程池调用所在的线程进行处理,也就是任务从哪里来回哪里去。

自定义线程池

我们理解了线程池的七大参数和四大拒绝策略以后,我们可以自己定义一个线程池,毕竟东西都是人造出来了,而在实际工作环境中一般都会使用自定义线程池,下面我们就来自定义一个简单的线程池。

/**
 * 自定义线程池
 */
public class CustThreadPool {

    public static void main(String[] args) {
		//创建自定义的线程池对象
        ExecutorService threadPool = CustThreadPool.newCustThreadExecutor();
        try{
            for (int i=1; i<=30; i++){
                //通过线程池执行任务
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();//解锁
        }
        
    }

    /**
     * 自定义线程池方法
     *   - 核心线程池数为 2 个
     *   - 线程池中线程总数的最大值为 5 个
     *   - 线程池中非核心线程闲置超时时长为 3 秒时销毁
     *   - 使用的阻塞队列为链表阻塞队列
     *   -  Executors.defaultThreadFactory():创建线程的方式。固定写法
     *   - 使用的拒绝策略是当队列满了以后就会抛弃添加的任务
     * @return 线程池对象
     */
    public static ExecutorService newCustThreadExecutor(){
        return new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy()
        );
    }
}

输出:

pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-5 ok

首先我们自定义的线程池中,核心线程有2个,线程池总数为5个,队列中只能放3个任务,我们定义的拒绝策略是当队列满了以后,其他的任务就会被抛弃掉。因此刚刚开始的时候前面的几个任务都是由核心线程来出来,然后任务多了以后超过了核心线程的承受能力,就会使用非核心线程去执行,当任务超过了5个,同时队列中的3个位置也满了,这个时候就会执行拒绝策略,由于这里是队列满了以后其他的任务都会被抛弃,所以以上就输出8个任务。

举例说明

其实以上就和去银行窗口取款是一样的道理,如下图所示,总的线程数就相当于银行的柜台,核心线程就相当于开放的窗口,队列就相当于银行里面等候区【等候区只有5个椅子,只能5个人等待】,而任务就相当于人。

我们平时的时候,银行只会开放两个窗口办理业务,首先上图中就有两个人占了两个窗口正在办理业务,一旦来了其他人需要办理业务员,等待区还有座位就到等待区进行等待,等前面两个人办理好了再过去办理;当需要办理东西的人多了,等待区的座位已经被人坐满了,银行处理业务的窗口当前只开了两个【没有超过最大的窗口数5个】,这个时候就会多开几个窗口让人去办理业务;如果这个时候有一大批人需要办理业务,5个窗口全部开了并且都站了人办理业务,并且等待区也满了,这个时候就会实施拒绝策略,如果是DiscardPolicy就会拒绝其他的人进银行,等到窗口办理业务的人走了,等待区的人去窗口办理业务,等待区有位置的时候才放人进来。

【描述得不是很清楚哈,但是大概的意思就是这样。】

第一步:提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。

第二步:如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。

第三步:当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。

第四步:如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。

线程池的状态

线程池有这几个状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。源码中的显示为:

	//线程池状态
   private static final int RUNNING    = -1 << COUNT_BITS;
   private static final int SHUTDOWN   =  0 << COUNT_BITS;
   private static final int STOP       =  1 << COUNT_BITS;
   private static final int TIDYING    =  2 << COUNT_BITS;
   private static final int TERMINATED =  3 << COUNT_BITS;

线程池各个状态的切换:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RHFFkOAw-1603457218535)(F:\Typora文件存储位置\多线程并发笔记\【学习笔记】学习多线程,看完这篇超详细的教程就够了.assets\16bf3b10e39a52d0)]

RUNNING

  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入TIDYING状态;

TIDYING

  • 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  • terminated()执行完毕,进入TERMINATED状态

TERMINATED

  • 该状态表示线程池彻底终止

线程池的异常处理

可参考:https://juejin.im/post/6844903889678893063#heading-8

待更新…

CPU密集型和密集型 IO的介绍【线程池调优】

针对上面所说的,我们可能在自定义线程池的时候有这么一个问题:**线程池的最大线程数maximumPoolSize到底该如何定义呢?**它该定义为多少呢?

有两种解决方案:1、CPU密集型;2、IO密集型。

下面我们来详细说明一下。

CPU密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

首先得回顾我们之前的并行的概念:**系统具有同时处理多个任务的能力。同时运行,只有具备多个CPU才能实现。CPU多核,多个线程可以同时执行。**而这里的多核,我们可以查看一下自己的电脑配置,下面截图给出我的电脑配置:

打开任务管理器–>资源,查看下图:

我的电脑比较菜,只有4核,等有钱了就换个高配置的电脑,哈哈哈。我这里只有4核,就说明有4条线程可以同时执行,这样的效率是最高的,所以这里的CPU密集型就是指的是:最大线程数maximumPoolSize可以用我们的电脑CPU核数来定义,也就是几核的CPU最大线程数就定义为几,这样可以保证CPU的效率最高。

而我们代码中肯定不能就直接写核数了,因为每台电脑的CPU核数不一定相同,所以我们得用代码来获取CPU的核数:

public static ExecutorService newCustThreadExecutor(){
        //获取CPU的核数
        Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
                2,
                Runtime.getRuntime().availableProcessors(),
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy()
        );
    }

获取CPU核数的方法:Runtime.getRuntime().availableProcessors();

IO密集型

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。IO密集型涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。

而我们需要去判断那些大型任务的数量【十分占用IO资源的】,在程序中只要将线程池最大线程数maximumPoolSize大于程序中十分耗IO资源的任务数目就可以了。

I/O密集型适合读写,比如数据库的读写操作,CPU密集型适合运算

四大函数式接口【必须掌握】

作为新时代的程序员,必须得知道这些东东:lambda表达式、链式编程、函数式接口、Stream流式计算。而这里我们主要介绍的就是这四种之一:函数式接口。

Stream流式计算

待更新…

ForkJoin详解

待更新…

异步回调

待更新…

Java锁机制

锁的概念

JAVA 内置锁

  • 隐性锁:每个JAVA对象可以用作实现同步的内置锁,线程在访问同步代码块时必须先获取该内置锁,在退出和中断的时候需要释放内置锁。Java内置锁通过synchronized关键字使用,使用其修饰方法或者代码块,就能保证方法或者代码块以同步方式执行。有对象锁和类锁(static方法和class上枷锁)区分,两者不冲突可以并行存在。

  • 显性锁:显式锁(ReentrantLock)正式为了解决这些灵活需求而生,ReentrantLock的字面意思是可重入锁,可重入的意思是线程可以同时多次请求同一把锁,而不会自己导致自己死锁。

锁的分类

可重入锁:Synchronized和ReentrantLook都是可重入锁,锁的可重入性标明了锁是针对线程分配方式而不是针对方法。例如调用Synchronized方法A中可以调用Synchronized方法B,而不需要重新申请锁。

读写锁:按照数据库事务隔离特性的类比读写锁,在访问统一个资源(一个文件)的时候,使用读锁来保证多线程可以同步读取资源。ReadWriteLock是一个读写锁,通过readLock()获取读锁,通过writeLock()获取写锁。

可中断锁:可中断是指锁是可以被中断的,Synchronized内置锁是不可中断锁,ReentrantLock可以通过lockInterruptibly方法中断显性锁。例如线程B在等待等待线程A释放锁,但是线程B由于等待时间太久,可以主动中断等待锁。

公平锁:公平锁是指尽量以线程的等待时间先后顺序获取锁,等待时间最久的线程优先获取锁。synchronized隐性锁是非公平锁,它无法保证等待的线程获取锁的顺序,ReentrantLook可以自己控制是否公平锁。

待更新…

Volatile关键字

说到Volatile,我们一般都会遇到这么一个问题:请你谈谈你对Volatile的理解?

一般都会这样回答:

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

那为啥会有以上这三点呢?懵逼了…没关系,让我们学习完这部分,学完以后还有啥不懂的,只要明白了原理,问啥啥都会!!!

待更新…

ThreadLocal由浅入深

待更新…

CAS的学习

可重入锁

自旋锁

死锁排查

多线程设计模式

待更新…

结束语

关于多线程的知识点总结,对于本文我感觉不是很全,因为其中的一部分技术涉及到源码深入分析,原谅我现在技术没到那个阶段,很多东西都需要强大的功底支持,所以就没有深入的去探究,同时感觉那些技术要是写进来的话,太耗费时间了【原谅我最近是真抽不出时间去完成】,如果可以的话,我后面会单独的将其中的技术进行拆分整理成文章发布出来,如果你也对多线程的深入学习感兴趣,不如关注我一下,我们一起学习,一起探究,一起变得更优秀!本文参考了很多人的文章,文末已附链接,文中由于都是一字一字手打上去的,难免会出现错误的地方以及不对的地方,如果发现错误之处,劳烦各位一定帮忙指出,我一定会及时更正,如果文章对你有所帮助,一定要记得点赞支持一下。

参考资料

B站狂神JUC视频:https://www.bilibili/video/BV1B7411L7tE
https://www.jianshu/p/ec94ed32895f

synchronized:https://www.jianshu/p/d53bf830fa09

锁:https://www.jianshu/p/b343a9637f95

并发容器List:https://wwwblogs/skywang12345/p/3498483.html

https://wwwblogs/skywang12345/p/3533995.html

https://juejin.im/post/6844903680370556941#heading-6

队列:

https://wwwblogs/WangHaiMing/p/8798709.html

https://juejin.im/post/6844903640709201934?utm_source=gold_browser_extension

https://www.jianshu/p/c422ed5ea9ce/

线程池:

https://juejin.im/post/6844903889678893063#heading-8

https://www.jianshu/p/50fffbf21b39

https://www.jianshu/p/7726c70cdc40

https://www.jianshu/p/210eab345423

【更新时间:2020.10.23 21:08】

本文标签: 这篇多线程知识详细就够了