@TOC
一、JUC (java.util.concurrent)
1. Callable 接口
Callable 是一个 interface . 也是一种创建线程的方式。
谈到创建多线程,就会想到Runnable 接口。
但是Runnable 有个问题:不适合于 让线程计算出一个结果,这样的代码。
例如:像创建一个线程,让这个线程计算 1+2+3+…+1000
要基于 Runnable 来实现,就很麻烦。
- 创建一个类 Result , 包含一个 sum 表示最终结果,lock 表示线程同步使用的锁对象.
- main 方法中先创建 Result 实例,然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + … + 1000.
- 主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了).
- 当线程 t 计算完毕后,通过 notify 唤醒主线程,主线程再打印结果
可以看到, 上述代码需要一个辅助类 Result, 还需要使用一系列的加锁和 wait notify 操作, 代码复 杂, 容易出错.
代码示例: 创建线程计算 1 + 2 + 3 + ... + 1000, 使用 Callable 版本
- 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
- 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
- 把 callable 实例使用 FutureTask 包装一下.
- 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
- 在主线程中调用 futureTask.get( ) 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结 果.
理解 Callable
Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作
理解 FutureTask
想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 “小票” . 这个小票就是FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没
2. ReentrantLock (可重入锁)
ReentrantLock其实就是可重入锁
我们都知道 synchronized 也是一个可重入锁。现在又蹦出一个 ReentrantLock。
为什么有了 synchronized,还需要 ReentrantLock呢?
这是因为 ReentrantLock 可以做到一些 synchronized 实现不了的功能。
也就是说 ReentrantLock 提供了 一些 synchronized 没有的功能。
基础用法
ReentrantLock 主要提供了3个方法:
1、lock:加锁
2、unlock:解锁
3、trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁
把加锁和解锁两个操作分开了
ReentrantLock lock = new ReentrantLock(); // ----------------------------------------- lock.lock(); try { // working } finally { lock.unlock() // 保证不管是否异常都能执行到 unlock, 这么写比较麻烦 }当多个线程竞争同一把锁的时候,就会阻塞。【这一点和 synchronized一样】
ReentrantLock 和 synchronized 区别
1、
- synchronized 是一个关键字
- ReentrantLock 是一个标准库的类。
关键字就意味着:其背后的逻辑是 JVM 内部实现的。(C++代码实现的)类:背后的逻辑是 Java代码实现的
2、
- synchronized 不需要手动释放锁,出了代码块,锁就自然释放了。
- ReentrantLock 必须要手动释放锁,要谨防忘记释放。
3、(重点)
- synchronized 如果竞争锁失败,就会阻塞等待
- ReentrantLock 除了会阻塞等待,还有一手:trylock【失败了,就直接返回】
trylock 给我们加锁操作增添了一份灵活性,并不需要完全去进行阻塞死等,可以根据我们的需要,来选择等还是不等,还是说等,以及等多久、
所以 trylock 给了我们更加灵活的回旋余地。
这是synchronized 所不具备的!
4、(重点)
- synchronized 是一个非公平锁。
- ReentrantLock 提供了 非公平锁 和 公平锁 两个版本!!!
在构造方法中,通过参数来指定 当前是公平锁,还是非公平锁。
5、
- 基于 synchronized 衍生出来的 等待机制,是 wait 和notify。功能是有限的
- 基于 ReentrantLock 衍生出来的 等待机制,是 Condition 类(又可称为条件变量)功能要更丰富一些。
如何选择使用哪个锁?
锁竞争不激烈的时候,使用 synchronized,效率更高,自动释放更方便
锁竞争激烈的时候,使用 ReentrantLock,搭配 trylock 更灵活控制加锁的行为,而不是死等
如果需要使用公平锁,使用 ReentrantLock
日常开发中,绝大部分情况下,synchronized就够用了!
3. Semaphore 信号量
是一个更广义的锁
锁是信号量里第一种特殊情况,叫做 “二元信号量"
理解信号量
开车经常会遇到一个情况,停车,停车场入口一般会有个牌子,上面写着 “当前空闲xx个车位”,每次有个车开出来,车位数+1这个牌子就是信号量,描述了可用资源 (车位)的个数,每次申请一个可用资源,计数器就 -1 (称为Р操作)
- 当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1 (这个称为信号量的 P 操作)
- 当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源
锁就可以视为 “二元信号量”,可用资源就一个,计数器的取值非 0 即 1
信号量就把锁推广到了一般情况,可用资源更多的时候,如何处理的
实际开发中,并不会经常用到信号量
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用
代码示例:
- 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
- acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
- 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果
4. CountDownLatch
我们可以把它理解为 “终点线”
同时等待 N 个任务执行结束
假设有一场跑步比赛,当所有的选手都冲过终点,此时认为是比赛结束
这样的场景在开发中,也是存在的
例如:多线程下载
迅雷…下载一个比较大的资源 (电影),通过多线程下载就可以提高下载速度
把一个文件拆成多个部分,每个线程负责下载其中的一个部分,得是所有的线程都完成自己的下载,才算整
个下载完
- countDown 给每个线程里面去调用,就表示到达终点了
- await 是给等待线程去调用,当所有的任务都到达终点了,await 就从阻塞中返回,就表示任务完成
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩
代码示例:
- 构造 CountDownLatch 实例,初始化 10 表示有 10 个任务需要完成
- 每个任务执行完毕,都调用 latch.countDown() ,在 CountDownLatch 内部的计数器同时自减.
- 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕,相当于计数器为 0 了
运行结果:
Thread-7 到达终点 Thread-8 到达终点 Thread-6 到达终点 Thread-9 到达终点 Thread-1 到达终点 Thread-0 到达终点 Thread-3 到达终点 Thread-2 到达终点 Thread-4 到达终点 Thread-5 到达终点 比赛结束二、线程安全的集合类
这里面其实也算是 JUC 的一部分。
Vector、Stack、HashTable 是线程安全的,但是不推荐使用,其他剩余类都是线程不安全的。
1. 多线程环境使用 ArrayList
1、 自己使用同步机制 (synchronized 或者 ReentrantLock)
2、使用标准库里面的操作:Collections.synchronizedList(new ArrayList);
Collections.synchronizedList(new ArrayList);第二种方法没有第一种方法灵活,因为并不是所有的方法都涉及到加锁。
第二种方法,就属于无脑加锁的
3、使用 CopyOnWriteArrayLis
写时拷贝,在修改的时候,会创建一份副本出来
CopyOnWrite容器即写时复制的容器。
- 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
- 添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。
优点:
- 在读多写少的场景下, 性能很高, 不需要加锁竞争.
缺点:
- 占用内存较多.
- 新写的数据不能被第一时间读取到
这样做的好处,就是修改的同时对于读操作,是没有任何影响的,读的时候优先读旧的版本不会说出现读到一个 “修改了一半” 的中间状态
也叫做 “双缓冲区" 策略操作系统,创建进程的时候,也是通过写时拷贝。显卡在渲染画面的时候,也是通过类似的机制。
也是适合于读多写少的情况,也是适合于数据小的情况更新配置数据,经常会用到这种类似的操作
2、多线程环境使用队列
1). ArrayBlockingQueue //基于数组实现的阻塞队列 2). LinkedBlockingQueue //基于链表实现的阻塞队列 3). PriorityBlockingQueue //基于堆实现的带优先级的阻塞队列 4). TransferQueue //最多只包含一个元素的阻塞队列这个在之前写过了就不写了
3、多线程下使用哈希表 【高频】
首先我们要明白 HashMap 这个类 本身 线程并不安全。不能直接在多线程中使用
解决方法:
1、使用 HashTable 类 【不推荐使用】
2、使用 ConcurrentHashMap 类 【推荐使用】
至于为什么不推荐使用 HashTable ,而是推荐 ConcurrentHashMap 类。
这就需要了解 HashTable 内部的构造,
HashTable 与 HashTableConcurrentHashMap 的区别。
HashTable 是如何保证线程安全的 ?
就是给关键方法加锁
public synchronized V put(K key, V value) { public synchronized V get(Object key) {上述这种直接对 put/get 方法进行加锁的操作。
其实就是在针对 this 来进行加锁
当有多个线程 来访问这个 HashTable 的时候,无论是什么样的操作,什么样的数据,都会出现锁竞争。
这样的设计就会导致锁竞争的概率非常大,效率就比较低!
ConcurrentHashMap 是如何保证线程安全的 ?
操作元素的时候,是针对这个 元素所在的链表的头结点 来加锁的
如果你两个线程操作是针 对两个不同的链表上的元素, 没有线程安全问题,其实不必加锁
由于 hash 表中,链表的数目非常多,每个链表的长度是相对短的,因此就可以保证锁冲突的概率就非常小了
总结:
ConcurrentHashMap 减少了锁冲突,就让锁加到每个链表的头结点上 (锁桶)
ConcurrentHashMap 只是针对写操作加锁了,读操作没加锁,而只是使用
ConcurrentHashMap 中更广泛的使用 CAS,进一步提高效率 (比如维护 size 操作)
ConcurrentHashMap 针对扩容,进行了巧妙的化整为零
-
对于 HashTable 来说,只要你这次 put 触发了扩容,就一口气搬运完,会导致这次 put 非常卡顿
- 对于 ConcurrentHashMap,每次操作只搬运一点点,通过多次操作完成整个搬运的过程
- 同时维护一个新的 HashMap 和一个旧的,查找的时候,既需要查旧的也要查新的,插入的时候**只插入新的,直到搬运完毕再销毁旧的
三、多线程哈希表面试题
1. ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁. 目的是为了进一步降低锁冲突的概率。
为了保证读到刚修改的数据, 搭配了volatile 关键字
2. 介绍下 ConcurrentHashMap的锁分段技术?
这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了。
简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.
3. ConcurrentHashMap在jdk1.8做了哪些优化?
- 取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象)。
- 将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式.
- 当链表较长的时候(大于等于 8 个元素)就转换成红黑树
4. Hashtable和HashMap、ConcurrentHashMap 之间的区别?
- HashMap: 线程不安全. key 允许为 null
- Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率低. key 不允许为 null.
- ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null
四、其他面试题
1)、谈谈 volatile关键字的用法?
1)、谈谈 volatile关键字的用法?
volatile 能够保证内存可见性.
强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰
的变量, 可以第一时间读取到最新的值
2)、Java多线程是如何实现数据共享的?
2)、Java多线程是如何实现数据共享的?
JVM 把内存分成了这几个区域:
方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到
3)、Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
3)、Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
创建线程池主要有两种方式:
- 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
- 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.
LinkedBlockingQueue 表示线程池的任务队列. 用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务.
4)、Java线程共有几种状态?状态之间怎么切换的?
4)、Java线程共有几种状态?状态之间怎么切换的?
- NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.
- BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
- WAITING: 调用 wait 方法会进入该状态.
- TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
- TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.
5)、在多线程下,如果对一个数进行叠加,该怎么做?
5)、在多线程下,如果对一个数进行叠加,该怎么做?
- 使用 synchronized / ReentrantLock 加锁
- 使用 AtomInteger 原子操作
6)、Servlet是否是线程安全的?
6)、Servlet是否是线程安全的?
Servlet 本身是工作在多线程环境下.
如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行操作, 是可能出现线程不安全的情况的
7)、Thread和Runnable的区别和联系?
7)、Thread和Runnable的区别和联系?
-
Thread 类描述了一个线程.
- Runnable 描述了一个任务.
在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用
- Runnable 来描述这个任务
8)、多次start一个线程会怎么样
8)、多次start一个线程会怎么样
第一次调用 start 可以成功调用.
后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常
9)、有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
9)、有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
synchronized 加在非静态方法上, 相当于针对当前对象加锁.如果这两个方法属于同一个实例:
- 线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容.
如果这两个方法属于不同实例:
- 两者能并发执行, 互不干扰
10)、进程和线程的区别?
10)、进程和线程的区别?
- 进程是包含线程的,每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位