文章目录
一、多线程第一天
1、今日内容
-
相关的概念(进程,线程,并发,并行)
-
创建线程的方式
-
控制线程的方法
-
线程安全问题
-
死锁
-
生产者消费者模型
2、并发和并行
并行:在同一时刻,有多个指令在多个CPU上**同时**执行。
并发:在同一时刻,有多个指令在单个CPU上**交替**执行。
比如吃馒头:① 现在存在2个馒头,如果有两个人同时进行食用,这个现象就是并行;② 如果一个人需要同时食用这2个馒头(两个馒头轮流食用),这个现象就是并发;
3、进程和线程
进程:正在运行的应用程序。进程是操作系统的最小调度单元。
线程:一个进程中可以包含多个任务,每一个任务可以看做成一个线程。线程是依赖于进程的,一个进程至少存在一个线程。
4、多线程的实现方式
三种:
-
继承Thread类
-
实现Runnable接口
-
实现Callable和FutureTask创建线程
(1)方式一 :多线程的第一种实现方式
① 定义一个类,让其继承Thread类
② 重写Thread类中的run方法(该run方法的方法体就代表了线程需要完成的任务。因此把run方法称之为线程执行体)
③ 创建Thread子类的实例,及创建了线程对象
④ 调用线程对象的start()方法来启动该线程。
具体实现代码(线程类MyThread)
public class MyThread extends Thread{
@Override
public void run() {
//代码就是线程在开启之后执行的代码
for (int i = 0; i <5; i++) {
System.out.println(Thread.currentThread().getName()+"线程执行了···");
}
}
public static void main(String[] args) {
//创建一个线程对象
MyThread t1 = new MyThread();
//开启一条线程
t1.start();
//创建一个线程对象
MyThread t2 = new MyThread();
//开启第二条线程
t2.start();
}
}
上述代码的执行过程:
当我们执行main方法时候,此时jvm会开启一个线程去执行,一个线程可以看做是程序的一条执行路径;当我们在main方法中又开启了两个线程,并且将其启动起来,那么此时在程序中会存在3条执行路径。他们之间彼此都是独立的,进行同时执行。
多线程程序的小问题:
其一,调用start()方法表示开启一个线程。
其二,start()方法只能调用一次,如果多次调用就会产生异常。IllegalThreadStateException
(2)方式二:多线程的第二种实现方式
实现多线程的第二种方式就是借助于Runnable接口进行实现
具体的步骤如下:
① 定义Runnable接口的实现类,并重写该接口的run()方法。
② 创建Runnable实现类的实例。
③ 创建Thread对象,然后将第二步创建的实例作为参数传递过来。
④ 调用start方法启动线程。
定义Runnable接口实现类
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+"线程执行了···");
}
}
public static void main(String[] args) {
//定义runnable实现类的实例
MyRunnable mr = new MyRunnable();
//创建Thread对象,然后将创建实例作为参数传递过来
Thread t = new Thread(mr);
// 调用start方法启动线程
t.start();
}
}
(3)方式三:多线程的第三种实现方式
FutureTask的继承体系图如下所示:
实现步骤如下
① 创建一个类实现Callable接口,并重写call方法。
② 创建Callable实现类的对象。
③ 创建FutureTask对象,把第二步创建的对象作为参数进行传递。
④ 创建Thread对象,把第三步创建的FutureTask对象作为参数进行传递。
⑤ 调用start方法启动线程。
⑥ 调用FutureTask对象的get()方法获取线程的执行结果。
创建一个类实现Callable接口
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+"线程执行了···");
}
//返回值就表示线程运行完毕之后的结果
return "答应";
}
public static void main(String[] args) throws Exception {
//线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable();
//可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
FutureTask ft = new FutureTask(mc);
//创建线程对象
Thread t = new Thread(ft);
//开启线程
t.start();
//获取线程执行结果
String str = (String) ft.get();
System.out.println("str:"+str);
}
}
(4)三种创建线程方法的对比
① Callable最大的特点:call方法存在返回值,线程执行完毕以后可以给主程序返回一个结果,其他两种不行。
② 继承Thread和实现接口的区别:
- 继承Thread类的这种方式可以直接使用Thread类中所定义的方法,但是扩展性较差。
- 实现接口的这种方式是不可以使用Thread类中所定义的方法,但是扩展性较强。
5、Thread类中的成员方法
(1)获取和设置线程的名称
public Thread(String name) // 使用构造方法设置线程名称
public final synchronized void setName(String name) // 调用成员方法设置线程名称
public final String getName() // 获取线程的名称
public static native Thread currentThread(); // 获取当前正在执行这个run方法的线程对象
代码演示
public class Thread01 extends Thread{
@Override
public void run() {
System.out.println("第三次:"+Thread.currentThread().getName());//第三次:线程一
}
public static void main(String[] args) {
Thread01 t = new Thread01();
System.out.println("第一次:"+t.getName());//第一次:Thread-0
t.setName("线程一");
System.out.println("第二次:"+t.getName());//第二次:线程一
t.start();
}
}
如何获取main线程的线程名称?
思路:获取当前正在执行这个 main方法的线程对象,然后调用getName()方法获取线程名称。
public static void main(String[] args) {
//获取main线程的线程名
System.out.println("main:"+Thread.currentThread().getName());//main:main
}
(2)线程休眠的方法(重点掌握)
public static native void sleep(long millis) throws InterruptedException;
代码演示
public class Thread02_sleep extends Thread{
public Thread02_sleep() {
}
public Thread02_sleep(String name) {
super(name);
}
@Override
public void run() {
//代码就是线程开启之后执行的代码
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->线程执行了");
try {
//Thread.sleep(1000);// 让线程进行休眠1秒
TimeUnit.SECONDS.sleep(1);// 让线程进行休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread02_sleep t = new Thread02_sleep();
t.start();
}
}
6、线程的优先级(了解)
线程的调度模型:
(1)分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
(2)抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
java使用的是抢占式调度模型。
默认情况下线程的优先级是5,最大的优先级是10,最小的优先级是1
和线程优先级相关的方法:
public final int getPriority() // 获取线程的优先级
public final void setPriority(int newPriority) // 设置线程的优先级
我们通过设置线程的优先级来"建议"jvm优先执行某一个线程,但是jvm不一定采纳。因此我们不要企图使用线程的优先级来绑定某一些业务操作。
7、守护线程
后台线程的简介:有一种线程是在后台运行的,它的任务就是为其他线程提供服务,这种线程被称之为 “后台线程” ,又称之为守护线程。JVM的垃圾回收线程就是典型的后台线程。
后台线程的特征:如果所有的前台线程都死亡,后台线程会自动死亡。当整个JVM中只存在后台线程,那么程序就没有运行的必要了,整个JVM就退出了。
举例:舞台表演;在舞台中表演的一个个演员我们就可以将其看做成一个个的前台线程,而在后台默默工作的一个个人员,我们可以将其看做成一个个的后台线程。
//把一个线程设置为后台线程:
public final void setDaemon(boolean on)
实例代码
//女神线程
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
//备胎线程
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();//10次
MyThread2 t2 = new MyThread2();//100次
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设置为守护线程
//当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
//注意:当普通线程执行完毕,守护线程不会立即停止,会挣扎一会。
t2.setDaemon(true);
t1.start();
t2.start();
}
}
//结果:线程1执行10次,线程2执行不到100次。
8、线程安全问题
线程安全问题的产生原因:当多个线程对共享数据进行操作的时候有可能 会存在线程安全问题。
解决线程安全问题:
思路:当某一个线程在对共享变量进行操作的时候,其他线程处于等待状态即可。 也就是说:一次只允许一个线程对共享变量进行操作。具体的解决方案:就是使用锁机制进行解决
(1)基于同步代码块的锁机制来解决线程安全问题
同步代码块的格式:
synchronized (对象){ // 多个线程必须使用同一把锁.
...
}
代码优化:
//售票问题:必须使用Runnable
public class Thread03Ticket implements Runnable{
private int ticket=1000;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if (ticket<1){
break;
}else{
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"票数剩余:"+ticket);
}
}
}
}
public static void main(String[] args) {
Thread03Ticket thread03Ticket = new Thread03Ticket();
new Thread(thread03Ticket).start();
new Thread(thread03Ticket).start();
}
}
(2)同步方法和静态同步方法的锁对象
**synchronized:**关键字可以使用在普通方法上,也可以使用在静态方法上,或者静态代码块。
同步方法:被synchronized所修饰的普通方法被称之同步方法。(锁对象是this)
静态同步方法: 被synchronized所修饰的静态方法被称之为静态同步方法。(锁对象是当前类字节码文件对象:类名.class)
具体的使用格式如下所示:
修饰符 synchronized 返回值类型 方法名(参数列表) { ... }
(3)StringBulider和StringBuffer的区别
① StringBulider是线程不安全的,单线程环境可以大胆的去使用
② StringBuffer是线程安全的,多线程可以大胆的去使用
(4)HashMap和Hashtable的区别
HashMap是线程不安全的,Hashtable是线程安全的。
如果是单线程环境我们可以选择HashMap,如果是多线程环境,我们可以选择Hashtable,但是不建议。因为Hashtable使用的同步方法的方式去实现线程的安全性,效率较低。所以在多线程环境下要保证线程的安全性,我们可以使用ConcurrentHashMap。
9、Lock锁的使用
为什么要存在Lock锁,优点?通过Lock我们可以明确的知道在什么地方添加上了锁,在什么地方释放了锁。
Jdk1.5以后提供的锁机制。
public interface Lock {
void lock(); // 加锁
void unlock(); // 解锁或者释放锁
}
Lock是一个接口不能直接对其进行new,要用只能使用它的子类,常见的子类就是:ReentrantLock
public class Thread04TicketLock implements Runnable{
private int ticket=500;
//private Object obj = new Object();
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
//synchronized(obj){
try{
lock.lock(); // 加锁
if (ticket==0){
Thread.sleep(1);
break;
}else{
Thread.sleep(1);
ticket--;
System.out.println(Thread.currentThread().getName()+"票数剩余:"+ticket);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock(); // 释放锁
}
//}
}
}
public static void main(String[] args) {
Thread04TicketLock thread03Ticket = new Thread04TicketLock();
new Thread(thread03Ticket).start();
new Thread(thread03Ticket).start();
}
}
注意事项:要保证线程的安全性,多个线程需要使用同一个ReentrantLock对象。
10、死锁
死锁现象:
多个线程在抢占资源的时候出现了相互等待的状态。
死锁诊断:
当程序出现了死锁现象,我们应该如何进行诊断呢?使用jdk自带的工具: jstack
对上面的程序使用jstack进行死锁诊断。
举例死锁
public class Thread05Ticket_deadlock extends Thread{
public static Object t1 = new Object();
public static Object t2 = new Object();
public static void main(String[] args){
new Thread(){
@Override
public void run(){
synchronized (t1){
System.out.println("Thread1 get t1");
try {
Thread.sleep(100);
}catch (Exception e){}
synchronized (t2){
System.out.println("Thread2 get t2");
}
}
}
}.start();
new Thread(){
@Override
public void run(){
synchronized (t2){
System.out.println("Thread2 get t2");
try {
Thread.sleep(100);
}catch (Exception e){}
synchronized (t1){
System.out.println("Thread2 get t1");
}
}
}
}.start();
}
}
死锁诊断
I:\heima_idea1\project01\day16>jps //查看当前正在运行的进程
7680 KotlinCompileDaemon
13252 Launcher
7348 Thread05Ticket_deadlock
15708
3740 Jps
I:\heima_idea1\project01\day16>jstack -l 7348 //查看死锁状态
2020-11-16 23:22:40
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.4+10-LTS mixed mode):
Threads class SMR info:
····
····
===================================================
"Thread-0":
at com.itheima.Thread01.Thread05Ticket_deadlock$1.run(Thread05Ticket_deadlock.java:20)
- waiting to lock <0x0000000089397850> (a java.lang.Object)
- locked <0x0000000089397840> (a java.lang.Object)
"Thread-1":
at com.itheima.Thread01.Thread05Ticket_deadlock$2.run(Thread05Ticket_deadlock.java:37)
- waiting to lock <0x0000000089397840> (a java.lang.Object)
- locked <0x0000000089397850> (a java.lang.Object)
Found 1 deadlock. //发现一个死锁
11、线程通讯问题
线程通讯问题: 就是多个线程进行协调工作
**常见的使用场景:**就是生产者消费者模型
生产者和消费者的代码实现
共享数据
public class Desk {
//定义一个标记
//true 就表示桌子上有汉堡包的,此时允许吃货执行
//false 就表示桌子上没有汉堡包的,此时允许厨师执行
public static boolean flag = false;
//汉堡包的总数量
public static int count = 10;
//锁对象
public static final Object lock = new Object();
}
消费者
public class Foodie extends Thread {
@Override
public void run() {
// 1,判断桌子上是否有汉堡包。
// 2,如果没有就等待。
// 3,如果有就开吃
// 4,吃完之后,桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
// 汉堡包的总数量减一
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(Desk.flag){ // true
//有
System.out.println("吃货在吃汉堡包");
Desk.flag = false;
Desk.lock.notifyAll();
Desk.count--;
}else{
//没有就等待
//使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
生产者代码
public class Cooker extends Thread {
// 生产者步骤:
// 1,判断桌子上是否有汉堡包
// 如果有就等待,如果没有才生产。
// 2,把汉堡包放在桌子上。
// 3,叫醒等待的消费者开吃。
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(!Desk.flag){
//生产
System.out.println("厨师正在生产汉堡包");
Desk.flag = true;
Desk.lock.notifyAll();
}else{
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
12、阻塞队列(重要)
阻塞队列的继承体系
两者的特点
ArrayBlockingQueue: 底层是数组,有界。
LinkedBlockingQueue: 底层是链表,无界。但不是真正的无界,最大为int的最大值。
BlockingQueue接口中和阻塞相关的方法:
// 放数据,当队列满了以后,线程处于阻塞状态
void put(E e) throws InterruptedException;
// 取数据,当队列中没有数据,线程处于阻止状态
E take() throws InterruptedException;
ArrayBlockingQueue的使用
import java.util.concurrent.ArrayBlockingQueue;
public class demo01 {
public static void main(String[] args) throws InterruptedException {
// 创建阻塞队列的对象,容量为 2
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
// 存储元素
queue.put("汉堡包");
queue.put("苹果");
// 取元素
System.out.println(queue.take());
System.out.println(queue.take());
//取不到会阻塞,存大于取不会堵塞
System.out.println(queue.take());
System.out.println("程序结束了");
}
}
LinkedBlockingDeque的使用
import java.util.concurrent.LinkedBlockingDeque;
public class demo02LinkedBlockingQueue {
public static void main(String[] args) throws InterruptedException {
// 创建阻塞队列的对象
LinkedBlockingDeque<String> queue = new LinkedBlockingDeque<>();
// 存储元素
queue.put("汉堡包");
queue.put("苹果");
queue.put("香蕉");
// 取元素
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
//取不到也会阻塞,存大于取不会堵塞
System.out.println(queue.take());
System.out.println("程序结束了");
}
}
13、通过阻塞队列实现生产者消费者模型
思路:创建两个线程类,分别是生产者线程和消费者线程;生产者线程负责从向阻塞队列中放数据,消费者线程复制从阻塞队列中取数据。并且生产者线程和消费者线程需要使用同一个阻塞队列。
核心代码:
ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);
// 保障多个线程所使用的队列是同一个阻塞队列
Foodie f = new Foodie(bd);
Cooker c = new Cooker(bd);
f.start();
c.start();
二、多线程第二天
1、今日内容
-
线程的状态(面试)
-
线程池(重点)
-
volatile(重点)
-
并发工具类的使用
2、线程的状态
Java中线程的状态:
public class Thread {
public enum State {
NEW , //新建
RUNNABLE , //可运行状态
BLOCKED , //阻塞状态
WAITING , //无限等待状态
TIMED_WAITING , //计时等待
TERMINATED;//终止
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
线程状态的转换关系:
3、线程池
线程池
提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。
线程池存在的意义:
系统创建一个线程的成本是比较高
的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗
有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。
线程池原理
线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。
4、自定义线程池代码实现(了解)?
线程池的思路和生产者消费者模型是很接近的。
1、准备一个任务容器
2、一次性启动多个(2个)消费者线程
3、开始任务容器是空的,所以线程都在wait
4、直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒
5、这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来
在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程。
实现思路:
· 创建一个线程池类(ThreadPool)
· 在该类中定义两个成员变量poolSize(线程池初始化线程的个数) , BlockingQueue(任务容器)
· 通过构造方法来创建两个线程对象(消费者线程),并且启动
· 使用内部类的方式去定义一个线程类(TaskThread),可以提供一个构造方法用来初始化线程名称
· 两个消费者线程需要不断的从任务容器中获取任务,如果没有任务,则线程处于阻塞状态。
· 提供一个方法(submit)向任务容器中添加任务
· 定义测试类进行测试
线程池类
public class ThreadPool {
// 初始化线程个数
private static final int DEFAULT_POOL_SIZE = 2 ;
// 在该类中定义两个成员变量poolSize(线程池初始化线程的个数) , BlockingQueue<Runnable>(任务容器)
private int poolSize = DEFAULT_POOL_SIZE ;
private BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<Runnable>() ;
// 无参构造方法
public ThreadPool(){
this.initThread();
}
// 有参构造方法,通过构造方法来创建两个线程对象(消费者线程),并且启动
public ThreadPool(int poolSize) {
if(poolSize > 0) {
this.poolSize = poolSize ;
}
this.initThread();
}
// 初始化线程方法
public void initThread(){
for(int x = 0 ; x < poolSize ; x++) {
new TaskThread("线程--->" + x).start();
}
}
// 提供一个方法(submit)向任务容器中添加任务
public void submit(Runnable runnable) {
try {
blockingQueue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 使用内部类的方式去定义一个线程类
public class TaskThread extends Thread {
// 提供一个构造方法,用来初始化线程名称
public TaskThread(String name) {
super(name);
}
@Override
public void run() {
while(true) {
try {
// 两个消费者线程需要不断的从任务容器中获取任务,如果没有任务,则线程处于阻塞状态。
Runnable task = blockingQueue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
测试类
public class ThreadPoolDemo01 {
public static void main(String[] args) {
// 创建线程池对象,无参构造方法创建
// ThreadPool threadPool = new ThreadPool();
ThreadPool threadPool = new ThreadPool(5);
// 提交任务
for(int x = 0 ; x < 10 ; x++) {
threadPool.submit( () -> {
System.out.println(Thread.currentThread().getName() + "---->>>处理了任务");
});
}
}
}
使用无参构造方法创建线程池对象,控制台输出结果
线程--->0---->>>处理了任务
线程--->1---->>>处理了任务
线程--->0---->>>处理了任务
线程--->1---->>>处理了任务
线程--->0---->>>处理了任务
线程--->1---->>>处理了任务
线程--->0---->>>处理了任务
线程--->1---->>>处理了任务
线程--->0---->>>处理了任务
线程--->1---->>>处理了任务
通过控制台的输出,我们可以看到在线程池中存在两个线程,通过这2个线程处理了10个任务。
使用有参构造方法创建线程池对象,传递的参数是5,控制台输出结果
线程--->3---->>>处理了任务
线程--->4---->>>处理了任务
线程--->2---->>>处理了任务
线程--->0---->>>处理了任务
线程--->2---->>>处理了任务
线程--->4---->>>处理了任务
线程--->3---->>>处理了任务
线程--->1---->>>处理了任务
线程--->2---->>>处理了任务
线程--->0---->>>处理了任务
通过控制台的输出,我们可以看到在线程池中存在两个线程,通过这5个线程处理了10个任务。
5、Java中线程池
(1)通过Executors中静态方法进行获取:
//创建一个可缓存线程池,可灵活的去创建线程,并且灵活的回收线程,若无可回收,则新建线程。
ExecutorService newCachedThreadPool():
//初始化一个具有固定数量线程的线程池
ExecutorService newFixedThreadPool(int nThreads):
//初始化一个具有一个线程的线程池
ExecutorService newSingleThreadExecutor():
//初始化一个具有一个线程的线程池,支持定时及周期性任务执行
ScheduledExecutorService newSingleThreadScheduledExecutor():
ExecutorService就是线程池。
① newCachedThreadPool方法获取的线程池
会根据提交任务的数量来分配线程的个数,如果有空间的线程,直接使用空闲线程去执行对应的任务即可。
public class Demo01Executor_newCachedThreadPool {
public static void main(String[] args) {
//创建一个可缓存线程池,可灵活的去创建线程,并且灵活的回收线程,若无可回收,则新建线程。
ExecutorService es = Executors.newCachedThreadPool();
//提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了···");
}
});
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了···");
}
});
// 关闭线程池,在进行项目开发的时候线程池是不会关闭的
es.shutdown();
}
}
//执行结果
pool-1-thread-1线程执行了···
pool-1-thread-2线程执行了···
② newFixedThreadPool用于获取具有指定个数线程的线程池对象
public class Demo02Executor_newFixedThreadPool {
public static void main(String[] args) {
//参数不是初始值而是最大值
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
System.out.println(pool.getPoolSize());//0
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
System.out.println(pool.getPoolSize());//2
executorService.shutdown();
}
}
//执行结果
0
2
pool-1-thread-2在执行了
pool-1-thread-1在执行了
③ newSingleThreadExecutor获取具有一个线程的线程池对象
public class Demo03Executor_newSingleThreadExecutor {
public static void main(String[] args) {
//初始化一个具有一个线程的线程池
ExecutorService es = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100; i++) {
es.submit(()->{
//pool-1-thread-1 100次
System.out.println(Thread.currentThread().getName());
});
}
es.shutdown();
}
}
④ newSingleThreadScheduledExecutor方法的获取的线程池可以让我们的任务定时执行或者周期性的执行
public class Demo04Executor_newSingleThreadScheduledExecutor {
public static void main(String[] args) {
//一、定时器
//1.初始化一个具有一个线程的线程池,支持定时及周期性任务执行
ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor();
//2.提交任务,10s以后开始执行该任务
es.schedule(()->{
System.out.println("我执行了···");
},3,TimeUnit.SECONDS);// 提交任务,10s以后开始执行该任务
//3.关闭线程池
es.shutdown();
//二、周期性的执行
//1.获取线程池对象
ScheduledExecutorService es2 = Executors.newSingleThreadScheduledExecutor();
//2.提交任务,10s以后开始第一次执行该任务,然后每隔1秒执行一次
es2.scheduleAtFixedRate( () -> {
System.out.println("间隔执行了····");
},10,1,TimeUnit.SECONDS);
//3.关闭线程池
//es.shutdown();
}
public ScheduledFuture<?> schedule(
Runnable command, //线程
long delay, //多长时间以后执行该任务
TimeUnit unit){ //单位
return null;
}
public ScheduledFuture<?> scheduleAtFixedRate(
Runnable command, //线程
long initialDelay, //开始第一次执行该任务的时间
long period, //每次间隔时间
TimeUnit unit){ //单位
return null;
}
}
(2)通过ThreadPoolExecutor来创建线程池(重点)
JDK底层所提供的线程池本质上就是ThreadPoolExecutor
通过Executors这个类中的静态方法获取的线程池也是ThreadPoolExecutor,Executors是对ThreadPoolExecutor做了一层封装。封装的好处,可以大大的简化代码的书写,但是灵活性较差。因此在后期的项目开发中很少去使用Executors中的方法来获取线程池。
一般情况下都是直接使用ThreadPoolExecutor创建一个线程池。
① 构造方法及其参数:
public ThreadPoolExecutor(
int corePoolSize, //核心线程的最大值,不能小于0
int maximumPoolSize, //最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
long keepAliveTime, //空闲线程最大存活时间,不能小于0
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //任务队列,不能为null
ThreadFactory threadFactory, //创建线程工厂,不能为null
RejectedExecutionHandler handler) //任务的拒绝策略,不能为null
ThreadPoolExecutor这个线程池进行线程销毁的时候只会销毁临时线程,不会销毁核心线程。
② 示例代码
// 创建一个线程池对象
public class Demo05Executor_ThreadPoolExecutor {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,5,5,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//提交任务
pool.submit(()->{
System.out.println(Thread.currentThread().getName());
});
pool.submit(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
④ 线程池的工作原理
当我们通过submit方法向线程池中提交任务的时候,具体的工作流程如下:
1、客户端每次提交一个任务,线程池就会在核心线程池中创建一个工作线程来执行这个任务。当核心线程池中的线程已满时,则进入下一步操作。
2、把任务试图存储到工作队列中。等待核心线程池中的空闲线程执行。如果工作队列满了,则进入下个流程。
3、线程池会再次在非核心线程池区域去创建新工作线程来执行任务,直到当前线程池总线程数量超过最大线程数时,就是按照指定的任务处理策略处理多余的任务。
⑤ 任务的处理策略
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
6、volatile关键字
多个线程对共享数据操作的时候可能会存在的问题:某一个线程对这个数据进行了修改,而其他线程感知不到。
(1)问题代码
public class Main {
public static void main(String[] args) {
Boy boy = new Boy();
Girl girl = new Girl();
Thread t1 = new Thread(boy);
Thread t2 = new Thread(girl);
t1.start();
t2.start();
}
}
public class Money {
public static int money = 100000;
}
public class Girl implements Runnable{
@Override
public void run() {
while (Money.money==100000){
}
System.out.println("取钱:被女孩发现了");
}
}
public class Boy implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
Money.money=90000;
System.out.println("男孩:我取了一万块钱。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序问题 : 女孩虽然知道结婚基金是十万,但是当基金的余额发生变化的时候,女孩无法知道最新的余额。当A线程修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题 。
(2)JMM:Java内存模型
概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model):描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存
和从内存中读取变量
这样的底层细节。
特点:
① 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
② 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
③内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
**问题解释:**了解了一下JMM,那么接下来我们就来分析一下上述程序产生问题的原因。
产生问题的流程分析:
① boy从主内存读取到数据放入其对应的工作内存
② 将money的值更改为90000,但是这个时候money的值还没有回写主内存
③ 此时girl读取到了money的值并将其放入到自己的工作内存中,此时money的值为100000
④ boy将money的值写回到主内存,但是girl里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主内存中的值,所以while(true)读取到的值一直是100000。(如果有一个时刻girl从主内存中读取到了money的最新值,就可以跳出循环,girl何时从主内存中读取最新的值,我们无法控制)
(3)问题的解决
方案1:使用volatile 修饰共享变量
作用:使用volatile 修饰共享变量,修饰完毕以后,那么线程在进行工作的时候,首先清空自己工作内存中的数据,然后从主内存中获取最新的数据。这样就保证了多个线程对共享变量的可见性。
Volatile关键字:强制线程每次在使用的时候,都会看一下共享区域最新的值。
代码实现 : 使用volatile关键字解决
public class Money {
public static volatile int money = 100000;
}
**方案2:**使用同步代码块
//修改Money类
public class Money {
public static int money = 100000;
public static Object object = new Object();
}
//修改Girl类
public class Girl implements Runnable{
@Override
public void run() {
while (true) {
synchronized (Money.object){ //或者使用this
if (Money.money==100000){
System.out.println("被女孩发现了···");
break;
}
}
}
}
}
对上述代码加锁完毕以后,某一个线程支持该程序的过程如下:
① 线程获得锁 ② 清空工作内存 ③ 从主内存拷贝共享变量最新的值到工作内存成为副本
④ 执行代码 ⑤ 将修改后的副本的值刷新回主内存中 ⑥ 线程释放锁
7、volatile与synchronized的区别
区别:
① volatile只能修饰实例变量和类变量,不能修饰局部变量,而synchronized可以修饰方法,以及代码块。
② volatile保证数据的可见性(立即更新),但是不保证原子性(++操作);而synchronized是一种排他(互斥)的机制(因此有时我们也将synchronized这种锁称之为排他(互斥)锁),synchronized修饰的代码块,被修饰的代码块称之为同步代码块,无法被中断可以保证原子性,也可以间接的保证可见性。
7、原子性
(1)概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
(2)案例:从张三的账户给李四的账户转1000元,这个动作将包含两个基本的操作:从张三的账户扣除1000元,给李四的账户增加1000元。这两个操作必须符合原子性的要求,要么都成功要么都失败。
(3)count++操作包含3个步骤:
① 从主内存中读取数据到工作内存
② 对工作内存中的数据进行++操作
③ 将工作内存中的数据写回到主内存
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
(4)产生问题的执行流程分析:
① 假设此时count的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量count的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态。
② 线程B也需要从主内存中读取count变量的值,由于线程A没有对count值做任何修改因此此时B读取到的数据还是100
③ 线程B工作内存中对count执行了+1操作,但是未刷新之主内存中
④ 此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作。
⑤ 线程B将101写入到主内存
⑥ 线程A将101写入到主内存
虽然计算了2次,但是只对A进行了1次修改。
问题演示:
public class MyAtomThread implements Runnable{
private int count=0;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//1,从共享数据中读取数据到本线程栈中.
//2,修改本线程栈中变量副本的值
//3,会把本线程栈中变量副本的值赋值给共享数据.
count++;
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread myAtomThread = new MyAtomThread();
for (int i = 1; i <= 100; i++) {
new Thread(myAtomThread).start();
}
}
}
问题结果:
(5)原子性问题的解决方案:
① 使用synchronized
public class MyAtomThread implements Runnable{
private int count=0;
private Object obj = new Object();
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//1,从共享数据中读取数据到本线程栈中.
//2,修改本线程栈中变量副本的值
//3,会把本线程栈中变量副本的值赋值给共享数据.
synchronized (obj) {
count++;
}
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread myAtomThread = new MyAtomThread();
for (int i = 1; i <= 100; i++) {
new Thread(myAtomThread).start();
}
}
}
② 使用Java中的原子类进行解决
public class MyAtomThread implements Runnable {
AtomicInteger ac = new AtomicInteger(0);
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
int count = ac.incrementAndGet();
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 1; i <= 100; i++) {
new Thread(atom).start();
}
}
}
8、原子类
(1)概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型
、原子更新数组
、原子更新引用
和原子更新属性(字段)
。本次我们只讲解:更新基本类型
(2)更新基本类型Atomic包提供了以下3个类:
AtomicBoolean: 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong: 原子更新长整型
以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下:
构造方法:
public AtomicInteger(): 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
成员方法:
int get(): 获取值
int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
AtomicInteger保证原子性的原理:CAS算法 +自旋锁
(3)CAS的全称是: Compare And Swap(比较再交换) 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-write转换为原子操作,这个原子操作直接由处理器保证。
**CAS原理:**CAS有3个操作数:内存值V,旧的预期值A,要修改的新值B。当且仅当旧预期值A和内存值V相同时,将内存值V修改为B并返回true,否则什么都不做,并返回false。
举例说明:
CAS算法的实现是通过Unsafe这个类中的相关方法进行实现,自旋的过程本质上就是一个循环。
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
Unsafe类中的方法:weakCompareAndSetInt方法的源码
public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
return compareAndSetInt(o, offset, expected, x);
}
Unsafe类中的方法:compareAndSetInt方法的源码
public final native boolean compareAndSetInt(Object o,long offset, int expected, int x);
(4)CAS和synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
① synchronized是从悲观的角度出发:
总是设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。
② CAS是从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。CAS这种机制我们也可以将其称之为乐观锁。采用CAS进行并发编程我们常常将其称之为无锁并发。
CAS算法的问题:ABA问题(自行了解一下)
9、Hashtable保证数据的安全性
(1)HashMap
public class MyHashMapDemo {
public static void main(String[] args) throws InterruptedException {
HashMap<String, String> hm = new HashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 25; i++) {
hm.put(i + "", i + "");
}
});
Thread t2 = new Thread(() -> {
for (int i = 25; i < 51; i++) {
hm.put(i + "", i + "");
}
});
t1.start();
t2.start();
System.out.println("----------------------------");
//为了t1和t2能把数据全部添加完毕
Thread.sleep(1000);
//0-0 1-1 ..... 50- 50
for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i + ""));
}//0 1 2 3 .... 50
}
}
HashMap是线程不安全的(多线程环境下可能存在问题)。
为了保证数据的安全性我们可以使用HashTable,但是HashTable的效率底下。
(2)Hashtable
//上述代码换成HashTable,结果输出正常,无null。
Hashtable<String, String> hm = new Hashtable<>();
Hashtable底层的数据结构是哈希表。它可以保证数据的安全性,但是效率比较低下。
Hashtable安全性
HashTable采取悲观锁synchronized的形式保证数据的安全性,只要有线程访问,会将整张表全部锁起来,所以HashTable的效率低下。只有当这个线程执行完毕以后,其他的线程才可以继续使用这个哈希表。
Hashtable存值原理
HashTable和HashMap一样底层是hash表结构,这个hash表是由数组+链表实现的,数组默认长度也是16,加载因子也是0.75(表示数组存满12个元素时hash表就要扩容)
Hashtable适用性
由于HashTable效率低下,因此后期再进行项目开发的时候,如果我们需要使用多个线程来操作容器,那么我们首选的就是:ConcurrentHashMap
(3)ConcurrentHashMap
继承关系
//上述代码换成ConcurrentHashMap,结果输出正常,无null。
ConcurrentHashMap<String, String> hm = new ConcurrentHashMap<>();
小结
HashMap是线程不安全的。多线程环境下会有数据安全问题。
HashTable是线程安全的,但是会将整张表锁起来,效率低下。
ConcurrentHashMap也是线程安全的,效率较高。
(4)ConcurrentHashMapJDK1.7的原理
存值和扩容原理
大数组不能扩容,数组长度16。小数组默认长度为2,加载因子0.75,每次扩容两倍。因为是数组,所以有索引和默认初始化值。
加锁原理
在JDK7的时候,这种安全策略采用的是分段锁的机制,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock。在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。
参用的就是分段锁来保证线程的安全性。
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
缺点
ConcurrentHashMap需要二次哈希,大数组也不能扩容,效率可以进一步提升。
(5)ConcurrentHashMap JDK1.8的原理
特点:底层实现:哈希表。(数组 + 链表 + 红黑树的结合体)。
① 存值和扩容原理
创建一个长度为16,加载因子为0.75。因为是数组,有索引,初始化值为null,可以扩容。
扩容的原理和变红黑树的原理和HashMap是一样的。
② 加锁原理
结合CAS机制+synchronized同步代码块形式保证线程安全。
但是ConcurrentHashMap可以保证线程的安全性,在保证线程安全性的时候使用的:局部锁 + CAS算法。
③ 小结
① 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。在第一次添加元素的时候创建哈希表。
② 计算当前元素应存入的索引。
③ 如果该索引位置为null,则利用CAS算法,将本结点添加到数组中。
④ 如果该索引位置不是null,则利用volatile关键字获得当前位置最新的结点地址。挂在他下边,变成链表。
⑤ 当链表长度大于等于8时,自动转换成红黑树。
⑥ 以链表或者红黑树头结点作为锁对象,配合悲观锁保证多线程操作集合时的数据安全性。
(6) TreeMap
10、CountDownLatch计数器
CountDownLatch作用:可以让一个线程等待其他线程执行完毕以后再次进行执行。
CountDownLatch原理:在CountDownLatch内部存在一个计数器,其他线程执行完毕以后,可以调用CountDownLatch中countDown方法对计数器进行-1操作,当计数器的值为0的时候,等待的线程被唤醒,此时开始执行。
构造方法:
public CountDownLatch(int count) // 初始化一个CountDownLatch,count表示的就是计数器初始化值
成员方法:
public void await() // 让线程处于等待状态
public void countDown() // 让计数器进行-1操作
代码演示
//设置一个妈妈线程,等待孩子吃完
public class Mother implements Runnable{
private CountDownLatch countDownLatch;
public Mother(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
countDownLatch.await();// 让线程处于等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("妈妈收拾碗筷");
}
}
//设置三个孩子线程
public class ChileThread1 implements Runnable{
private CountDownLatch countDownLatch;
public ChileThread1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
for (int i = 1; i <=15; i++) {
System.out.println(Thread.currentThread().getName()+"在吃第"+i+"个包子");
}
countDownLatch.countDown();// 让计数器进行-1操作
}
}
public class ChileThread2 implements Runnable{
private CountDownLatch countDownLatch;
public ChileThread2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
for (int i = 1; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"在吃第"+i+"个包子");
}
countDownLatch.countDown();// 让计数器进行-1操作
}
}
public class ChileThread3 implements Runnable{
private CountDownLatch countDownLatch;
public ChileThread3(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
for (int i = 1; i <=18; i++) {
System.out.println(Thread.currentThread().getName()+"在吃第"+i+"个包子");
}
countDownLatch.countDown();// 让计数器进行-1操作
}
}
//主类
public class Main {
public static void main(String[] args) throws InterruptedException {
// 初始化一个CountDownLatch,参数就是计数器初始化值
CountDownLatch countDownLatch = new CountDownLatch(3);
//创建线程
Thread t1 = new Thread(new ChileThread1(countDownLatch));
Thread t2 = new Thread(new ChileThread1(countDownLatch));
Thread t3 = new Thread(new ChileThread1(countDownLatch));
Thread t4 = new Thread(new Mother(countDownLatch));
//设置线程名称
t1.setName("小明");t2.setName("小刚");t3.setName("小花花");t4.setName("妈妈");
//启动线程
t4.start();Thread.sleep(1000);
t1.start();t2.start();t3.start();
}
}
执行结果
使用场景:让某一条线程等待其他线程执行完毕之后再执行。
11、Semaphore信号量
**(1)Semaphore作用:**控制访问某一个资源的线程数量(限制某段代码块的并发数)
(2)Semaphore构造方法:
public Semaphore(int permits) // int类型的参数表示的是同时访问资源的线程数量
semaphore有一个构造函数,可以传入一个int型整数n
由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
(3)Semaphore成员方法:
public void acquire() // 获取一个允许访问的标识
public void release() // 归还允许访问的标识
(4)代码演示
//线程类
public class MySemaphore implements Runnable{
//1.定义管理员对象
private Semaphore semaphore = new Semaphore(2);;
@Override
public void run() {
//2.获取通行证
try {
semaphore.acquire();
//3.开始行使
System.out.println(Thread.currentThread().getName()+"开始行使");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"归还通行证···");
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.归还通行证
semaphore.release();
}
}
//主类
public class Main {
public static void main(String[] args) {
MySemaphore mySemaphore = new MySemaphore();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(mySemaphore);
t.start();
}
}
}
(5)执行结果
**(6)Semaphore最常见的使用场景:**就是用来进行限流操作。