Java并发工具(学习笔记)

Java并发工具

  • 线程池
    • 线程池的自我介绍
    • 创建和停止线程池
    • 给线程加点料
    • 实现原理、源码分析
  • ThreadLocal
    • 两大使用场景-ThreadLocal的用途
    • 使用ThreadLocal带来的好处
    • 原理、源码分析
    • 主要方法介绍
    • ThreadLocalMap类
    • ThreadLocal注意点
  • 千变万化的锁
    • Lock接口
    • 锁的分类
    • 乐观锁和悲观锁
    • 可重入锁和非可冲入锁(ReentrantLock为例)
    • 公平锁和非公平锁
    • 共享锁和排它锁:(ReentrantReadWriteLock读写锁重点)
    • 自旋锁和阻塞锁
    • 可中断锁
    • 锁优化
  • 原子类
    • 什么是原子类,有什么用?
    • 6类原子类纵览
    • Atomic*基本类型原子类(AtomicInteger为例)
    • Atomic*Array数组类型原子类
    • Atomic*Reference引用类型原子类
    • AtomicIntegerFieleUpdater升级原来变量
    • Adder累加器
    • Accumulator累加器
  • CAS原理
    • 什么是CAS
    • 应用场景
    • CAS实现原子操作(AtomicInteger为例)
    • 缺点
  • final关键字和不变性
    • 什么是不变性(Immutable)
    • final的作用
    • 修饰变量
    • final修饰方法
    • final修饰类
    • 注意点
    • 不变性和final的关系
  • 并发容器精讲
    • 并发容器概览
    • 集合类的历史
    • ConcurrentHashMap(重点、面试常考)
    • CopyOnWriteArrayList
    • 并发队列Queue(阻塞队列、非阻塞队列)
    • 阻塞队列**BlockingQueue**
  • 线程协作、控制并发流程
    • 什么是控制并发流程
    • CountDownLatch倒计时门闩
    • Semaphore信号量
    • Condition接口(又称条件对象)
    • Cyclicbarrier循环栅栏
  • AQS
    • 学习AQS的思路
    • 为什么需要AQS
    • AQS的作用
    • AQS内部原理解析
    • 应用实例、源码解析
    • 利用AQS实现一个自己的Latch门闩
  • Future和Callable -----治理线程第二大法宝
    • Runnable的缺陷
    • Callable接口
    • Future类
    • 用法1:线程池的submit方法返回Future对象
    • 用法2:用FutureTask来创建Future
    • Future的注意点

线程池

线程池的自我介绍

  • 如果不使用线程池,每个任务都新开一个线程处理
    一个线程
    for循环创建线程
    当任务数量上升到1000
  • 这样开销太大,我么们希望有固定数量的线程,来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题

为什么要使用线程池

  • 问题一:反复创建线程开销大
  • 问题二:过多的线程会占用太多内存

解决上面两个问题的思路

  • 用少量的线程 一 避免内存占用过多
  • 让这部分线程都保持工作,且可以反复执行任务 一 避免生命周期的损耗

线程池的好处

  • 加快相应速度
  • 合理利用CPU和内存
  • 统一管理

线程池适合应用的场合

  • 服务器接受到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率
  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以用线程池来管理

创建和停止线程池

  • 线程池构造函数的参数
    Java并发工具(学习笔记)_第1张图片
    参数中的corePoolSize和maxPoolSize
  • corePoolSize指的是核心线程数:线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新线程去执行任务
  • 线程池有可能会再核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限,这就是最大量maxPoolSize
    Java并发工具(学习笔记)_第2张图片
    添加线程规则

1.如果线程小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行任务
2.如果线程数等于(或大于)corePoolSize但少于maximumPoolSize,则将任务放入队列
3.如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程来运行任务
4.如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务

是否需要增加线程的判断顺序是:
1.corePoolSize
2.workQueue
3.maxPoolSizeJava并发工具(学习笔记)_第3张图片
增减线程的特点
1.通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池
2.线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它
3.通过设置maximumPoolSize为很高的值,例如Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务
4.是只有在队列填满时才创建多余corePoolSize的线程,所以如果你使用的是无界队列(例如LinkedBlockingQueue),那么线程数就不会超过corePoolSize

keepAliveTime
如果线程池当前的线程数多于corePoolSize,那么如果多于的线程空闲时间超过keepAliveTime,他们就会被终止

ThreadFactory用来创建线程
新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等

newFixedThreadPool
由于传进去的LinkedBlockingQueue是没有容量上限的,所有当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM

newSingleThreadExecutor
1.单线程的线程池:它只会用唯一的工作线程来执行任务
2.它的原理和FixedThreadPool是一样的,但是此时的线程数量被设置为了1
可以看出,这是和刚才的newFixedThreadPool的原理基本一样,只不过把线程数直接设置成了1,所以这会导致同样的问题,也就是当请求堆积的时候,可能会占用大量的内存

newCachedThreadPool
1.可缓存线程池
2.特点:无界线程池,具有自动回收多余线程的功能
这是的弊端在于第二个参数maximumPoolSize被设置为了Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM

newScheduledThreadPool
支持定时及周期性任务执行的线程池

正确的创建线程池的方法

  • 根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大,我们想给线程取什么名字等等

线程池里的线程数量设定为多少比较合适

  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右
  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于cpu核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:
  • 线程数 = CPU核心数 * (1 + 平均等待时间 / 平均工作时间)

停止线程池的正确方法

  1. shutdown
  2. isShutdown
  3. isTerminated
  4. awaitTermination
  5. shutdownNow

给线程加点料

任务太多,怎么拒绝?

拒绝时机
1.当Executor关闭时,提交新任务被拒绝
2.以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和

4种拒绝策略
1.AbortPolicy 直接抛出异常
2.DiscardPolicy 默默丢弃任务
3.DiscardOldestPolicy 丢弃最老的
4.CallerRunsPolicy 让主线程去运行避免了业务损失,可以让提交速度降低下来这是一种负反馈

实现原理、源码分析

  • 线程池组成部分
    1.线程池管理器
    2.工作线程
    3.任务列队
    4.任务接口(Task)

Executor家族

  • Executor
  • ExecutorService 继承了Executor接口 又新增了几个方法
  • Executors 一个工具类,帮我们快速创建线程池

ExecutorService executorService = Executors.newFixedThreadPool();

Java并发工具(学习笔记)_第4张图片

线程池实现任务复用的原理

  • 相同线程执行不用任务

线程池状态

  • RUNNING:接受新任务并处理排队任务
  • SHUTDOWN:不接受新任务,但处理排队任务
  • STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务
  • TIDYING,中文是整洁,理解了中文就容易理解这个状态了:所有任务都已终止,workerCount为零时,线程会转换到TIDYING状态,并将运行terminate()钩子方法
  • TERMINATED:terminate()运行完成

使用线程池的注意点

  • 避免任务堆积
  • 避免线程数过度增加
  • 排查线程泄露

ThreadLocal

两大使用场景-ThreadLocal的用途

  • 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
  • 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

典型场景1:每个线程需要一个独享的对象

  • 每个Thread内有自己的实例副本,不共享
  • 比如:教材只有一本,一起做笔记有线程安全问题。复印后没问题
public class ThreadLocalNormalUsage00 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(10);
                System.out.println(date);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(104707);
                System.out.println(date);
            }
        }).start();
    }
    public String date(int seconds){
        //参数的单位是毫秒, 从1970.1.1 00:00:00  GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

延伸出10个,那就有10个线程和10个SimpleDateFormat,写法不优雅

/**
 * 描述:     10个线程打印日期
 */
public class ThreadLocalNormalUsage01 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
            Thread.sleep(100);
        }
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}
/**
 * 描述:     1000个打印日期的任务,用线程池来执行
 */
public class ThreadLocalNormalUsage02 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}

/**
 * 描述:     1000个打印日期的任务,用线程池来执行
 */
public class ThreadLocalNormalUsage03 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}

所有线程都共用同一个simpleDateFormat对象发生了线程安全问题

/**
 * 描述:     加锁来解决线程安全问题
 */
public class ThreadLocalNormalUsage04 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage04().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalNormalUsage04.class) {
            s = dateFormat.format(date);
        }
        return s;
    }
}

SimpleDateFormat的进化之路

  1. 2个线程分别用自己的SimpleDateFormat,这没问题
  2. 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅(应该复用对象),但勉强可以接受
  3. 但是当需求变成了1000个,那么必然要用线程池(否则消耗内存过多)
  4. 所有的线程共有同一个simpleDateFormat对象
  5. 这是线程不安全的,出现了并发安全问题
  6. 我们可以选择加锁,加锁后结果正常,但是效率低
  7. 在这里更好的解决方案是使用ThreadLocal
  8. lambda表达式

更好的解决方案是使用ThreadLocal

/**
 * 描述:     利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 */
public class ThreadLocalNormalUsage05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
//        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
		//lambda表达式
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

