前言:
大家好,我是良辰丫,今天学习多线程最后一节内容,我们主要去了解信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题,我们需要重点去掌握,💞💞💞
🧑个人主页:良辰针不戳
📖所属专栏:javaEE初阶
🍎励志语句:生活也许会让我们遍体鳞伤,但最终这些伤口会成为我们一辈子的财富。
💦期待大家三连,关注,点赞,收藏。
💌作者能力有限,可能也会出错,欢迎大家指正。
💞愿与君为伴,共探Java汪洋大海。
目录
1. 信号量
信号量
,其实就是用来表示可用资源个数,它的本质其实是一个计数器.
- 申请一个资源我们可以称为p操作.
- 释放一个资源我们叫做v操作.
其实这和生活中的例子非常相似,有的汽车充电站会有这种计数器,进去充电,相当于申请了一个充电资源;充满电断开电源相当于释放一个充电资源.
- 所谓的锁其实也可以看做一个计数器,加锁后,计数器为1,释放锁后计数器为0.
- 信号量是广义的锁,不光能管理非0即1的资源,也能管理多个资源.
如果计数器为0,继续申请资源会进入阻塞状态.
- 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
- acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
- 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
Semaphore semaphore =newSemaphore(4);Runnable runnable =newRunnable(){@Overridepublicvoidrun(){try{System.out.println("申请资源");
semaphore.acquire();System.out.println("获取到资源");Thread.sleep(1000);System.out.println("释放资源了");
semaphore.release();}catch(InterruptedException e){
e.printStackTrace();}}};for(int i =0; i <20; i++){Thread t =newThread(runnable);
t.start();}
2. CountDownLatch
一种特别针对专有场景的组件.
同时等待 N 个任务执行结束
- 就像一场比赛,我们可以约定最后一个人到达终点比赛才会结束.
- 下载一个大文件,为了提高效率,会分块传输,只有文件全部传过去文件才会结束传输(可以使用多线程).
- 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
- 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
- 主线程中使用 latch.await,阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
publicstaticint num;publicstaticvoidmain(String[] args)throwsException{CountDownLatch latch =newCountDownLatch(10);Thread t =newThread(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep((long)Math.random()*10000);System.out.println(num++);
latch.countDown();}catch(Exception e){
e.printStackTrace();}}});for(int i =0; i <10; i++){newThread(t).start();}// 必须等到 10 人全部回来
latch.await();System.out.println("比赛结束");}
3. 一些常见面试题
- 线程同步有哪些?
synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.
- 为什么有了 synchronized 还需要 juc 下的 lock?
以 juc 的 ReentrantLock 为例,
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活.
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true 开启公平锁模式.
- synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
- AtomicInteger 的实现原理是什么?(前面文章有,可参考)
classAtomicInteger{privateint value;publicintgetAndIncrement(){int oldValue = value;while(CAS(value, oldValue, oldValue+1)!=true){
oldValue = value;}return oldValue;}}
- 信号量是什么?
- 信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
- 使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.
- 解释一下 ThreadPoolExecutor 构造方法的参数
publicThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
- maximumPoolSize :线程池中能拥有最多线程数.
- keepAliveTime :表示空闲线程的存活时间.
- TimeUnit unit :表示keepAliveTime的单位.
- corePoolSize :线程池中核心线程数的最大值.
- workQueue :用于缓存任务的阻塞队列.
- threadFactory :指定创建线程的工厂.
- handler :表示当 workQueue 已满,且池中的线程数达到 maximumPoolSize 时,线程池拒绝添加新任务时采取的策略。(线程池详解)
4. 线程安全的集合类
4.1 多线程环境使用 ArrayList
原来的集合类, 大部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的(如果多线程下进行使用,可能出现难以预料的问题).
需要多线程下使用这些东西,那么该怎么办呢?
- 使用锁,手动保证线程安全,多个线程去修改ArrayList此时可能出现问题,就可以给修改操作进行加锁.
- 标准库还提供了一些线程安全版本的集合类,如果需要使用ArrayList,可以使用Vector代替,但是这个关键方法都是带有synchronized,这是太老的集合类,不建议大家使用.
- Collections.synchronizedList(new ArrayList);synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.,synchronizedList 的关键操作上都带有 synchronized.使用这个壳可以套用你想用的集合类.
- CopyOnWriteArrayList 支持写时拷贝集合类,线程安全是多个线程修改不同的变量(没加锁),修改的时候拷贝一份.如果是多线程读,由于读本身就是线程安全,就没有事;如果此时有一个线程尝试修改,就会触发写时拷贝;由于这样的引用操作赋值,本身就是原子的,就可以保证线程安全,不用加锁,也能完成修改.
优点
:在读多写少的场景下, 性能很高,不需要加锁竞争.缺点
:占用内存较多; 新写的数据不能被第一时间读到.
4.2 多线程环境使用队列
- ArrayBlockingQueue 基于数组实现的阻塞队列
- LinkedBlockingQueue 基于链表实现的阻塞队列
- PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
- TransferQueue 最多只包含一个元素的阻塞队列
4.3 多线程环境使用哈希表
HashMap本身就是线程不安全的
,因此在多线程情况下一般不用.
那么在多线程情况下我们可以使用哪些呢?
- Hashtable
- ConcurrentHashMap
4.3.1 Hashtable
只是简单的把关键方法加上了 synchronized 关键字.相当于给this(对象本身)加锁.
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
- 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.
- 一个HashTable只有一把锁,两个线程访问它的任意数据都会出现锁竞争.
4.3.2 ConcurrentHashMap
- 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
- 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
- 优化了扩容方式: 化整为零 ①发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去. ②扩容期间, 新老数组同时存在. ③后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素. ④搬完最后一个元素再把老数组删掉. ⑤这个期间, 插入只往新数组加. ⑥这个期间, 查找需要同时查新数组和老数组
- ConcurrentHashMap中每个哈希桶都有一把锁,只有两个线程访问的恰好是同一个哈希桶上的数据时才会出现锁冲突.
4.3.3 Hashtable与ConcurrentHashMap的区别(重点)
- 加锁粒度不同(触发锁冲突的频率),HashTable是针对整个哈希表加锁,任何的增删查改操作都会触发加锁,也都有可能出现锁竞争.(其实没必要加锁那么勤快,会严重降级效率)
- HashTable插入元素时,根据key计算hash值(数组下标),把这个新的元素挂到对应的下标链表上.(HashMap链表太长的时候(注意是HashMap)还会把链表变成红黑树). 两个线程插入两个元素是否会出现线程安全问题? 两个线程修改不同的变量不会出现线程安全;虽然没有线程安全问题,但是由于锁是加到this上,仍然会针对同一个锁对象产生锁竞争,产生阻塞等待.
- ConcurrentHashMap中每个链表的头结点作为一把锁,每次进行操作都是针对链表的头结点进行加锁,操作不同的链表就是针对不同的锁加锁,这样就不会产生锁冲突.这样就导致大部分加锁操作实际是没有锁冲突的,此时这里加锁操作的开销就非常小了.
- 无锁编程(升级机制),更充分的利用了CAS机制,比如获取/更新元素的个数,就可以直接使用CAS完成,不必加锁.CAS也能保证线程安全,往往比锁更高效,但是这个操作不经常用,使用范围没有锁那么广泛.
- 优化了扩容策略,对于HashTable,如果元素太多,就会涉及到扩容操作,出现负载因子就需要进行扩容操作.扩容需要申请内存空间,搬运元素(把元素从旧的哈希表上删除,插到新的哈希表上);但是如果元素非常多,搬运一次,成本非常高,这就会导致put操作非常卡顿. CocurrentHashmap策略,化整为零,并不会试图一次性搬运所有的元素,每次只搬运一小部分. put触发扩容的时候,就会直接创建更大的内存空间,一部分进行搬运(速度较快),此时相当于有两份哈希表,插入元素的时候,直接在新表操作;删除元素删旧表的;查找的时候新旧表都查.
5. ConcurrentHashMap相关面试题
- ConcurrentHashMap的读是否要加锁?
读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了volatile 关键字.
- 介绍下 ConcurrentHashMap的锁分段技术?
- 这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁. 目的也是为了降低锁竞争的概率.
- 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.
- ConcurrentHashMap在jdk1.8做了哪些优化?
- 取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).
- 将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于8 个元素)就转换成红黑树.
- ) Hashtable和HashMap、ConcurrentHashMap 之间的区别?
- HashMap: 线程不安全. key 允许为 null.
- Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
- ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null.
看到这里,我们的多线程知识点就要进入尾声了,接下来我们总结几个多线程和锁常见的面试考点.
6. 多线程常见面试题
- 谈谈 volatile关键字的用法?
volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第一时间读取到最新的值.
- Java多线程是如何实现数据共享的?
JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中,可以让多个线程都访问到.
- Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
创建线程池主要有两种方式(需要掌握):
- 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
- 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.
LinkedBlockingQueue
表示线程池的任务队列. 用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务.
- Java线程共有几种状态?状态之间怎么切换的?
- NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.
- BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
- WAITING: 调用 wait 方法会进入该状态.
- TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
- TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.
- 在多线程下,如果对一个数进行叠加,该怎么做?
- 使用 synchronized / ReentrantLock 加锁
- 使用 AtomInteger 原子操作
- Servlet是否是线程安全的?
Servlet 本身是工作在多线程环境下. 如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行操作, 是可能出现线程不安全的情况的.
- Thread和Runnable的区别和联系?
- Thread 类描述了一个线程.
- Runnable 描述了一个任务.
- 在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用Runnable 来描述这个任务
- 多次start一个线程会怎么样?
第一次调用 start 可以成功调用. 后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常.
- 有synchronized两个方法,两个线程分别同时调用这个方法,会发生什么呢?
synchronized 加在非静态方法上, 相当于针对当前对象加锁.
- 如果这两个方法属于同一个实例: 线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容.
- 如果这两个方法属于不同实例:两者能并发执行, 互不干扰
10.线程与进程的区别?
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
版权归原作者 良辰针不戳 所有, 如有侵权,请联系我们删除。