前言:
大家好,我是良辰丫,今天学习多线程最后一节内容,我们主要去了解信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题,我们需要重点去掌握,💞💞💞
🧑个人主页:良辰针不戳
📖所属专栏:javaEE初阶
🍎励志语句:生活也许会让我们遍体鳞伤,但最终这些伤口会成为我们一辈子的财富。
💦期待大家三连,关注,点赞,收藏。
💌作者能力有限,可能也会出错,欢迎大家指正。
💞愿与君为伴,共探Java汪洋大海。

目录
信号量,其实就是用来表示可用资源个数,它的本质其实是一个计数器.
- 申请一个资源我们可以称为p操作.
- 释放一个资源我们叫做v操作.
其实这和生活中的例子非常相似,有的汽车充电站会有这种计数器,进去充电,相当于申请了一个充电资源;充满电断开电源相当于释放一个充电资源.
- 所谓的锁其实也可以看做一个计数器,加锁后,计数器为1,释放锁后计数器为0.
- 信号量是广义的锁,不光能管理非0即1的资源,也能管理多个资源.
如果计数器为0,继续申请资源会进入阻塞状态.
- 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
- acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
- 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
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 = new Thread(runnable);
t.start();
}

一种特别针对专有场景的组件.
同时等待 N 个任务执行结束
- 就像一场比赛,我们可以约定最后一个人到达终点比赛才会结束.
- 下载一个大文件,为了提高效率,会分块传输,只有文件全部传过去文件才会结束传输(可以使用多线程).
- 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
- 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
- 主线程中使用 latch.await,阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
public static int num;
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
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++) {
new Thread(t).start();
}
// 必须等到 10 人全部回来
latch.await();
System.out.println("比赛结束");
}