典型场景2 实例:当前用户信息需要被线程内所有方法共享

  • 一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),再从service-2()传到service-3(),以此类推,但是这样做会导致代码冗杂不易维护
    Java并发工具(学习笔记)_第5张图片
    方法

  • 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等)

  • 这些信息在用一线程内相同,但是不用的线程使用的业务内容是不相同

  • 在此基础上可以演进,使用UserMap
    Java并发工具(学习笔记)_第6张图片

  • 当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响
    Java并发工具(学习笔记)_第7张图片

  • 更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息的目的

  • 强调的是同一个请求内(同一个线程内)不同方法间的共享

  • 不需要重写**initialValue()方法,但是必须手动调用set()**方法

/**
 * 描述:     演示ThreadLocal用法2:避免传递参数的麻烦
 */
public class ThreadLocalNormalUsage06 {

    public static void main(String[] args) {
        new Service1().process("");
    }
}

class Service1 {
    public void process(String name) {
        User user = new User("超哥");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.get();
        System.out.println("Service2拿到用户名:" + user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

ThreadLocal的两个作用

  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立对象)
  2. 在任何方法中都可以轻松获取到该对象

根据共享对象的生成时机不同,选择initialValue或set来保存对象
场景一:initialValue 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制
场景二:set 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中取,以便后续使用。

使用ThreadLocal带来的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效地**利用内存、节省开销:**相比于每个人物都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
  • 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数。ThreadLocal使得代码耦合度更低,更优雅

原理、源码分析

Java并发工具(学习笔记)_第8张图片

  • 每个Thread对象中都持有一个ThreadLocalMap成员变量

主要方法介绍

T initialValue(): 初始化

  • 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
  • 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
  • 通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法
  • 是没有默认实现的,如果我们要用initialValue方法,需要自己实现,通常是匿名内部类的方式

void set(T t): 为这个线程设置一个新值

T get(): 得到这个线程对应的value。如果是首次调用get(),则会调用initialize来得到这个值

  • get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
  • 注意,这个map已经map中的key和value都是保存在线程中,而不是保存在ThreadLocal中

void remove() : 删除对应这个线程的值

ThreadLocalMap类

  • ThreadLocalMap类,也就是Thread.threadLocals
  • ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[]table,可以认为是一个map,键值对:

键:这个ThreadLocal
值:实际需要的成员变量,比如user或者simpleDateFormat对象

  • 冲突:HashMap
    Java并发工具(学习笔记)_第9张图片

  • ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

两种使用场景殊途同归

  • 通过源码分析可以看出,setInitialValue和直接set最后都是利用**map.set()**方法来设置值
  • 也就是说,最后都会对应到ThreadLocalMap的一个Entry,只不过是起点和入口不一样

ThreadLocal注意点

内存泄漏

  • 什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
  • Key的泄漏:ThreadLocalMap中的Entry继承自WeakReference,是弱引用
  • 弱引用的特点是,如果这个对象被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收
  • 所以弱引用不会阻止GC,因此弱引用的机制

Value的泄漏

  • ThreadLocalMap的每个Entey都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用

  • 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了

  • 但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:
    在这里插入图片描述

  • 因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM

  • JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收

  • 但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏

如何避免内存泄漏(阿里规约)

  • 调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法

空指针异常

  • 在进行get之前,必须先set,否则可能会报空指针异常?

共享对象

  • 如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题

如果可以不适用ThreadLocal就解决问题,那么就不要强行使用

  • 例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal

优先使用框架的支持,而不是自己创造

  • 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏

实际应用场景 - 在Spring中的实例分析

  • DateTimeContextHolder类,看到里面用了ThreadLocal
  • 每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景

千变万化的锁

Lock接口

Lock简介、地位、作用

  • 锁是一种工具,用于控制对共享资源的访问
  • Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
  • Lock并不是用来代替synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,来提供高级功能
  • Lock接口最常见的实现类是ReentrantLock
  • 通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现类也可以允许并发访问,比如ReadWriteLock里面的ReadLock

为什么需要Lock?
为什么synchronized不够用?

  • **效率低:**锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在视图获得锁的线程
  • 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
  • 无法知道是否成功获取到锁

Lock主要方法介绍

  • 在Lock中声明了个方法来获取锁
  • lock()、tryLock()、tryLock(long time,TimeUnit unit)和lockInterruptibly()

lock()

  • lock()就是最普通的获取锁。如果锁已经被其他线程获取,则进行等待
  • Lock不会像synchrinized一样在异常时自动释放锁
  • 因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
  • lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock()就会陷入永久等待
/**
 * 描述:     Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
 */
public class MustUnlock {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try{
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
    }
}

tryLock()

  • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回true,否则返回false,代表获取锁失败
  • 相比于lock,这样的方法显然功能更强大了,我们可以根据是否能获取锁来决定后续程序的行为
  • 该方法会立即返回,即便在拿不到锁时不会一直在那等
  • 主要方法介绍: tryLock(long time,TimeUnit unit):超时就放弃
  • lockInterruptibly():相当于tryLock(long time,TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断
  • unlock():解锁
/**
 * 描述:     用tryLock来避免死锁
 */
public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r1.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();

    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程1获取到了锁1");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程1获取到了锁2");
                                    System.out.println("线程1成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock2.unlock();
                                }
                            } else {
                                System.out.println("线程1获取锁2失败,已重试");
                            }
                        } finally {
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程2获取到了锁2");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程2获取到了锁1");
                                    System.out.println("线程2成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock1.unlock();
                                }
                            } else {
                                System.out.println("线程2获取锁1失败,已重试");
                            }
                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class LockInterruptibly implements Runnable {

    private Lock lock = new ReentrantLock();
public static void main(String[] args) {
    LockInterruptibly lockInterruptibly = new LockInterruptibly();
    Thread thread0 = new Thread(lockInterruptibly);
    Thread thread1 = new Thread(lockInterruptibly);
    thread0.start();
    thread1.start();

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread1.interrupt();
}
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了");
        }
    }
}

可见性保证

  • 可见性
  • happens-before:我们这件事发生了如果其他线程一定能看到我之前所做的修改代表他们拥有heppens-before
  • Lock的加解锁和synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作
    Java并发工具(学习笔记)_第10张图片
    Java并发工具(学习笔记)_第11张图片

锁的分类

  • 这些分类,是从各种不同角度出发去看的
  • 这些分类不是互斥的,也就是多个类型可以并存:有可能一个锁,同时属于两种类型
  • 比如ReentrantLock既是互斥锁,又是可重入锁
  • 好比一个人们可以同时是男人,有是军人
    Java并发工具(学习笔记)_第12张图片

乐观锁和悲观锁

为什么诞生非互斥同步锁

  • 互斥同步锁的劣势
    • 阻塞和唤醒带来的性能劣势
    • 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行
    • 优先级反转

什么是乐观锁和悲观锁

  • 是否锁住资源的角度分类

悲观锁

  • 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失
  • java中悲观锁的实现就是synchronizedLock相关类

Java并发工具(学习笔记)_第13张图片
Java并发工具(学习笔记)_第14张图片

乐观锁

  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作的对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:如果没有改变过,就说明真的是只有我自己在操作,那我就正常去修改数据
  • 如果数据和我一开始拿到的不一样,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现的
    Java并发工具(学习笔记)_第15张图片
    Java并发工具(学习笔记)_第16张图片
    Java并发工具(学习笔记)_第17张图片
    Java并发工具(学习笔记)_第18张图片
    Java并发工具(学习笔记)_第19张图片

典型例子

  • 悲观锁:synchronized、lock接口
  • 乐观锁的典型例子是原子类、并发容器
  • Git:Git就是乐观锁的典型例子,当我们往远端仓库push的时候,git会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码了,我们的这次提交就失败;如果远端和本地版本号一致,我们就可以顺利提交版本到远端仓库
  • Git不适合用悲观锁
  • 数据库
    -select for update就是悲观锁
    -用version控制数据库就是乐观锁

开销对比

  • 悲观锁的原始开销要高于乐观锁,但特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
  • 相反,虽然乐观锁一开始的开销比悲观锁要小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

两种锁各自的使用场景:各有千秋

  • 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
    1.临界区有IO操作
    2.临界区代码复杂或者循环量大
    3.临界区竞争非常激烈
  • 乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高

可重入锁和非可冲入锁(ReentrantLock为例)

/**
 * 描述:     演示多线程预定电影院座位
 */
public class CinemaBookSeat {

    private static ReentrantLock lock = new ReentrantLock();

    private static void bookSeat() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始预定座位");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "完成预定座位");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
    }
}
/**
 * 描述:     演示ReentrantLock的基本用法,演示被打断
 */
public class LockDemo {

    public static void main(String[] args) {
        new LockDemo().init();
    }

