文章目录

  • ​​1、JUC简介​​
  • ​​2、线程和进程​​
  • ​​3、Lock锁(重点)​​
  • ​​3.1、传统的synchronized锁:队列锁​​
  • ​​3.2、Lock接口​​
  • ​​3.3、synchronized和Lock锁的区别​​
  • ​​4、生产者和消费者问题​​
  • ​​4.1、生产者和消费者:synchronized版​​
  • ​​4.2、JUC版的生产者和消费者​​
  • ​​4.3、Conditional实现精准的通知唤醒​​
  • ​​5、关于锁的八种现象​​
  • ​​6、不安全的集合类​​
  • ​​6.1、List​​
  • ​​6.2、Set​​
  • ​​6.3、Map​​
  • ​​7、Callable​​
  • ​​8、常用的辅助类​​
  • ​​8.1、CountDownLatch​​
  • ​​8.2、CyclicBarrier​​
  • ​​8.3、Semaphore​​



1、JUC简介

指java.util包下的三个工具类

java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks

实现多线程共有三种方式,我们传统的是:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

使用实现Runnable几口的方式,代码没有返回值,效率相比较于实现Callable也比较低



2、线程和进程

一个进程包含多个线程

Java默认有两个线程:main线程、GC线程

Java是不能够开启线程的:他的底层调用的start0()是一个native方法,由底层的C++方法编写

private native void start0();

并发和并行

并发:CPU单核,多线个程操作同一个资源,通过快速交替的方式,达到一种并行的假象

并行:CPU多核,多个线程同时执行

我们可以使用代码的方式查看我们的电脑核数

package pers.mobian.demo01;

public class test01 {
public static void main(String[] args) {
//打印我们电脑的核数
System.out.println(Runtime.getRuntime().availableProcessors());
}
}

并发编程的本质:充分利用CPU的资源

线程的状态

根据源码可得,状态共六种:

public enum State {
//新生态
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待(死等)
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}

wait和sleep的区别

1、来自不同的类

wait:来自Object类

sleep:来自Thread类

2、所的释放不同

wait:会释放锁(清醒着的人好说话)

sleep:不会释放锁(睡着了的人,你说好话求情,他听不见)

3、使用的范围是不同的

wait:只能在同步代码块中使用

sleep:可以在任何地方使用

4、概念上的不同

sleep是Thread类的静态方法。sleep的作用是让线程休眠指定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件时恢复线程执行。wait是Object的方权法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者。

5、是否会抛出异常

很多地方都说wait()不会抛出异常,但是我自己在实验的时候,也是需要抛出异常的,这个点有待斟酌…



3、Lock锁(重点)

3.1、传统的synchronized锁:队列锁

利用synchronized锁资源的代码实现:

package pers.mobian.demo01;

public class test01 {
public static void main(String[] args) throws InterruptedException {
// 并发:多线程操作同一个资源类,
Ticket ticket = new Ticket();
// @FunctionalInterface 函数式接口
new Thread(() -> {
for (int i = 1; i < 20; i++) {
//把资源类丢入线程
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "C").start();
}
}

class Ticket {
//共卖40张票
private int number = 30;

// 卖票的方式
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
}
}
}



3.2、Lock接口

根据JDK帮助文档,我们可得:

Lock锁是一个接口,其所有的实现类为:

ReentrantLock(可重入锁)ReentrantReadWriteLock.ReadLock(可重入读写锁中的读锁)ReentrantReadWriteLock.WriteLock(可重入读写锁中的写锁)

该锁的使用方式

//通过接口实例化Lock锁的具体实现类
Lock l = ...;
//加锁
l.lock();
try {
// access the resource protected by this lock
}
finally {
//解锁
l.unlock();
}

将上面的synchronized锁方法进行改写

public class lock {
public static void main(String[] args) throws InterruptedException {
//System.out.println(Runtime.getRuntime().availableProcessors());
// 并发:多线程操作同一个资源类, 把资源类丢入线程
Ticket2 ticket = new Ticket2();
// @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "C").start();

}
}