1. 线程同步有哪些?
synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.
2. 为什么有了 synchronized 还需要 juc 下的 lock?
以 juc 的 ReentrantLock 为例,
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活.
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true 开启公平锁模式.
- synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
3. AtomicInteger 的实现原理是什么?(前面文章有,可参考)
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
4. 信号量是什么?
- 信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
- 使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.
5. 解释一下 ThreadPoolExecutor 构造方法的参数
public ThreadPoolExecutor(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 时,线程池拒绝添加新任务时采取的策略。(线程池详解)
原来的集合类, 大部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的(如果多线程下进行使用,可能出现难以预料的问题).
需要多线程下使用这些东西,那么该怎么办呢?
- 使用锁,手动保证线程安全,多个线程去修改ArrayList此时可能出现问题,就可以给修改操作进行加锁.
- 标准库还提供了一些线程安全版本的集合类,如果需要使用ArrayList,可以使用Vector代替,但是这个关键方法都是带有synchronized,这是太老的集合类,不建议大家使用.
- Collections.synchronizedList(new ArrayList);synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.,synchronizedList 的关键操作上都带有 synchronized.使用这个壳可以套用你想用的集合类.
- CopyOnWriteArrayList
支持写时拷贝集合类,线程安全是多个线程修改不同的变量(没加锁),修改的时候拷贝一份.如果是多线程读,由于读本身就是线程安全,就没有事;如果此时有一个线程尝试修改,就会触发写时拷贝;由于这样的引用操作赋值,本身就是原子的,就可以保证线程安全,不用加锁,也能完成修改.
优点:在读多写少的场景下, 性能很高,不需要加锁竞争.
缺点:占用内存较多; 新写的数据不能被第一时间读到.
- ArrayBlockingQueue
基于数组实现的阻塞队列- LinkedBlockingQueue
基于链表实现的阻塞队列- PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列- TransferQueue
最多只包含一个元素的阻塞队列
HashMap本身就是线程不安全的,因此在多线程情况下一般不用.
那么在多线程情况下我们可以使用哪些呢?
- Hashtable
- ConcurrentHashMap
只是简单的把关键方法加上了 synchronized 关键字.相当于给this(对象本身)加锁.
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
- 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.
- 一个HashTable只有一把锁,两个线程访问它的任意数据都会出现锁竞争.
- 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
- 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
- 优化了扩容方式: 化整为零
①发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
②扩容期间, 新老数组同时存在.
③后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.
④搬完最后一个元素再把老数组删掉.
⑤这个期间, 插入只往新数组加.
⑥这个期间, 查找需要同时查新数组和老数组- ConcurrentHashMap中每个哈希桶都有一把锁,只有两个线程访问的恰好是同一个哈希桶上的数据时才会出现锁冲突.
- 加锁粒度不同(触发锁冲突的频率),HashTable是针对整个哈希表加锁,任何的增删查改操作都会触发加锁,也都有可能出现锁竞争.(其实没必要加锁那么勤快,会严重降级效率)
- HashTable插入元素时,根据key计算hash值(数组下标),把这个新的元素挂到对应的下标链表上.(HashMap链表太长的时候(注意是HashMap)还会把链表变成红黑树).
两个线程插入两个元素是否会出现线程安全问题?
两个线程修改不同的变量不会出现线程安全;虽然没有线程安全问题,但是由于锁是加到this上,仍然会针对同一个锁对象产生锁竞争,产生阻塞等待.
- ConcurrentHashMap中每个链表的头结点作为一把锁,每次进行操作都是针对链表的头结点进行加锁,操作不同的链表就是针对不同的锁加锁,这样就不会产生锁冲突.这样就导致大部分加锁操作实际是没有锁冲突的,此时这里加锁操作的开销就非常小了.
- 无锁编程(升级机制),更充分的利用了CAS机制,比如获取/更新元素的个数,就可以直接使用CAS完成,不必加锁.CAS也能保证线程安全,往往比锁更高效,但是这个操作不经常用,使用范围没有锁那么广泛.
- 优化了扩容策略,对于HashTable,如果元素太多,就会涉及到扩容操作,出现负载因子就需要进行扩容操作.扩容需要申请内存空间,搬运元素(把元素从旧的哈希表上删除,插到新的哈希表上);但是如果元素非常多,搬运一次,成本非常高,这就会导致put操作非常卡顿. CocurrentHashmap策略,化整为零,并不会试图一次性搬运所有的元素,每次只搬运一小部分.
put触发扩容的时候,就会直接创建更大的内存空间,一部分进行搬运(速度较快),此时相当于有两份哈希表,插入元素的时候,直接在新表操作;删除元素删旧表的;查找的时候新旧表都查.
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关键字的用法?
volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第一时间读取到最新的值.
2. Java多线程是如何实现数据共享的?
JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中,可以让多个线程都访问到.
3. Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
创建线程池主要有两种方式(需要掌握):
- 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
- 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.
LinkedBlockingQueue表示线程池的任务队列. 用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务.
4. Java线程共有几种状态?状态之间怎么切换的?
- NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.
- BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
- WAITING: 调用 wait 方法会进入该状态.
- TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
- TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.
5. 在多线程下,如果对一个数进行叠加,该怎么做?
- 使用 synchronized / ReentrantLock 加锁
- 使用 AtomInteger 原子操作
6. Servlet是否是线程安全的?
Servlet 本身是工作在多线程环境下. 如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行操作, 是可能出现线程不安全的情况的.
7. Thread和Runnable的区别和联系?
- Thread 类描述了一个线程.
- Runnable 描述了一个任务.
- 在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用Runnable 来描述这个任务
8. 多次start一个线程会怎么样?
第一次调用 start 可以成功调用. 后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常.
9. 有synchronized两个方法,两个线程分别同时调用这个方法,会发生什么呢?
synchronized 加在非静态方法上, 相当于针对当前对象加锁.
- 如果这两个方法属于同一个实例:
线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容.- 如果这两个方法属于不同实例:两者能并发执行, 互不干扰
10.线程与进程的区别?
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
//1.验证返回状态码是否是200pm.test("Statuscodeis200",function(){pm.response.to.have.status(200);});//2.验证返回body内是否含有某个值pm.test("Bodymatchesstring",function(){pm.expect(pm.response.text()).to.include("string_you_want_to_search");});//3.验证某个返回值是否是100pm.test("Yourtestname",function(){varjsonData=pm.response.json
目录第1题连续问题分析:解法:第2题分组问题分析:解法:第3题间隔连续问题分析:解法:第4题打折日期交叉问题分析:解法:第5题同时在线问题分析:解法:第1题连续问题如下数据为蚂蚁森林中用户领取的减少碳排放量iddtlowcarbon10012021-12-1212310022021-12-124510012021-12-134310012021-12-134510012021-12-132310022021-12-144510012021-12-1423010022021-12-154510012021-12-1523.......找出连续3天及以上减少碳排放量在100以上的用户分析:遇到这类
我正在尝试使用ruby编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?
我是ruby的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp
我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:
我正在构建一个小部件来显示奥运会的奖牌数。我有一个“国家”对象的集合,其中每个对象都有一个“名称”属性,以及奖牌计数的“金”、“银”、“铜”。列表应该排序:1.首先是奖牌总数2.如果奖牌相同,按类型分割(金>银>铜,即2金>1金+1银)3.如果奖牌和类型相同,则按字母顺序子排序我正在用ruby做这件事,但我想语言并不重要。我确实找到了一个解决方案,但如果感觉必须有更优雅的方法来实现它。这是我做的:使用加权奖牌总数创建一个虚拟属性。因此,如果他们有2个金牌和1个银牌,加权总数将为“3.020100”。1金1银1铜为“3.010101”由于我们希望将奖牌数排序为最高的,因此列表按降序排
所以,Ruby1.9.1现在是declaredstable.Rails应该与它一起工作,并且正在慢慢地将gem移植到它。它具有native线程和全局解释器锁(GIL)。自从GIL到位后,原生线程是否比1.9.1中的绿色线程有任何优势? 最佳答案 1.9中的线程是原生的,但它们被“放慢了速度”,一次只允许一个线程运行。这是因为如果线程真的并行运行,它会混淆现有代码。优点:IO现在在线程中是异步的。如果一个线程阻塞在IO上,那么另一个线程将继续执行直到IO完成。C扩展可以使用真正的线程。缺点:任何非线程安全的C扩展都可能存在使用Thre
我在一个ruby文件中有一个函数可以像这样写入一个文件File.open("myfile",'a'){|f|f.puts("#{sometext}")}这个函数在不同的线程中被调用,使得像上面这样的文件写入不是线程安全的。有谁知道如何以最简单的方式使这个文件写入线程安全?更多信息:如果重要的话,我正在使用rspec框架。 最佳答案 您可以通过File#flock给锁File.open("myfile",'a'){|f|f.flock(File::LOCK_EX)f.puts("#{sometext}")}
我编写了几个类来控制我想如何处理多个网站,两者都使用类似的方法(即登录、刷新)。每个类都打开自己的WATIR浏览器实例。classSite1definitialize@ie=Watir::Browser.newenddeflogin@ie.goto"www.blah.com"endend无线程的main中的代码示例如下require'watir'require_relative'site1'agents=[]agents这工作正常,但在当前代理完成登录之前不会移动到下一个代理。我想合并多线程来处理这个问题,但似乎无法让它工作。require'watir'require_relative