    private void init() {
        final Outputer outputer = new Outputer();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("悟空");
                }

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("大师兄");
                }

            }
        }).start();
    }

    static class Outputer {

        Lock lock = new ReentrantLock();

        //字符串打印方法,一个个字符的打印
        public void output(String name) {

            int len = name.length();
            lock.lock();
            try {
                for (int i = 0; i < len; i++) {
                    System.out.print(name.charAt(i));
                }
                System.out.println("");
            } finally {
                lock.unlock();
            }
        }
    }
}

可重入性质
避免死锁、提升封装性

ReentrantLock方法介绍

  • getHoldCount获得锁次数
  • isHeldByCurrentThread可以看出锁是否被当前线程持有
  • getQueueLength可以返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试时候使用,上线后用到的不多

公平锁和非公平锁

什么是公平和非公平

  • 公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。
  • 注意:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队

为什么要有非公平锁

  • 提高效率 - 避免唤醒带来的空档期

公平的情况(以ReentrantLock为例)

  • 如果在创建ReentrantLock对象时,参数填写为true,那么这额就是个公平锁
  • 假设线程 1 2 3 4是按顺序调用lock()的

不公平的情况(以ReentrantLock为例)

  • 如果在线程1释放锁的时候,线程5恰好去执行lock()
  • 由于ReentrantLock()发现此时并没有线程持有lock这把锁(线程2还没有来得及获得到,因为获取需要时间)
  • 线程5可以插队,直接拿到这把锁,这也是ReentrantLock默认的公平策略,也就是“不公平”

特例

  • 针对tryLock()方法,他不遵守设定的公平的规则
  • 例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他现在在等待队列里了

对比公平和非公平的优缺点

公平锁

  • 优势:各线程公平平等,每个线程在等待一段时间后,总有执行的机会
  • 劣势:更慢,吞吐量更小

不公平锁

  • 优势:更快,吞吐量更大
  • 劣势:有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行

共享锁和排它锁:(ReentrantReadWriteLock读写锁重点)

什么是共享锁和排他锁

  • 排他锁,又称为独占锁、独享锁
  • 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据
  • 共享锁和排他锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁

读写锁的作用

  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果**没有写锁的情况下,读是无阻塞的,**提高了程序的执行效率

读写锁的规则

  • 多个线程只申请读锁,都可以申请到
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
  • 一句话总结:要么是一个或个线程同时有锁,要么个线程有写锁,但是两者不会同时出现(要么多读,要么一写)

读写锁的规则

  • 换一种思路理解:读写锁只是一把锁,可以通过两种方式锁定:读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定
  • 这里是把“获取写锁”理解为“把读写锁进行写锁定”,相当于是换了一种思路,不过原则是不变的,就是要么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不会同时出现

ReentrantReadWriteLock具体用法

  • 现在用了读写锁,线程1和线程2可以同时用读锁,提高了效率:
  • 当线程1和线程2都释放了锁以后,线程3和线程4就可以写入了,但是只能有一个线程持有写锁

读锁插入策略

  • 公平锁:不允许插队
  • 非公平:假设线程2和线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程5不在队列里,现在过来想要读取
    策略1:线程5加入读锁。读可以插队,效率高。容易造成饥饿。
    策略2:线程5加入等待队列。避免饥饿。
  • 策略的选择取决于具体锁的实现,ReentrantReadWriteLock的实现是选择了策略2,是很明智的。
  •  (非公平) 1.写锁可以随时插队
      2.读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队	
    

自旋锁和阻塞锁

  • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
  • 如果同步代码块的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失
  • 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
  • 而为了让当前线程**“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销**。这就是自旋锁。
  • 阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒

自旋锁的缺点

  • 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
  • 在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的
  • 在java1.5版本及以上的并发框架java.util.concurrent和atmoic包下的类基本都是自旋锁的实现
  • AtomicInteger的实现:自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直至修改成功

自旋锁的适用场景

  • 自旋锁一般用于多核的服务器 ,在并发度不是特别高的情况下,比阻塞锁的效率高
  • 另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的

可中断锁

  • 在Java中,synchronized就不是可中断锁,而Lock是可中断锁,因为tryLock(time)和lockInterruptibly都能响应中断
  • 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁

锁优化

Java虚拟机对锁的优化

  • 自旋锁和自适应
  • 锁消除
  • 锁粗化

个人写代码优化锁和提高并发性能

  • 缩小同步代码块
  • 尽量不要锁住方法
  • 减少请求锁的次数
  • 避免增多加解锁情况
  • 锁中尽量不要再包含锁
  • 选择合适的锁类型或合适的工具类

原子类

什么是原子类,有什么用?

  • 不可分割
  • 一个操作是不可中断的,即便是多线程的情况下也可以保证
  • java.util.concurrent.atomic
  • 原子类的作用和锁类型,是为了保证并发情况下线程安全。不过原子类相比于锁,有一定的优势
  • 粒度更细:原子变量可以把竞争范围缩小到变量级别,这是我们可以获得的最细粒度的情况了,通常锁的粒度都要大于原子变量的粒度
  • 效率更高:通常,使用原子类的效率会比使用锁的效率更高,除了高度竞争的情况

6类原子类纵览

Java并发工具(学习笔记)_第20张图片

Atomic*基本类型原子类(AtomicInteger为例)

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

AtomicInteger常用方法

  • public final int get() 获取当前的值
  • public final getAndSet(int newValue) 获取当前的值,并设置新的值
  • public final int getAndIncrement() 获取当前的值,并自增
  • public final int getAndDecrement() 获取当前的值,并自减
  • public final int getAndAdd(int delta) 获取当前的值,并加上预期的值
  • boolean compareAndSet(int expect,int update) 如果当前的数值等于预期值,则以原子的方式将该值设置为输入值(update)
**
 * 描述:     演示AtomicInteger的基本用法,对比非原子类的线程安全问题,使用了原子类之后,不需要加锁,也可以保证线程安全。
 */
public class AtomicIntegerDemo1 implements Runnable {

    private static final AtomicInteger atomicInteger = new AtomicInteger();

    public void incrementAtomic() {
        atomicInteger.getAndAdd(-90);
    }

    private static volatile int basicCount = 0;

    public synchronized void incrementBasic() {
        basicCount++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo1 r = new AtomicIntegerDemo1();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("原子类的结果:" + atomicInteger.get());
        System.out.println("普通变量的结果:" + basicCount);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            incrementAtomic();
            incrementBasic();
        }
    }
}

Atomic*Array数组类型原子类

/**
 * 描述:     演示原子数组的使用方法
 */
public class AtomicArrayDemo {
    public static void main(String[] args) {
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(1000);
        Incrementer incrementer = new Incrementer(atomicIntegerArray);
        Decrementer decrementer = new Decrementer(atomicIntegerArray);
        Thread[] threadsIncrementer = new Thread[100];
        Thread[] threadsDecrementer = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threadsDecrementer[i] = new Thread(decrementer);
            threadsIncrementer[i] = new Thread(incrementer);
            threadsDecrementer[i].start();
            threadsIncrementer[i].start();
        }

//        Thread.sleep(10000);
        for (int i = 0; i < 100; i++) {
            try {
                threadsDecrementer[i].join();
                threadsIncrementer[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < atomicIntegerArray.length(); i++) {
//            if (atomicIntegerArray.get(i)!=0) {
//                System.out.println("发现了错误"+i);
//            }
            System.out.println(atomicIntegerArray.get(i));
        }
        System.out.println("运行结束");
    }
}

class Decrementer implements Runnable {

    private AtomicIntegerArray array;

    public Decrementer(AtomicIntegerArray array) {
        this.array = array;
    }

    @Override
    public void run() {
        for (int i = 0; i < array.length(); i++) {
            array.getAndDecrement(i);
        }
    }
}

class Incrementer implements Runnable {

    private AtomicIntegerArray array;

    public Incrementer(AtomicIntegerArray array) {
        this.array = array;
    }

    @Override
    public void run() {
        for (int i = 0; i < array.length(); i++) {
            array.getAndIncrement(i);
        }
    }
}

Atomic*Reference引用类型原子类

  • AtomicReference:AtomicReference类的作用,和AtomicInteger并没有本质区别,AtomicInteger可以让一个整数保证原子性,而AtomicReference可以让一个对象保证原子性,当然,AtomicReference的功能明显比AtomicInteger强,因为一个对象里可以包含很多属性。用法和AtomicInteger类似

AtomicIntegerFieleUpdater升级原来变量

  • AtomicIntegerFieleUpdater对普通变量进行升级
  • 使用场景:偶尔需要一个原子get-set操作
  • 可见范围(不支持private)、不支持static
/**
 * 描述:     演示AtomicIntegerFieldUpdater的用法
 */
public class AtomicIntegerFieldUpdaterDemo implements Runnable{

    static Candidate tom;
    static Candidate peter;