class Ticket2 {

private int number = 30;
//1.实例化锁
Lock l = new ReentrantLock();

public void sale() {

//2.加锁
l.lock();
try {
//3.编写业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4.解锁
l.unlock();
}
}
}

补充:公平锁非公平锁

当我们实例化ReentrantLock(可重入锁)时,可以根据我们传递得参数,继而使用公平锁和非公平锁。

public ReentrantLock() {
//无参构造非公平锁,默认使用非公平锁
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
//传递为false时,使用公平锁
sync = fair ? new FairSync() : new NonfairSync();
}

公平锁:大家都十分得公平,遵循先来后到

非公平锁:大家不是同样的公平,可以出现插队的现象



3.3、synchronized和Lock锁的区别

1、synchronized是Java内置的关键字;Lock锁是Java的一个类

2、synchronized无法获取到锁的状态;Lock锁可以判断是否获取到了锁

3、synchronized会自动的释放锁;Lock必须要手动的去释放锁,如果不释放,就会造成死锁

4、synchronized中,线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着;Lock锁出现这种情况,可以使用tryLock()尝试获取锁

5、synchronized是不可中断的、非公平的、可重入锁;Lock锁是非公平的(可以自己设置)、可判断的、可重入锁

6、synchronized适合锁少量的同步代码;Lock锁适合锁大两的同步代码

7、synchronized有代码块锁和方法锁;Lock只有代码块锁

8、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(拥有更多的子类)



4、生产者和消费者问题

4.1、生产者和消费者:synchronized版

代码实现:

package pers.mobian.providerconsumer;

public class SynTest01 {
public static void main(String[] args) {

Num num = new Num();
//编写两个线程类
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}


class Num {

private int number = 0;

//+1
public synchronized void increment() throws InterruptedException {
if (number != 0) {
//业务代码
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}

//-1
public synchronized void decrement() throws InterruptedException {
if (number == 0) {
//业务代码
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
}

在我们编写的一个简单的生产者和消费者的类中,如果我们出现两个以上的线程类的时候,就又会出现资源的不同步问题。

我们查阅JDK的帮助文档可得:

JUC(一)_i++

它推荐我们使用while,于是我们就可以将我们的代码修改为

package pers.mobian.providerconsumer;

public class SynTest01 {
public static void main(String[] args) {

Num num = new Num();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();


}
}


class Num {

private int number = 0;

//+1
public synchronized void increment() throws InterruptedException {
while (number != 0) {
//业务代码
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}

//-1
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
//业务代码
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
}

总结:使用while代替if就可以防止虚假唤醒



4.2、JUC版的生产者和消费者

两种方式的转变:

JUC(一)_System_02

修改synchronized锁的测试代码:

package pers.mobian.providerconsumer;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest01 {
public static void main(String[] args) {

Num2 num = new Num2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}

class Num2 {

private int number = 0;

//实例化锁
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();


//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
//业务代码
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}

}

//-1
public 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();
}
}
}

对比于传统的synchronized版本的形式,JUC版本使用起来更加的复杂,不过随之而来的是更多的优势!!!



4.3、Conditional实现精准的通知唤醒

参考JDK帮助文档可得:

JUC(一)_i++_03

对应的编写我们的业务代码:

package pers.mobian.providerconsumer;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest02 {
public static void main(String[] args) {

Num3 num = new Num3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();

new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}


class Num3 {

private int number = 2;
private Lock lock = new ReentrantLock();
//用这三个conditional对象,来标记不同
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();


//具体方法的实现步骤:业务判断 --> 执行 --> 通知
//A
public void printA() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 1) {
condition1.await();
}
number = 2;
System.out.println(Thread.currentThread().getName() + "=>AAAAAAAAAAA");
//通知其他线程
condition2.signal();

} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}

}

//B
public void printB() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 2) {
condition2.await();
}
number = 3;
System.out.println(Thread.currentThread().getName() + "=>BBBBBBBBBBB");
//通知其他线程
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}

}

//C
public synchronized void printC() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 3) {
condition3.await();
}
number = 1;
System.out.println(Thread.currentThread().getName() + "=>CCCCCCCCCC");
//通知其他线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}



5、关于锁的八种现象

我们传统的线程模板

