并发编程的本质:充分利用CPU资源。
目录
1.准备环境
准备工作使用IDEA作为开发环境,新建一个Maven项目
1.添加一个包
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
2.配置好环境,使用jdk1.8,三步分别配置如下
环境准备完毕。
2.回顾线程和进程
一共三个包
普通的线程编码使用Thread,Runnable,效率都比callable低,实际企业里使用Callable比较多。
问题:Java真的可以开启线程吗?答案不可以。代码解释如下:
先看下Thread线程启动源码:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0(); //这里实际调用的是底层C++的方法,因为Java干不了开启线程的事情
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
//只能通过本地方法去调,底层是C++,因为Java是运行在JVM上的,无法操作底层的硬件。
并发和并行
并发:指的是多线程操作同一个资源,即交替执行;
- CPU一核,模拟出来多条线程,天下武功,唯快不破,快速交替
并行:多个人一起行走,同时执行
- CPU多核,多个线程可以同时执行;
本机CPU核数多少如下:
或者任务管理器里的
或者通过代码来获取CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
3.回顾多线程
线程的5大状态:直接贴源码,来自Thread.State
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//一直等
WAITING,
//超时等待,过期不候
TIMED_WAITING,
//线程终止
TERMINATED;
}
wait和sleep区别:
- 来自不同的类,wait->Object,sleep->Thread
- wait会释放锁,sleep抱着锁睡,不会释放锁
- wait必须在同步代码块里(synchronized),sleep可以在任何地方
4.Lock锁(重点)
基本写法
lock.lock();
try {
//业务代码块
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
Lock锁是一个接口
有三个实现类,如下
lock锁的语句写法如上
传统的synchronized写法如下,但这里并没有用到JUC的写法
public class Demo3 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"A").start();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"B").start();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"C").start();
}
}
class Ticket {
private int number = 50;
public synchronized void sale(){
if(number>0){
System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"票,剩余"+number);
}
}
}
然后使用JUC下的lock锁对他进行改造如下,首先会new一个ReentrantLock对象,
这个对象点进去看源码发现默认无参时使用非公平锁,
公平锁:十分公平,先来后到;
非公平锁:十分不公平,可以插队(默认)
为什么默认使用非公平锁?是因为如果两个线程,第一个要用3h执行完毕,第二个要用3s,如果使用公平锁会造成严重耗时问题.
非公平锁总体会比公平要好一些,它是根据每个线程对资源抢占能力来分配的,不需要严格的安装锁的请求顺序接入
非公平锁与公平锁的创建
- 非公平锁:ReentrantLock()或ReentrantLock(false)
final ReentrantLock lock = new ReentrantLock();
- 公平锁:ReentrantLock(true)
final ReentrantLock lock = new ReentrantLock(true)
改造代码如下:
public class Demo3 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"A").start();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"B").start();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"C").start();
}
}
/*lock三部曲
1.new ReentrantLock();
2.lock.lock(); //加锁
3.finally=> lock.unlock(); //解锁
*/
class Ticket {
private int number = 50;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); //加锁
try {
//业务代码
if(number>0){
System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"票,剩余"+number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}
}
使用JUC下的lock锁可以保证多线程在并发编码下的操作安全性,
lock锁与synchronized的区别
- Synchronized 内置的Java关键字 ,Lock 是一个java接口
- Synchronized 无法判断获取锁的状态, Lock可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock必须要手动释放锁!如果不释放锁,否则造成死锁
- Synchronized 线程1 ( 获得锁)、线程2(等待,如果线程1阻塞则线程2傻傻的等) ; Lock锁就不一定会等待下去 ;
- Synchronized 可再入锁 ,不可以中断的,非公平; Lock ,可再入锁,可以判断锁,非公平(可以自己设置) ;
- Synchronized 适合锁少量的代码同步问题, Lock适合锁大量的同步代码!
传统生产者消费者问题以及如何解决虚假唤醒问题?
代码如下,有四个线程,两个线程操作加方法,另外两个线程操作减方法,同时两个方法都用synchronized进行修饰,如下:
synchronized(wait,notify)
public class Demo3 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类,把资源类丢入线程
Data data = new Data();
//线程1,执行加方法
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//线程2,执行加方法
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
//线程3,执行减方法
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
//线程4,执行减方法
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data {
private int number = 0;
public synchronized void increment() throws InterruptedException{
if(number!=0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,执行完毕
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException{
if(number==0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,执行完毕
this.notifyAll();
}
}
/*
A=>2
B=>3
C=>2
C=>1
C=>0
其中一段输出结果
*/
发现似乎有两个以上的线程同时针对number进行了加操作,否则四个线程应该结果应该都是1或0,造成这个原因就是虚假唤醒,因为这里使用了if,
如jdk1.8文档里所说,wait方法应该用在while循环里,不能用if,所以上面的方法改为while即可.这也是一个面试点.
这里仍然有个问题,四个线程并没有按照顺序交替执行,四个线程仍然在没有顺序的争抢CPU时间片,所以解决方法见下方JUC写法.
JUC版的生产者与消费者问题
Lock(await,sinal),需要使用到condition实例
现在使用lock锁来改造传统的消费者与生产者写法如下:
改造传统的生产者与消费者,只改上方的Data外部类即可,其余不变:
class Data {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException{
lock.lock();
//将代码块包起来
try {
while (number!=0){
//等待
//this.wait();
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,执行完毕
//this.notifyAll();
condition.signalAll(); //唤醒全部
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public synchronized void decrement() throws InterruptedException{
lock.lock();
try {
while(number==0){
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,执行完毕
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
效果与传统的写法一致,然后任何一种新的技术的出现绝不仅仅只是覆盖原先的写法,contidion可以做到让线程精准通知并按照顺序来执行,所以这解决了上方传统写法遗留的问题
Condition实现精准通知唤醒
紧接上文,实现A执行完调用B,B执行完调用C...
public class Demo3 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类,把资源类丢入线程
Data data = new Data();
//线程1,执行加方法
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printA();
}
},"A").start();
//线程2,执行加方法
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printB();
}
},"B").start();
//线程3,执行减方法
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printC();
}
},"C").start();
}
}
class Data {
private int number = 1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void printA() {
lock.lock();
//将代码块包起来
try {
while (number!=1){
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAA");
number=2;
condition2.signal(); //唤醒指定的B,也不是用了很高级的技术,只是指定唤醒了
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
//将代码块包起来
try {
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBB");
number=3;
condition3.signal(); //唤醒指定的C
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
//将代码块包起来
try {
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>CCC");
number=1;
condition1.signal(); //唤醒指定的A
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5.集合类不安全解决方案
集合不安全指的是集合内部的多个方法不能保证原子性操作.
CopyOnWriteArrayList
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 1; i <=30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
上述代码会报java.util.ConcurrentModificationException异常,这是并发修改异常!
因为并发下ArrayList不安全的,有以下解决方案:
- 使用Vector,即List<String> list = new Vector<>();
- List<String> list = Collections.synchronizedList(new ArrayList<>());这种是用工具类把ArrayList转成synchronized的.
- 使用JUC, List<String> list = new CopyOnWriteArrayList<>(); 写入时复制,写入的时候避免覆盖,造成数据问题,相比上面两种,这个更好.
CopyOnWrite比Vector好在哪里?
如上,Vector的源码时用synchroinzed修饰,只要有这个字段的方法所以效率很低,来看看copyonwrite源码如下:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
我们先来看看 CopyOnWriteArrayList 的 add() 方法,其实也非常简单,就是在访问的时候加锁,拷贝出来一个副本,先操作这个副本,再把现有的数据替换为这个副本。
CopyOnWriteArrayList 的 get(int index) 方法就是普通的无锁访问。
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
1.CopyOnWrite 思想
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种通用优化策略。其核心思想是,如果有多个调用者(Callers)同时访问相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
通俗易懂的讲,写入时复制技术就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。
JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是采用了 COW 思想,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下
2.优点
对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。
3.缺点
数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。
内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap。
CopyOnWriteSet
set与list在写法上与上方类似,如下
public class Demo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 1; i <=30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
这个代码同样也会报并发修改异常问题,即java.util.ConcurrentModificationException,
解决方案如下:
- Set<String> set = Collections.synchronizedSet(new HashSet<>()); 这是用工具类转成synchronized的写法
- Set<String> set = new CopyOnWriteArraySet<>(); 这是JUC的写法,推荐使用
HashSet底层是什么?
底层源码如下:
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
就是new了一个HashMap.
ConcurrentHashMap
ConcurrentHashMap是一个线程安全的.
public class Demo {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
for (int i = 1; i <=30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
这个代码也会提示java.util.ConcurrentModificationException异常,解决如下:
- Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
- Map<String,String> map = new ConcurrentHashMap<>();
关于concurrentHashMap的原理:
分段锁技术:ConcurrentHashMap 相比 HashTable 对锁的处理不同的点在于:前者是分段部分数据锁定
每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,后者是全部锁定。
HashMap 不是线程安全的,因此多线程操作需要注意,通常使用 HashTable 或者 Collections.synchronizedMap() 来返回线程安全的 HashMap ,但是这两种方法都是对所有方法实现同步,即使用synchronized修饰,导致读写性能比较低,而 ConcurrentHashMap 引入“分段锁”的概念,可以理解为把一个大的 Map 差分成小的 HashTable ,根据 key.hashCode() 来决定把 key 放到哪个 HashTable 中去。
就是把 Map 分成了N个 Segment , put 和 get 的时候,都是现根据 key.hashCode() 算出放到哪个Segment中.
ConcurrentHashMap (简称 CHM )是在 Java 1.5作为 Hashtable 的替代选择新引入的,是 concurrent 包的重要成员。在 Java 1.5之前,如果想要实现一个可以在多线程和并发的程序中安全使用的 Map ,只能在 HashTable 和 synchronizedMap 中选择,因为 HashMap 并不是线程安全的。但再引入了 CHM 之后,我们有了更好的选择。 CHM 不但是线程安全的,而且比 HashTable 和 synchronizedMap 的性能要好。相对于 HashTable 和 synchronizedMap 锁住了整个 Map , CHM 只锁住部分 Map 。 CHM 允许并发的读操作,同时通过同步锁在写操作时保持数据完整性。
Segment 是什么呢?
Segment 本身就相当于一个 HashMap 对象。每个Segment都持有自己的锁 ,Segment 是一种可重入锁(继承ReentrantLock)
同 HashMap 一样, Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。
从图中可以看出来ConcurrentHashMap的主干是个Segment数组。
在了解以上的功能 ,之后我们继续看一下ConcurrentHashMap核心构造方法代码。
// 跟HashMap结构有点类似
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即HashEntry数组
}
从以上代码可以看出ConcurrentHashMap有比较重要的三个参数:
- loadFactor 负载因子 0.75
- threshold 初始 容量 16
- concurrencyLevel 实际上是Segment的实际数量 默认16。
ConcurrentHashMap如何发生ReHash?
ConcurrentLevel 一旦设定的话,就不会改变。ConcurrentHashMap当元素个数大于临界值的时候,就会发生扩容。但是ConcurrentHashMap与其他的HashMap不同的是,它不会对Segment 数量增大,只会增加Segment 后面的链表容量的大小。即对每个Segment 的元素进行的ReHash操作。
ConcurrentHashMap 小总结:
- CHM 允许并发的读和线程安全的更新操作
- 在执行写操作时,CHM 只锁住部分的 Map
- 并发的更新是通过内部根据并发级别将 Map 分割成小部分实现的
- 高的并发级别会造成时间和空间的浪费,低的并发级别在写线程多时会引起线程间的竞争
- CHM 的所有操作都是线程安全
- CHM 返回的迭代器是弱一致性, fail-safe 并且不会抛出 ConcurrentModificationException 异常
- CHM 不允许 null 的键值
- 可以使用 CHM 代替 HashTable,但要记住 CHM 不会锁住整个 Map
有待补漏,参深入理解ConcurrentHashMap原理分析以及线程安全性问题_猴凉凉的博客-CSDN博客_concurrenthashmap内存泄漏
6.Callable接口简介
有两种创建线程的方法-一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。
底层是一个泛型函数式接口,V决定方法的返回类型,可以自主设置.
callable和runnable区别:
- 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。请注意,不能使用Callable创建线程,只能使用Runnable创建线程。
- 另一个区别是call()方法可以引发异常,而run()则不能。
-
为实现Callable而必须重写call方法。
要创建线程,需要Runnable。为了获得结果,需要future。
Java库具有具体的FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。
可以通过为其构造函数提供Callable来创建FutureTask,然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。
因此,间接地使用Callable创建线程。
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myc = new MyCallable();
FutureTask futureTask = new FutureTask(myc); //适配类
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start(); //这里开两个线程跑call发现结果只会执行一次,是因为第一次结果会被缓存,效率高
Integer o = (Integer) futureTask.get(); //获取Callable的返回结果,这里的get方法可能会产生阻塞
System.out.println(o);
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() {
System.out.println("你好");
return 123456;
}
}
细节:
- 有缓存
- 结果可能需要等待,会阻塞
7.常用辅助类(必会)
CountDownLatch--减法计数器
public class Demo {
public static void main(String[] args) throws InterruptedException {
//总数是6,必须要执行任务的时候再使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
int temp = i;
new Thread(()->{
System.out.println(temp); //这里会报错,会提示 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量,所以在new Thread外面再写一个
System.out.println(Thread.currentThread().getName()+" Go Out");
countDownLatch.countDown(); //数量-1
},String.valueOf(i)).start();
}
countDownLatch.await(); //等待计数器归零,然后再向下执行
System.out.println("Close Door");
}
}
/*
1 Go Out
2 Go Out
3 Go Out
4 Go Out
5 Go Out
6 Go Out
Close Door
*/
等所有任务执行完毕再关门
原理:
countDownLatch.countDown(); //数量-1
countDownLatch.await(); //等待计数器归零,然后再向下执行
每次有线程调用coundDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续往下执行!
CyclicBarrier--加法计数器
在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。
利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。下图演示了这一过程。
在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,
当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。
public class Demo {
public static void main(String[] args) throws InterruptedException {
//召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
int temp=i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集了"+temp+"个龙珠");
try {
cyclicBarrier.await(); //等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
/*
Thread-0收集了1个龙珠
Thread-1收集了2个龙珠
Thread-2收集了3个龙珠
Thread-3收集了4个龙珠
Thread-5收集了6个龙珠
Thread-4收集了5个龙珠
Thread-6收集了7个龙珠
召唤神龙成功!
*/
Semaphore--信号量
经常用,Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,
例如,实现一个文件允许的并发访问数。
public class Demo {
public static void main(String[] args) {
//线程数量:停车位!限流,一次只能停三辆
Semaphore semaphore = new Semaphore(3); //创建Semaphore信号量,初始化许可大小为3
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire(); 请求获得许可,如果有可获得的许可则继续往下执行,许可数减1。否则进入阻塞状态
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release(); //释放许可,许可数加1
}
},String.valueOf(i)).start();
}
}
}
/*
1抢到车位
3抢到车位
2抢到车位
3离开车位
2离开车位
4抢到车位
5抢到车位
1离开车位
6抢到车位
5离开车位
6离开车位
4离开车位
*/
原理:
- semaphore.acquire(); 获得,假设如果已经满了,等待,等待有资源被释放,-1;
- semaphore.release(); 获得,会将当前的信息量释放+1,然后唤醒等待的线程!
作用:多个共享资源互斥的使用,并发限流,控制最大的线程数!