    public static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater
            .newUpdater(Candidate.class, "score");

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            peter.score++;
            scoreUpdater.getAndIncrement(tom);
        }
    }

    public static class Candidate {
        volatile int score;
    }

    public static void main(String[] args) throws InterruptedException {
        tom=new Candidate();
        peter=new Candidate();
        AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("普通变量:"+peter.score);
        System.out.println("升级后的结果"+ tom.score);
    }
} 

Adder累加器

  • 是Java 8引入的,相对是比较新的一个类
  • 高并发下LongAdder比AtomicLong效率高,不过本质是空间换时间
  • 竞争激烈的时候,LongAdder把不同线程对应到不同的Cell(它的内部结构)上进行修改,降低了冲突的概率,是多段锁的理念,提高了并发性
/**
 * 描述:     演示高并发场景下,LongAdder比AtomicLong性能好
 */
public class AtomicLongDemo {

    public static void main(String[] args) throws InterruptedException {
        AtomicLong counter = new AtomicLong(0);
        ExecutorService service = Executors.newFixedThreadPool(20);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            service.submit(new Task(counter));
        }
        service.shutdown();
        while (!service.isTerminated()) {

        }
        long end = System.currentTimeMillis();
        System.out.println(counter.get());
        System.out.println("AtomicLong耗时:" + (end - start));
    }

    private static class Task implements Runnable {

        private AtomicLong counter;

        public Task(AtomicLong counter) {
            this.counter = counter;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                counter.incrementAndGet();
            }
        }
    }
}
/**
 * 描述:     演示高并发场景下,LongAdder比AtomicLong性能好
 */
public class LongAdderDemo {

    public static void main(String[] args) throws InterruptedException {
        LongAdder counter = new LongAdder();
        ExecutorService service = Executors.newFixedThreadPool(20);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            service.submit(new Task(counter));
        }
        service.shutdown();
        while (!service.isTerminated()) {

        }
        long end = System.currentTimeMillis();
        System.out.println(counter.sum());
        System.out.println("LongAdder耗时:" + (end - start));
    }

    private static class Task implements Runnable {

        private LongAdder counter;

        public Task(LongAdder counter) {
            this.counter = counter;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        }
    }
}
  • 这里演示多线程情况下AtomicLong的性能,有16个线程对同一个AtomicLong累加

  • 由于竞争很激烈,每一个加法,都要flush和refresh,导致很耗费资源

  • 在内部,这个LongAdder的实现原理和刚才的AtomicLong是有不同的,刚才的AtomicLong的实现原理是,每一次加法都需要做同步,所以在高并发的时候会导致冲突比较多,也就降低了效率

  • 而此时的LongAdder,每个线程会有自己的一个计数器,仅用来在自己线程内计数,这样一来就不会和其他线程的计算器干扰

LongAdder带来的改进和原理

  • LongAdder引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数:
  • base变量:竞争不激烈,直接累加到该变量上
  • Cell[]数组:竞争激烈,各个线程分散累加到自己的槽Ceel[i]中

对比AtomicLong和Longadder

  • 在低争用下,AtomicLong和LongAdder这两个类就有相似的特征。但是在竞争激烈的情况下,LongAdder的预期吞吐量要高得多,但要消耗更多的空间
  • LongAdder适合的场景是统计求和计数的场景,而且LongAdder基本只提供add方法,而AtomoicLong还具有cas方法

Accumulator累加器

  • Accumulator和Adder非常相似,Accumulator就是一个更通用版本的Adder
/**
 * 描述:     演示LongAccumulator的用法
 */
public class LongAccumulatorDemo {
    public static void main(String[] args) {
        LongAccumulator accumulator = new LongAccumulator((x, y) -> 2 + x * y, 1);
        ExecutorService executor = Executors.newFixedThreadPool(8);
        IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));

        executor.shutdown();
        while (!executor.isTerminated()) {

        }
        System.out.println(accumulator.getThenReset());
    }
}

CAS原理

什么是CAS

  • CAS是用来实现线程安全的一种算法。利用了CPU的特殊指令,例如(Compare-and-Swap),其次一个指令就可以做好几个事情(比如 先比较后更新),实现了不能被打断的数据交换操作,避免了多线程情况下执行顺序不确定或者某个线程被暂停等不可预知的问题,从而到达线程安全。
  • 我认为V的值应该是A,如果是的话那我就把它改成B,如果不是A(说明被别人修改过了,那我就不修改了),避免多人同时修改导致出错
  • CAS有三个操作数:内存值V、预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才将内存值修改为B, 否则什么都不做。最后返回现在的V值
  • CAS的等价代码
**
 * 描述:     模拟CAS操作,等价代码
 */
public class SimulatedCAS {
    private volatile int value;

    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue) {
            value = newValue;
        }
        return oldValue;
    }
}

应用场景

  • 乐观锁
  • 并发容器
  • 原子类

CAS实现原子操作(AtomicInteger为例)

  • AtomicInteger加载Unsafe工具,用来直接操作内存数据
  • 用Unsafe来实现底层操作
  • 用volatile修饰value字段,保证可见性

Unsafe类

  • Unsafe是CAS的核心类。Java无法直接访问底层操作系统,而是通过native方法来访问。JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作
  • valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,这样我们就能通过unsafe来实现CAS了

Unsafe类中的compareAndSwapInt方法
1.方法中先想办法拿到变量value在内存中的地址
2.通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存的值。至此,最终完成了CAS的全过程

缺点

  • ABA问题,他在Compare-and-Swap的时候是先检查,只是检查和这个值是否相等并不检查在此时间是否被修改过。 ------ 就会认为没人修改过,接下来的操作和逻辑会出现问题。
  • 解决方法:沿用数据库的方式用乐观锁的时候添加一个版本号
  • 自旋时间过长:有一些操作是死环的,如果在此期间竞争非常激烈或长时间拿不到锁,这个自旋是消耗CPU的

final关键字和不变性

什么是不变性(Immutable)

  • 如果对象在被创建后,状态就不能被修改,那么它就是不可变的
  • 例子:person对象,final修饰过的age和name都不能再变
  • 具有不变性的对象一定是线程安全的,我们不需要对其采取任何额外的安全措施,也能保证线程安全

final的作用

  • 早期
  • 早期的Java实现版本中,会将final方法转为内嵌调用
  • 现在
  • 类防止被继承、方法防止被重写、变量防止被修改
  • 天生是线程安全的,而不需要额外的同步开销

修饰变量

final修饰变量

  • 被final修饰的变量,意味着值不能被修改。如果变量是对象,那么对象的引用不能变,但是对象自身的内容依然可以变化

final修饰变量:赋值时机

  • 属性被声明为final后,该变量则只能被赋值一次。且一旦被赋值,final的变量就不能再被改变,如论如何也不会变

final修饰:3种变量 赋值时机

  • final instance variable(类中的final属性)

第一种是在声明变量的等号右边直接赋值
第二种就是构造函数中赋值
第三就是在类的初始代码块中赋值(不常用)
如果不采用第一种赋值方法,那么就必须在第2、3种挑一个来赋值,而不能不赋值,这是final语法所规定的

  • final static variable(类中的static final属性)
    两个

两个赋值时机:除了在声明变量的等号右边直接赋值外,static final变量还可以用static初始代码块赋值,但是不能用普通初始代码块赋值

  • final local variable(方法中的final变量)
  • 和前面两种不同,由于这里的变量是在方法里的,所以没有构造函数,也不存在初始代码块
  • final local variable不规定赋值时机,只要求在使用前必须赋值,这和方法中的非final变量的要求也是一样的

为什么要规定赋值时机?
如果初始化不赋予值,后续赋值,就是从null变成你的赋值,这样就违反final不变的原则了

final修饰方法

  • 构造方法不允许final修饰

  • 不可被重写,也就是不能被override,即便是子类有同样名字的方法,那也不是override,这个和static方法是一个道理

  • 引申:static方法不能被重写

final修饰类

  • 不可被继承
  • 例如典型的String类就是final的,从没见过哪个类是继承String类的

注意点

  • final修饰对象的时候,只是对象的引用不可变,而对象本身的 属性是可以变化
  • final使用原则:良好的编程习惯

不变性和final的关系

  • 不变性并意味着,简单地用final修饰就不可变了
  • 对于基本数据类型,确实被final修饰后就具有不可变性
  • 但是对于对象类型,需要该对象保证自身被创建后,状态永远不会变才可以

满足以下条件时,对象才是不可变的

  • 对象创建后,其状态就不能修改
  • 所有属性都是final修饰的
  • 对象创建过程中没有发生逸出

把变量写在线程内部 - 栈封闭

  • 在方法里新建的局部变量,实际上是存储在每个线程私有的栈空间,而每个栈的占空间是不能被其他线程所访问的,所以不会有线程安全问题。这就是著名的"栈封闭"技术,是"线程封闭"技术的一种情况
/**
 * 描述:     演示栈封闭的两种情况,基本变量和对象 先演示线程争抢带来错误结果,然后把变量放到方法内,情况就变了
 */
public class StackConfinement implements Runnable {

    int index = 0;