public class Test01 {
public static void main(String[] args) {
Phone phone = new Phone();

new Thread(() -> {
for (int i = 0; i < 45; i++) {
phone.call();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 45; i++) {
phone.send();
}
}, "B").start();
}
}

class Phone {
public void send() {
System.out.println("发短信");
}
public void call() {
System.out.println("打电话");
}
}

这种访问方式,是随机访问的

下文会使用sleep方法,主要的目的是为了方法实际的运行顺序

现象一:我们对方法使用的synchronized关键字,先执行打电话

现象二:让我们的线程sleep4秒钟,结果依然还是先执行打电话

package pers.mobian.lock8;

import java.util.concurrent.TimeUnit;

public class Test01 {
public static void main(String[] args) {
Phone phone = new Phone();

new Thread(() -> {
phone.call();
}, "A").start();
new Thread(() -> {
phone.send();
}, "B").start();
}
}

class Phone {
public synchronized void send() {
System.out.println("发短信");
}

public synchronized void call() {
//现象二时添加,让线程先睡4秒种
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}

结论:synchronized锁的是我们方法的调用者,并且我们开启的两个线程方法使用的是同一把锁,那么就会出现谁先拿到,谁执行的现象。以至于我们让call方法sleep了四秒,依然是call方法先执行。



现象三:在之前代码的基础上,新添加一个普通的方法,此时先执行hello方法

现象四:我们新实例化一个Phone对象,使用不同得对象去调用方法,此时先执行发短信

package pers.mobian.lock8;

import java.util.concurrent.TimeUnit;

public class Test02 {
public static void main(String[] args) {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();

new Thread(() -> {
phone1.call();
}, "A").start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(() -> {
// phone1.hello();
phone2.send();
}, "B").start();
}
}

class Phone2 {
public synchronized void send() {
System.out.println("发短信");
}

public synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}

总结:现象三中,由于这是一个普通的方法,所以就没有锁的效果,又由于时间的延迟,导致先打印hello。现象四中,由于我们使用了不同的对象,以至于synchronized锁的对象不是同一个,所以先打印发短信。



现象五:在之前代码的基础上,我们在方法前添加static关键字,此时先执行打电话

现象六:我们再添加一个对象,使用不同的对象进行方法的打印,此时依然是先执行打电话

package pers.mobian.lock8;

import java.util.concurrent.TimeUnit;

public class Test03 {
public static void main(String[] args) {
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();

new Thread(() -> {
phone1.call();
}, "A").start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(() -> {
//phone1.send();
phone2.send();
}, "B").start();
}
}

class Phone3 {
public static synchronized void send() {
System.out.println("发短信");
}

public static synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}

总结:当我们添加了static关键字以后,此时我们先打印打电话,不仅是因为我们拿到的是同一个对象的锁,还因为我们的锁是直接锁的该类的Class模板。当我们再新添加一个对象的时,由于我们使用了static修饰,直接锁到了模板上,所以依然是先执行打电话。



现象七:当我们去掉一个锁方法的static关键字以后,此时先打印发短信

现象八:我们再新建一个对象后,使用不同的对象去调用,此时依然是先打印发短信

package pers.mobian.lock8;

import java.util.concurrent.TimeUnit;

public class Test04 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();

new Thread(() -> {
phone1.call();
}, "A").start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(() -> {
// phone1.send();
phone2.send();
}, "B").start();
}
}

class Phone4 {
public synchronized void send() {
System.out.println("发短信");
}

public static synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}

总结:现象七中,我们只添加一个static关键字,即模板Class中只有一部分被锁,则先打印出发短信。现象八中,我们又新建了一个对象,同理,由于我们锁的东西不一样(打电话锁模板,发短信锁对象),所以不冲突,那么就先执行发短信。

只有当锁的对象或者模板是同一个的时候,才能够借助调用的顺序来执行。



6、不安全的集合类

6.1、List

List是不安全的集合类

如何将它变得安全?有如下几种方法:

  1. 将ArrayList集合变成Vector集合
  2. 使用Collections.synchronizedList( new ArrayList<>() )的方式创建集合
  3. 使用CopyOnWriteArrayList<>()