    public void inThread() {
        int neverGoOut = 0;
            for (int i = 0; i < 10000; i++) {
                neverGoOut++;
            }
        System.out.println("栈内保护的数字是线程安全的:" + neverGoOut);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            index++;
        }
        inThread();
    }

    public static void main(String[] args) throws InterruptedException {
        StackConfinement r1 = new StackConfinement();
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r1);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(r1.index);
    }
}

并发容器精讲

并发容器概览

  • ConcurrentHashMap: 线程安全的HashMap
  • CopyOnWriteArrayList: 线程安全的List
  • BlockingQueue: 这是一个接口,表示阻塞队列,非常适用于作为数据共享的通道
  • ConcurrentLinkedQueue: 高效的非阻塞并发队列,使用链表实现。可以看做一个线程安全的LinkedList
  • ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找

集合类的历史

古老和过时的同步容器

  • Vector 和 Hashtable

ArrayList 和 HashMap

  • 虽然这两个类不是线程安全的,但是可以用Collections.synchronizedList(new ArrayList())和Collections.synchronizedMap(new HashMap()) 使之变成线程安全的

ConcurrentHashMap和CopyOnWriteArrayList

  • 取代同步的HashMap和同步的ArrayList
  • 绝大多数并发情况下,ConcurrentHashMap和CopyOnWriteArrayList的性能都更好

ConcurrentHashMap(重点、面试常考)

Map简介

  • HashMap
  • Hashtable
  • LinkedHashMap
  • TreeMap
    Java并发工具(学习笔记)_第21张图片
    Java并发工具(学习笔记)_第22张图片

为什么需要ConcurrentHashMap

  • 为什么不用 Collections.synchronizedMap()?

它是通过一个锁来保证不同线程之间的并发访问的,但是由于synchronized对于并发量高的时候性能并不理想

  • 为什么HashMap是线程不安全的

同时put碰撞导致数据丢失(如果多个线程同时put,可能计算出的hash值是一样的,那么这两个key会放在同一个位置。但是两个线程都放在同一个位置,会有一个人是丢失的)
同时put扩容导致数据丢失(如果多个同时put并且同时发现需要扩容,扩容之后的数组也只有一个会保留下来)
多线程同时扩容可能造成循环链表导致CPU100%

HashMap的分析
Java并发工具(学习笔记)_第23张图片
Java并发工具(学习笔记)_第24张图片
Java并发工具(学习笔记)_第25张图片
Java并发工具(学习笔记)_第26张图片
HashMap关于并发的特点

  • 非线程安全
  • 迭代时不允许修改内容
  • 只读的并发是安全的
  • 如果一定要把HashMap用在并发环境,用Collections.synchronizedMap(new HashMap())

JDK1.7的ConcurrentHashMap实现和分析
Java并发工具(学习笔记)_第27张图片
Java并发工具(学习笔记)_第28张图片

  • Java 7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法
  • 每个segment 独立上了ReentrantLock锁,每个segment之间互不影响,提高了并发效率
  • ConurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写(操作分别分布在不同的Segment上)。这个默认值可以在初始化的时候设置为其他值,但是一旦初始化后,是不可以扩容的

JDK1.8的ConcurrentHashMap实现和源码分析

  • 把1.7的版本完全重写了,代码从1.7的1000多行变成了现在6000多行。在实现上和在结构上和以前的segment有很大的区别。
  • 不再采用segment,而是采用node。保证线程安全的方式是CAS和synchronized
    Java并发工具(学习笔记)_第29张图片

putVal流程
判断key value不为空
计算hash值
根据对应位置节点的类型,来赋值,或者helpTransfer,或者增长链表,或者给红黑树增加节点
检查满足阈值就“红黑树化”
返回oldVal

get流程
计算hash值
找到对应的位置,根据情况进行:
直接取值
红黑树里找值
遍历链表取值
返回找到的结果

对比1.7和1.8的优缺点,为什么要把1.7的结构改为1.8

  • 数据结构:1.7中是segment结构,1.8中是链表加红黑树的结构,从以前默认的16变成了每个Node都独立,提高了并发性
  • Hash碰撞:1.7遇到Hash碰撞是拉链法用链表的形式用下,在1.8中先使用拉链法然后如果达到了条件就转成红黑树做近一步的平衡提升效率
  • 保证并发安全:1.7中采用分段锁采用segment保证线程安全的,segment继承于ReentrantLock;1.8中是CAS+sychronized
  • 查询复杂度:1.7中链表的查询复杂度是O(n),1.8中变成了红黑树时间复杂度为O(logn)
  • 为什么超过8要转为红黑树?:首先在数据量并不多的时候即使用链表也无所谓。刚开始不用红黑树后来转是红黑树的每一个结点占用的空间是链表的两倍,在空间上的损耗比链表大

ConcurrentHashMap也不是线程安全的?

不是用了ConcurrentHashMap你所做的任何事情都是线程安全的
所保证的是多个线程同时put数据不会错乱,在HashMap中多个数据同时put,内部的数据会发生错误导致结果不可预知

/**
 * 描述:     组合操作并不保证线程安全
 */
public class OptionsNotSafe implements Runnable {

    private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<String, Integer>();

    public static void main(String[] args) throws InterruptedException {
        scores.put("小明", 0);
        Thread t1 = new Thread(new OptionsNotSafe());
        Thread t2 = new Thread(new OptionsNotSafe());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(scores);
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            while (true) {
                Integer score = scores.get("小明");
                Integer newScore = score + 1;
                boolean b = scores.replace("小明", score, newScore);
                if (b) {
                    break;
                }
            }
        }
    }
}

CopyOnWriteArrayList

诞生的历史和原因

  • 代替Vector和SynchronizedList,就和ConcurrentHashMap代替SynchronizedMap的原因一样
  • Vector和SynchronizedList的锁的粒度太大,并发效率相对比较低,并且迭代时无法编辑
  • Copy-On-Write并发容器还包括CopyOnWriteArraySet,用来替代同步Set

使用场景

  • 读操作可以尽可能地快,而写即使慢一些也没有太大关系
  • 读多写少:黑名单,每日更新;监听器:迭代操作远多于修改操作

读写规则

  • 回顾读写锁:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
  • 读写锁规则的升级:读取是完全不用加锁的,并且更厉害的是,写入也不会阻塞读取操作,只有写入和写入之间需要同步等待

实现原理

  • CopyOnWrite 的含义:在计算机中如果要对一个内存进行修改有一个策略是不会内存直接进行修改,把这块内存拷贝一份,把拷贝的东西放在新的一块内存中进行修改,写完之后把指向原来内存的指针指向新建的内存
  • 好处: 读的时候不受限制。因为修改的过程中其实是在一个全新的地方在操作。不去管新的东西依然去读旧的,二者是不干涉的。 虽然有一定程度上的过期,不能拿到最新的数据,至少是能够操作的,提高了我们的并发效率 (适用的场合:更新的频率很低)
  • 创建新副本、读写分离: 所有的修改操作都是通过底层新建一个数组完成的,在他被修改的时候对整个原有数组进行一个复制,把修改的内容写入一个新的副本中再替换回去。 这是一个读写分离的思想,读和写使用完全不同的容器
  • 不可变原理: CopyOnWriteArrayList每一次修改的过程中都是新建一个新的容器,对于旧的容器来说没有任何人去操作他,他就是不可变的。也就是线程安全的,并发的读取没有问题。
  • 迭代的时候:如果数组的原内容已经被修改过了,但是迭代器它是不知道的,依然使用的是旧数组也不会报错,可能使用的数据是过期的

缺点

  • 数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上就能读到,请不要使用CopyOnWrite容器
  • 内存占用问题: 因为CopyOnWrite的写是赋值机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存

并发队列Queue(阻塞队列、非阻塞队列)

为什么要使用队列

  • 用队列可以在线程间传递数据:生产者消费者模式、银行转账
  • 考虑锁等线程安全问题的重任从“你”转移到了“队列”上

各并发队列关系图
Java并发工具(学习笔记)_第30张图片

阻塞队列BlockingQueue

什么是阻塞队列

  • 阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是具有阻塞功能
  • 通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的

阻塞功能: 最有特色的两个带有阻塞功能的方法是:

  • take() 方法:获取并移除队列的头结点,一旦如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据

  • put() 方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间

  • 是否有界(容量有多大) : 这是一个非常重要的属性,无界队列以为着里面可以容纳非常多(Integer.MAX_VALUE,约为2^31,是一个非常大的数,可以近似认为是无限容量)

  • 阻塞队列和线程池的关系:阻塞队列是线程池的重要组成部分

主要方法介绍

  • put,take(二者会阻塞)
  • add,remove,element(三者会抛出异常)
  • offer,poll,peek(offer返回布尔值,poll返回null)

ArrayBlockingQueue

  • 有界、指定容量
  • 公平: 还可以指定是否需要保证公平,如果想保证公平的话,那么等待了最长时间的线程会被优先处理,不过这会同时带来一定的性能损耗
  • 使用案例:有10个面试者,一共只有1个面试官,大厅里有3个位子供面试者休息,每个人的面试时间是10秒,模拟所有人面试的场景
public class ArrayBlockingQueueDemo {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
        Interviewer r1 = new Interviewer(queue);
        Consumer r2 = new Consumer(queue);
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

class Interviewer implements Runnable {

    BlockingQueue<String> queue;

    public Interviewer(BlockingQueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        System.out.println("10个候选人都来啦");
        for (int i = 0; i < 10; i++) {
            String candidate = "Candidate" + i;
            try {
                queue.put(candidate);
                System.out.println("安排好了" + candidate);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            queue.put("stop");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Consumer implements Runnable {

    BlockingQueue<String> queue;

    public Consumer(BlockingQueue queue) {

        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String msg;
        try {
            while(!(msg = queue.take()).equals("stop")){
                System.out.println(msg + "到了");
            }
            System.out.println("所有候选人都结束了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

LinkedBlockingQueue

  • 无界
  • 容量 Integer.MAX_VALUE
  • 内部:Node、两把锁。分析put方法

PriorityBlockingQueue

  • 支持优先级
  • 自然顺序(而不是先进先出)
  • 无界队列
  • PriorityQueue的线程安全版本

SynchronousQueue

  • 它的容量为0
  • 需要注意的是,SynchronousQueue的容量不是1二十0,因为SynchronousQueue不需要去持有元素,它所做的就是直接传递(direct handoff)
  • 效率很高
  • SynchronousQueue没有peek等函数,因为peek的含义是取出头结点,但是SynchronousQueue的容量是0,所以连头结点都没有,也就没有peek方法。同理,没有iterate相关方法
  • 是一个极好的用来直接传递的并发数据结构
  • SynchronousQueue的线程池Executors.newCachedThreadPool() 使用的阻塞队列

DelayQueue

  • 延迟队列,根据延迟时间排序
  • 元素需要实现Delayed接口,规定排序规则

非阻塞队列

  • 并发包中的非阻塞队列只有ConcurrentLinkedQueue这一种,它是使用链表作为其数据结构的,使用CAS非阻塞算法来实现线程安全(不具备阻塞功能),适合用在对性能要求较高的并发场景。用的相对比较少一些

如何选择适合自己的队列

  • 边界、空间、吞吐量

线程协作、控制并发流程

什么是控制并发流程

  • 控制并发流程的工具类,作用就是帮助我们程序员更容易让线程之间合作
  • 让线程之间相互配合,来满足业务逻辑
  • 比如让线程A等待线程B执行完毕后再执行等合作策略
    Java并发工具(学习笔记)_第31张图片

CountDownLatch倒计时门闩

CountDownLatch类的作用

  • 并发流程控制的工具 倒数门闩
  • 例子:购物拼团;大巴(游乐园坐过山车排队),人满发车
  • 流程:倒数结束之前,一直处于等待状态,直到倒计时结束了,此线程才继续工作

类的主要方法介绍

 - CountDownLatch(int count):仅有一个**构造函数**,参数cout为需要倒数的数值
  • await(): 调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
  • countDown(): 将count值减1,直到为0时,等待的线程会被唤起
    Java并发工具(学习笔记)_第32张图片

两个典型用法

  • 用法一: 一个线程等待多个线程都执行完毕,再继续自己的工作
  • 用法二: 多个线程等待某一个线程的信号,同时开始执行

注意点

  • 扩展用法:多个线程等多个线程完成执行后,再同时执行
  • CountDownLatch是不能重用的,如果需要重新计数,可以考虑CyclicBarrier或者创建新的CountDownLatch实例

总结

  • 两个典型用法:一等多多等一
  • CountDownLatch类在创建实例的时候,需要传递倒数次数。 倒数到0的时候,之前等待的线程会继续运行
  • CountDownLatch不能回滚重置
/**
 * 描述:     工厂中,质检,5个工人检查,所有人都认为通过,才通过
 */
public class CountDownLatchDemo1 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = new Runnable() {

                @Override
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("No." + no + "完成了检查。");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        latch.countDown();
                    }
                }
            };
            service.submit(runnable);
        }
        System.out.println("等待5个人检查完.....");
        latch.await();
        System.out.println("所有人都完成了工作,进入下一个环节。");
    }
}
/**
 * 描述:     模拟100米跑步,5名选手都准备好了,只等裁判员一声令下,所有人同时开始跑步。
 */
public class CountDownLatchDemo2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch begin = new CountDownLatch(1);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("No." + no + "准备完毕,等待发令枪");
                    try {
                        begin.await();
                        System.out.println("No." + no + "开始跑步了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            service.submit(runnable);
        }
        //裁判员检查发令枪...
        Thread.sleep(5000);
        System.out.println("发令枪响,比赛开始!");
        begin.countDown();
    }
}
/**
 * 描述:     模拟100米跑步,5名选手都准备好了,只等裁判员一声令下,所有人同时开始跑步。当所有人都到终点后,比赛结束。
 */
public class CountDownLatchDemo1And2 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch begin = new CountDownLatch(1);
        CountDownLatch end = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("No." + no + "准备完毕,等待发令枪");
                    try {
                        begin.await();
                        System.out.println("No." + no + "开始跑步了");
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("No." + no + "跑到终点了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        end.countDown();
                    }
                }
            };
            service.submit(runnable);
        }
        //裁判员检查发令枪...
        Thread.sleep(5000);
        System.out.println("发令枪响,比赛开始!");
        begin.countDown();

        end.await();
        System.out.println("所有人到达终点,比赛结束");
    }
}

Semaphore信号量

  • Semaphore可以用来限制或管理数量有限的资源的使用情况
  • 污染不能太多,污染许可证只能发3张
  • 信号量的作用是维护一个 “许可证” 的计数,线程可以“获取”许可证,那信号量剩余的许可证就减一,线程也可以“释放”一个许可证,那信号量剩余的许可证就加一,当信号量所拥有的许可证数量为0,那么下一个还想要获取许可证的线程,就需要等待,直到有另外的线程释放了许可证

信号量使用流程

  • 1.初始化Semaphore并指定许可证的数量
  • 2.在需要被现在的代码前加 **acquire()**或者acquireUninterruptibly()方法
  • 3.在任务执行结束后,调用 release() 来释放许可证

信号量主要方法介绍

  • new Semaphore(int permits,boolean fair): 这是可以设置是否要使用公平策略,如果传入true,那么Semaphore会把之前等待的线程放到FIFO的队列里,以便于当有了新的许可证,可以分发给之前等了最长时间的线程
  • acquire() (与下面的区别是可以响应中断)
  • acquireUninterruptibly()
  • tryAcquire(): 看看现在有没有空闲的许可证,如果有的话就获取,如果没有的话也没关系,不必陷入阻塞,我可以去做别的事,过一会再来查看许可证的空闲情况
  • tryAcquire(timeout): 和tryAcquire()一样,但是多了一个超时时间,比如“在3秒内获取不到许可证,就去做别的事”
  • release() 归还许可证

信号量特殊用法

  • 一次性获取或释放多个许可证
  • 比如TaskA会调用很多消耗资源的method1(),而TaskB调用的是不太消耗资源的method2(),假设我们一共有5个许可证。那么我们就可以要求TaskA获取5个许可证才能执行而TaskB只需要获取到一个许可证就能执行,这样就避免了A和B同时运行的情况,我们可以根据自己的需求合理分配资源

注意点

    1. 获取释放的许可证数量必须一致,否则比如每次都获取2个但是只释放1个甚至不释放,随着时间的推移,到最后许可证数量不够用,会导致程序卡死。(虽然信号量类并不对是否和获取的数量做规定,但是这是我们的编程规范,否则容易出错)
    1. 注意初始化Semaphore的时候设置公平性,一般设置为true会更合理
    1. 并不是必须由获取许可证的线程释放那个许可证,事实上,获取和释放许可证对线程并无要求,也许是A获取了,然后由B释放, 只要逻辑合理即可
    1. 信号量的作用,除了控制临界区最多同时有N个线程访问外,另一个作用是可以实现“条件等待”,例如线程1需要在线程2完成准备工作后才能开始工作,那么就线程1acquire(),而线程2完成任务后release(),这样的话,相当于是轻量级的CountDownLatch

Condition接口(又称条件对象)

作用

  • 当线程1需要等待某个条件的时候,它就是执行condition.await() 方法,一旦执行了await()方法,线程就会进入阻塞状态
  • 然后通常会有另外一个线程,假设是线程2,去执行对应的条件,直到这个条件达成的时候,线程2就会去执行condition.signal() 方法,这时JVM就会从被阻塞的线程里找,找到那些等待该condition的线程,当线程1就会收到可执行信号的时候,它的线程状态就会变成Runnable 可执行状态