第三种方式为写入时复制。它相比较于Vector,效率更加高效,因为Vector底层使用的是synchronized锁,而在JDK1.8中CopyOnWriyteArrayList底层是Lock锁(不过JDK11中貌似使用的又是synchronized锁)



6.2、Set

Set也是不安全的集合类

同样,将不安全的集合变成安全集合的方法:

  1. 使用Collections.synchronizedList( new HashSet<>() )的方式创建集合
  2. 使用CopyOnWriteArraySet<>()

补充:Set的底层是什么?(后面我也会写一些关于这三个集合的底层源码)

//默认的空参初始化方法
public HashSet() {
map = new HashMap<>();
}

//使用HashSet的add方法,依然是调用HashMap的底层put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

即HashSet的底层就是HashMap



6.3、Map

Map也是不安全的集合类

将不安全的集合变成安全集合的方法:

  1. 使用Collections.synchronizedMap( new HashMap() )的方式创建集合
  2. 使用ConcurrentHashMap<>()



7、Callable

由JDK帮助文档可得

JUC(一)_i++_04

相比较于之前的实现Runnable方法,实现Callable有哪些好处?

  1. Callable含有返回值
  2. Callable可以抛出异常
  3. Runnable使用的是run方法,而Callable则是call方法

由底层的源码可知,我们是没有办法直接调用这个实例的,我们需要在两者之间添加一层关系。

我们在Runnable的实现类中找到了一个FutureTask类,并且发现FutereTask是可以接受Callable对象的。

即,我们只需要创建一个FutureTask的实例对象,就可以用来接收实现Callable的线程类的实例对象,最后就只需要将FutureTask的实例放入线程中开启就可以了。

JUC(一)_i++_05

JUC(一)_i++_06

具体代码如下:

package pers.mobian.callable;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {

Mythread mythread = new Mythread();
FutureTask<String> stringFutureTask = new FutureTask<>(mythread);//适配类
new Thread(stringFutureTask,"A").start();
//如果再添加一条语句,结果还是打印一条结果,因为有缓存的存在,用于提高效率
new Thread(stringFutureTask,"B").start();

//这个get方法,可能会产生阻塞,应该放在代码的最后,或者使用异步通信来处理
System.out.println(stringFutureTask.get());

}
}

//传递的参数类型,就是我们需要返回时的参数类型
class Mythread implements Callable<String> {
public String call() {
System.out.println("成功调用call方法");
return "OK";
}
}



8、常用的辅助类

8.1、CountDownLatch

简单理解为减法计数器

测试代码实例:

package pers.mobian.add;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest01 {
public static void main(String[] args) throws InterruptedException {
//设置需要执行的任务的总数量
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown();//数量-1
}, String.valueOf(i)).start();
}
//等待计数器归零以后,再继续向下执行
countDownLatch.await();
System.out.println("Close door");
}
}

总结:countDownLatch.countDown()每执行一次,数量就会 -1 ,当数量变成0以后,countDownLatch.await()就会被唤醒,继而执行后面的方法



8.2、CyclicBarrier

减法器

package pers.mobian.add;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest01 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("上了五天课,可以回家休息了");
});

for (int i = 1; i <= 5; i++) {
//使用lambda表达式,相当于重新编写了一个类,是没有办法直接使用到上级的变量的
//此时需要我们自己定义一个临时变量来接受
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();
}
}
}

总结:当i变成5以后,就会执行cyclicBarrier对象中的接口方法。值得注意的是,如果我们设置的parties比较小的话,就会提前执行该方法



8.3、Semaphore

计数器

package pers.mobian.add;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreTest01 {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);

for (int i = 0; i < 7; i++) {
new Thread(() -> {
try {
//获取
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "占位三秒钟");
TimeUnit.SECONDS.sleep(3);
System.out.println("三秒钟到" + Thread.currentThread().getName() + "离开位置");

} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}

总结:semaphore.acquire()假如我们的信号量已经满了,就会等待,直到semaphore释放;semaphore.release()会将当前的信号量释放,然后唤醒等待的线程。

作用:多个共享资源互斥的使用、并发限流、控制最大的线程数。