signalAll()和signal()的区别

  • signalAll()会唤起所有的正在等待的线程
  • 但是signal()是公平的,只会唤起那个等待时间最长的线程

Condition注意点

  • 实际上,如果说Lock用来代替synchronized,那么Condition就是用来代替相对应的Object.wait/notify的,所以在用法和性质上,几乎都一样
  • await方法会自动释放持有的Lock锁,和Object.wait一样,不需要自己手动先释放锁
  • 调用await的时候,必须持有锁,否则会抛出异常,和Object.wait一样
/**
 * 描述:     演示Condition的基本用法
 */
public class ConditionDemo1 {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    void method1() throws InterruptedException {
        lock.lock();
        try{
            System.out.println("条件不满足,开始await");
            condition.await();
            System.out.println("条件满足了,开始执行后续的任务");
        }finally {
            lock.unlock();
        }
    }

    void method2() {
        lock.lock();
        try{
            System.out.println("准备工作完成,唤醒其他的线程");
            condition.signal();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionDemo1 conditionDemo1 = new ConditionDemo1();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    conditionDemo1.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        conditionDemo1.method1();
    }
}
/**
 * 描述:     演示用Condition实现生产者消费者模式
 */
public class ConditionDemo2 {

    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public static void main(String[] args) {
        ConditionDemo2 conditionDemo2 = new ConditionDemo2();
        Producer producer = conditionDemo2.new Producer();
        Consumer consumer = conditionDemo2.new Consumer();
        producer.start();
        consumer.start();
    }

    class Consumer extends Thread {

        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == 0) {
                        System.out.println("队列空,等待数据");
                        try {
                            notEmpty.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    notFull.signalAll();
                    System.out.println("从队列里取走了一个数据,队列剩余" + queue.size() + "个元素");
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    class Producer extends Thread {

        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == queueSize) {
                        System.out.println("队列满,等待有空余");
                        try {
                            notFull.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.offer(1);
                    notEmpty.signalAll();
                    System.out.println("向队列插入了一个元素,队列剩余空间" + (queueSize - queue.size()));
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

Cyclicbarrier循环栅栏

  • CyclicBarrier循环栅栏和CountDownLatch很类似,都能阻塞一组线程
  • 当有大量线程相互配合,分别计算不用任务,并且需要最后统一汇总的时候,我们可以使用CyclicBarrier。CyclicBarrier可以构造一个集结点,当某一个线程执行完毕,它就会到集结点等待,知道所有线程都到了集结点,那么该栅栏就会撤销,所有线程再统一出发,继续执行剩下的任务
  • 生活中的例子: 咱们3个人明天中午在学校碰面,都到期后,一起讨论下学期的计划

CyclicBarrier和CountDownLatch的区别

  • 作用不同:CyclicBarrier要等固定数量的线程都到达了栅栏位置才能继续执行,而CountDownLatch只需等待数字到0,也就是说,CountDownLatch用于事件,但是CyclicBarrier是用于线程的
  • 可重用性不同: CountDownLatch在倒数到0并触发门闩打开后,就不能再次使用了,除非新建新的实例;而CyclicBarrier可以重复使用

AQS

学习AQS的思路

  • 学习AQS的目的主要是想理解原理、提高技术,以及应对面试
  • 先从应用层面理解为什么需要他如何使用它,然后再看一看我们Java代码设计者是如何使用它的了解它的应用场景

为什么需要AQS

  • 锁和协作类有共同点 :闸门
  • ReentrantLock和Semaphore有共同点,事实上,不仅是它们,包括CountDownLatch、ReentrantReadWriteLock都有这样类似的“协作”(或者叫“同步”)功能,其实,它们底层都用了一个共同的基类,这就是AQS
  • 因为上面的那些协作类,他们有很多工作都是类似的,所以如果能提取一个工具类,那么就可以直接用,对于ReentrantLock和Semaphore而言就可以屏蔽很多细节,只关注它们自己的“业务逻辑”就可以了

Semaphore和AQS的关系

  • Semaphore内部有一个Sync类,Sync类继承了AQS
  • CountDownLatch也是一样的

AQS的比喻

  • 比喻:群面、单面
  • 安排就做、叫号、先来后到等HR的工作就是AQS的工作
  • 面试官不会去关心两个面试者是不是号码相同冲突了,也不想去管面试者需要一个地方坐着休息,这些都交给HR去做
  • Semaphore: 一个人面试完了以后,后一个人才能进去继续面试
  • CountDowbLatch: 群面,等待10人到齐
  • Semaphore、CountDownLatch这些同步工具类,要做的就只是写下自己的“要人”规则。比如是“出一个,近一个”,或者说“凑齐10个,一起面试”

如果没有AQS

  • 就需要每个协作工具自己实现:

同步状态的原子性管理
线程的阻塞与解除阻塞
队列的管理

  • 在并发场景下,自己正确且高效实现这些内容,是相当有难度的,所以我们用AQS来帮我们把这些脏活累活都搞定,我们只关注业务逻辑就够了

AQS的作用

  • AQS是一个用于构建锁、同步器、协作工具类的工具类(框架)。有了AQS以后,更多的协作工具类都可以很方便得被写出来
  • 有了AQS,构建线程协作类就容易多了

AQS内部原理解析

AQS最核心的就是三大部分:

  • state
  • 控制线程抢锁和配合的FIFO队列
  • 期望协作工具类去实现的获取/释放等重要方法

state状态1

  • 这里的state的具体含义,会根据具体实现类的不用而不用,比如在Semaphore里,它表示“剩余的许可证的数量”,而在CountDownLatch里,它表示“还需要倒数的数量
  • state是volatile修饰的,会被并发地修改,所以所有修改state的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。这些方法都依赖于j.u.c.atomic包的支持

state状态2

  • ReentrantLock
  • state用来表示“锁”的占有情况,包括可重入计数
  • 当state的值为0的时候,标识改Lock不被任何线程所占有

控制线程抢锁和配合的FIFO队列

  • 这个队列用来存放 “等待的线程”,AQS就是“排队管理器”,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁
  • AQS会维护一个等待的线程队列,把线程都放在这个队列里
  • 这是一个双向形式的队列

Java并发工具(学习笔记)_第33张图片
期望协作工具类去实现的获取/释放等重要方法

  • 这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同
  • 获取方法
  • 获取操作会依赖state变量,经常会阻塞(比如获取不到锁的时候)
  • Semaphore中,获取就是acquire 方法,作用是获取一个许可证
  • 而在CountDownLatch里面,获取就是await方法,作用是**“等待,直到倒数结束”**
  • 释放方法
  • 释放操作不会阻塞
  • 在Semaphore中,释放就是release方法,作用是释放一个许可证
  • CountDownLatch里面,获取就是countDown方法,作用是“倒数1个数
  • 需要重写tryAcquire和tryRelease等方法

应用实例、源码解析

AQS 用法

  • 第一步:写一个类,想好协作的逻辑,实现获取/释放方法
  • 第二步:内部写一个Sync类继承AbstractQueuedSynchronizer
  • 第三步:根据是否独占来重写tryAcquire/tryRelease或tryAcquireShared(int acquires)和tryReleaseShared(int releases)等方法,在之前写的获取/释放中法中调用 AQS的acquire/release或者Shared方法

AQS在CountDownLatch的应用

  • 构造函数
  • getCount
  • countDown
  • await
  • 调用CountDownLatch的await方法时,便会尝试获取“共享锁”,不过一开始是获取不到该锁的,于是线程被阻塞
  • 而“共享锁”可获取到的条件,就是“锁计数器”的值为0
  • 而“锁计数器”的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将“锁计数器” -1
  • count个线程调用countDown()之后,锁计数器才为0,而前面提到的等待获取共享锁的线程才能继续运行

AQS在Semaphore的应用

  • 在Semaphore中,state表示许可证的剩余数量
  • 看tryAcquire方法,判断nonfairTryAcquireShared大于等于0的话,代表成功
  • 这里会先检查剩余许可证数量够不够这次需要的,用减法来计算,如果直接不够,那就返回负数,表示失败,如果够了,就用自旋加compareAndSetState来改变state状态,直接改变成功就返回正数;或者是期间如果被其他人修改了导致剩余数量不够了,那也返回负数代表获取失败

AQS在ReentrantLock的应用

  • 分析释放锁的方法tryRelease
  • 由于是可重入的,所以state代表重入的次数,每次释放锁,先判断是不是当前持有锁的线程释放的,如果不是就抛异常,如果是的话,重入次数就减一,如果减到了0,就说明完全释放了,于是free就是true,并且把state设置为0
  • 加锁的方法

利用AQS实现一个自己的Latch门闩

/**
 * 描述:     自己用AQS实现一个简单的线程协作器
 */
public class OneShotLatch {

    private final Sync sync = new Sync();

    public void signal() {
        sync.releaseShared(0);
    }
    public void await() {
        sync.acquireShared(0);
    }

    private class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected int tryAcquireShared(int arg) {
            return (getState() == 1) ? 1 : -1;
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
           setState(1);

           return true;
        }
    }


    public static void main(String[] args) throws InterruptedException {
        OneShotLatch oneShotLatch = new OneShotLatch();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"尝试获取latch,获取失败那就等待");
                    oneShotLatch.await();
                    System.out.println("开闸放行"+Thread.currentThread().getName()+"继续运行");
                }
            }).start();
        }
        Thread.sleep(5000);
        oneShotLatch.signal();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"尝试获取latch,获取失败那就等待");
                oneShotLatch.await();
                System.out.println("开闸放行"+Thread.currentThread().getName()+"继续运行");
            }
        }).start();
    }
}

Future和Callable -----治理线程第二大法宝

Runnable的缺陷

  • 不能返回一个返回值
  • 也不能抛出 checked Exception(代码演示)

Callable接口

  • 类似于Runnable,被其他线程执行的任务
  • 实现call方法
  • 有返回值

Future类

  • Future的作用
  • 一个方法的计算可能很耗时,在计算的过程中没有必要在原地一直等待,用子线程去执行并且可以去做其他的事情,直到想要获取结果的时候再用Future获取过来

Callable和Future的关系

  • 可以用Future.get 来获取Callable接口返回的执行结果,还可以通过Future.isDone()来判断任务是否已经执行完了,以及取消这个任务,限时获取任务的结果等
  • 在call()未执行完毕之前,调用get()的线程(假定此时是主线程)会被阻塞,直到call()方法返回了结果后,此时future.get()才会得到该结果,然后主线程才会切换到runnable状态
  • 所以Future是一个存储器,它存储了call()这个任务的结果,而这个任务的执行时间是无法提前确定的,因为这完全取决于call()方法执行的情况

get()方法:获取结果
get方法的行为取决于Callable任务的状态,只有以下这5种情况:

  • 任务正常完成:get方法会立刻返回结果
  • 任务尚未完全(任务还没开始或进行中):get将阻塞并直到任务完成
  • 任务执行过程中抛出Exception:get方法会抛出ExecutionException:这里的抛出异常,是call()执行时产生的那个异常,看到这个异常类型是java.util.concurrent.ExecutionException。不论call()执行时抛出的异常类型是什么,最后get方法抛出的异常类型都是ExecutionException
  • 任务被取消:get方法会抛出CancellationException
  • 任务超时:get方法有一个重载方法,是传入一个延迟时间的,如果时间到了还没有获得结果,get方法就会抛出TimeoutException

get(long timeout,TimeUnit unit):有超时的获取

  • 超时的需求很常见
  • 用get(long timeout,TimeUnit unit)方法时,如果call()在规定时间内完成了任务,那么就会正常获取到返回值;而如果在指定时间内没有计算出结果,那么就会抛出TimeoutException
  • 超时不获取,任务需取消

cancel方法

  • 取消任务的执行

isDone() 方法

  • 只判断线程是否执行完毕,不判断线程是否执行成功

isCancelled()方法

  • 判断是否被取消

用法1:线程池的submit方法返回Future对象

  • 首先,要给线程池提交任务,提交时线程池会立刻返回给我们一个空的Future 容器。当线程的任务一旦执行完毕,也就是当我们可以获取结果的时候,线程池便会把该结果填入到之前给我们的那个Future中去(而不是创建一个新的Future),此时便可以从该Future中获得任务执行的结果

Future代码演示

  • get基本用法
  • Callable的Lambda表达式形式
  • 多个任务,用Future数组来获取结果
  • 任务执行过程中抛出Exception和isDone展示
  • 获取任务超时

cancel方法:取消任务的执行

  1. 如果这个任务还没有开始执行,那么这种情况最简单,任务会被正常的取消,未来也不会被执行,方法返回true
  2. 如果任务已完成,或者已取消:那么cancel()方法会执行失败,方法返回false
  3. 如果这个任务已经开始执行了,那么这个取消方法将不会直接取消该任务,而是会根据我们填的参数mayInterruptIfRunning做判断

Future.cancel(true)适用于:

任务能够处理interrupt

Future.cancel(false) 仅用于避免启动尚未启动的任务,适用于:

1.未能处理interrupt的任务
2.不清楚任务是否支持取消
3.需要等待已经开始的任务执行完成

/**
 * 描述:     演示一个Future的使用方法
 */
public class OneFuture {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        Future<Integer> future = service.submit(new CallableTask());
        try {
            System.out.println(future.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }

    static class CallableTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            Thread.sleep(3000);
            return new Random().nextInt();
        }
    }
}
/**
 * 描述:     演示一个Future的使用方法,lambda形式
 */
public class OneFutureLambda {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
//        Callable callable = new Callable() {
//            @Override
//            public Integer call() throws Exception {
//                Thread.sleep(3000);
//                return new Random().nextInt();
//            }
//        };
        Callable<Integer> callable = () -> {
            Thread.sleep(3000);
            return new Random().nextInt();
        };
        Future<Integer> future = service.submit(callable);
        try {
            System.out.println(future.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}
/**
 * 描述:     演示批量提交任务时,用List来批量接收结果
 */
public class MultiFutures {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(20);
        ArrayList<Future> futures = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            Future<Integer> future = service.submit(new CallableTask());
            futures.add(future);
        }
        Thread.sleep(5000);
        for (int i = 0; i < 20; i++) {
            Future<Integer> future = futures.get(i);
            try {
                Integer integer = future.get();
                System.out.println(integer);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

    static class CallableTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            Thread.sleep(3000);
            return new Random().nextInt();
        }
    }
}
/**
 * 描述:     演示get方法过程中抛出异常,for循环为了演示抛出Exception的时机:
 * 并不是说一产生异常就抛出,直到我们get执行时,才会抛出。
 */
public class GetException {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(20);
        Future<Integer> future = service.submit(new CallableTask());

        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(i);
                Thread.sleep(500);
            }
            System.out.println(future.isDone());
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("InterruptedException异常");
        } catch (ExecutionException e) {
            e.printStackTrace();
            System.out.println("ExecutionException异常");
        }
    }


    static class CallableTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            throw new IllegalArgumentException("Callable抛出异常");
        }
    }
}
**
 * 描述:     演示get的超时方法,需要注意超时后处理,调用future.cancel()。演
 * 示cancel传入truefalse的区别,代表是否中断正在执行的任务。
 */
public class Timeout {

    private static final Ad DEFAULT_AD = new Ad("无网络时候的默认广告");
    private static final ExecutorService exec = Executors.newFixedThreadPool(10);

    static class Ad {
        String name;

        public Ad(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Ad{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }


    static class FetchAdTask implements Callable<Ad> {

        @Override
        public Ad call() throws Exception {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                System.out.println("sleep期间被中断了");
                return new Ad("被中断时候的默认广告");
            }
            return new Ad("旅游订票哪家强?找某程");
        }
    }


    public void printAd() {
        Future<Ad> f = exec.submit(new FetchAdTask());
        Ad ad;
        try {
            ad = f.get(2000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            ad = new Ad("被中断时候的默认广告");
        } catch (ExecutionException e) {
            ad = new Ad("异常时候的默认广告");
        } catch (TimeoutException e) {
            ad = new Ad("超时时候的默认广告");
            System.out.println("超时,未获取到广告");
            boolean cancel = f.cancel(true);
            System.out.println("cancel的结果:" + cancel);
        }
        exec.shutdown();
        System.out.println(ad);
    }

    public static void main(String[] args) {
        Timeout timeout = new Timeout();
        timeout.printAd();
    } 
}

用法2:用FutureTask来创建Future

  • FutureTask来获取Future和任务的结果
  • FutureTask是一种包装器,可以把Callable转化为Future和Runnable,它同时实现二者的接口
  • 所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
  • 把Callable实例当做参数,生成FutureTask的对象,然后把这个对象当做一个Runnable对象,用线程池或另起线程去执行这个Runnable对象,最后通过FutureTask获取刚才执行的结果
    Java并发工具(学习笔记)_第34张图片
/**
 * 描述:     演示FutureTask的用法
 */
public class FutureTaskDemo {

    public static void main(String[] args) {
        Task task = new Task();
        FutureTask<Integer> integerFutureTask = new FutureTask<>(task);
//        new Thread(integerFutureTask).start();
        ExecutorService service = Executors.newCachedThreadPool();
        service.submit(integerFutureTask);

        try {
            System.out.println("task运行结果:"+integerFutureTask.get());

        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class Task implements Callable<Integer> {

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

Future的注意点

  • 当for循环批量获取 future的结果时,容易发生一部分线程很慢的情况,get方法调用时应使用timeout限制
  • Future的生命周期不能后退
  • 生命周期只能前进,不能后退。就和线程池的生命周期一样,一旦完全完成了任务,它就永久停在了“已完成”的状态,不能重头再来

你可能感兴趣的:(多线程,java,多线程)