文章目录
- 01_线程:线程高并发及运行机制
- 02_线程:线程安全问题
- 03_线程:线程安全之可见性
- 04_线程:线程安全之有序性
- 05_线程:线程安全之原子性
- 06_线程:volatile关键字
- 07_线程:原子类
- 08_线程:原子类CAS机制
- 09_线程:操作数组的原子类
- 10_线程:多行代码原子性问题
- 11_线程:synchronized关键字
- 12_线程:同步方法
- 13_线程:Lock锁
- 14_线程:CopyOnWriteArrayList类
- 15_线程:CopyOnWriteArraySet类
- 16_线程:ConcurrentHashMap类
- 17_线程:CountDownLatch类
- 18_线程:CyclicBarrier类
- 19_线程:Semaphore类
- 20_线程:Exchanger类
- 20_线程:Exchanger类
01_线程:线程高并发及运行机制
目标
- 能够理解多个线程执行后的运行机制 【了解】
路径
-
高并发概念
-
多线程的运行机制
高并发
高并发:是指在某个时间点上,有大量的线程同时访问同一资源
例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况
多线程的运行机制
多个线程开启之后,在内存中有各自的独立栈空间。
多个线程在各自的独立栈空间中运行,运行时互不影响,也不存在某种运行顺序(CPU随机切换执行)
//线程类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("i = " + i);
}
}
}
//测试类
public class Demo {
public static void main(String[] args) {
//1.创建两个线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//2.启动两个线程
t1.start();
t2.start();
}
}
小结
当start()方法执行时:开辟新线程空间,运行run方法
在进程中,只要有一个线程还在运行,当前进程不会结束
多线程在执行时:每个线程都在各自独立的栈空间中运行,互相不干扰
多线程执行是无序(CPU随机切换执行)
02_线程:线程安全问题
目标
- 理解导致线程安全问题的原因 【了解】
路径
- 线程共享资源介绍
- 线程安全问题
- 线程安全问题代码演示
线程共享资源
什么是共享资源?
-
多个线程访问了相同资源(同一个资源),这个资源就称为共享资源
- 共享资源通常是存放在堆中(对象)或者方法区中(静态变量)
线程安全问题
- 当多个线程同时读写共享资源的时候,如果出现了数据的脏乱的现象,就表明出现了线程的安全问题
- 举例:在某一个时刻,大量用户(线程)在抢购车票,同一个车票卖给了多个用户
案例代码
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
while (true){ //窗口永远在卖票
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e. printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName() ;
System.out.println(name + "正在卖: " + ticket--) ;
}
}
}
}
//测试类
public class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
通过程序运行,将会发现存在有重复售票的情况发生(数据脏乱)
小结
什么情况下会出现线程安全问题?
- 有多个线程,同时访问同一个资源,且多个线程会对共享资源进行写(修改)操作,此时就可能会发生线程中数据脏乱的问题(线程安全问题)
03_线程:线程安全之可见性
目标
- 理解线程的可见性概念 【了解】
路径
- 可见性概述
- 可见性问题的代码演示
可见性
可见性:当多个线程访问同一个资源时,一个线程修改了这个资源的值,其他线程能够立即看得到修改的值
可见性问题:多个线程访问同一个资源时,一个线程修改了资源的值,其他线程看不到修改后的值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oujShni5-1617098707199)(JavaSE%EF%BC%9ADay09_%E9%9A%8F%E5%A0%82%E7%AC%94%E8%AE%B0.assets/1758811-20190820220137162-379693173.png)]
每个线程有独立的工作内存,操作数据时,是从主内存将变量读取到自己的工作内存,然后在工作内存中进行运算再把变量写回到主内存中
案例代码
问题描述:多个线程同时访问了共享资源,其中一个线程改变了共享资源的值。但是不能实时的让其他线程更新到
//线程类
class MyThread extends Thread {
public static int a = 0;//共享变量(全局变量) //存储在主内存中
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将主内存的值,a修改为1");
a = 1;//在线程自己的栈空间中运算后,再把运算的结果写入到主内存中的共享变量
}
}
//测试类
public class Demo01 {
public static void main(String[] args) {
new MyThread().start();//开启线程
while (true) {
if (MyThread.a == 1) {
System.out.println("主线程检测到了主内存中a的值为1");
}
}
}
}
小结
可见性:
- 多个线程在访问同一个资源时,一个线程修改了共享资源中的值,其他线程应该立刻能看到(正常的)
可见性问题(线程安全问题):
- 多个线程在访问同一个资源时,一个线程修改了共享资源中的值,其他线程看不到共享资源修改后的值
原理:JVM内存模型
- 线程把主内存中的数据,读取自己的工作栈空间,在自己的工作栈空间中运算后,再把运算后的数据写入到主内存中
04_线程:线程安全之有序性
目标
- 能够理解有序性的概念 【了解】
路径
- 有序性概述
- 重排概念
有序性
有序性:多线程在执行的时候,代码执行顺序应该和代码的编写顺序要一样
有序性问题:
-
代码在编译期间,为了优化指令,对指令进行了重排
在单线程环境下是没问题的,但是在多线程环境下会出现不确定的结果
重排概念
-
有些时候“编译器”在编译代码时,会对代码进行“重排”
-
int a = 10; //1 int b = 20; //2 int c = a + b; //3 第1行和第2行可能会被"重排":可能先编译第2行,再编译第1行 在执行第3行之前,会将1,2编译完毕。1和2先编译谁,不影响第3行的结果
-
-
在多线程中,代码重排,可能会对另一个线程访问的结果产生影响
多线程中,通常是不希望对一些代码进行重排的(书写顺序和执行顺序一致)
小结
出现有序性问题原因:
- 编译器在编译代码,重排代码的顺序。造成代码执行顺序和代码书写顺序不一致
- 由于多线程是由CPU随机切换执行,就会发生:执行中数据脏乱
05_线程:线程安全之原子性
目标
- 理解原子性的概念 【了解】
路径
- 原子性概述
- 原子性问题代码演示
原子性
原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
原子性问题:
- 一个操作或者多个操作,执行的过程中被其他因素影响,导致了数据结果的脏乱
案例代码
public class MyThread extends Thread {
static int num = 0;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()+"线程执行结束");
}
}
public class Test1 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据的值:"+MyThread.num);
}
}
小结
原子性:线程中有多个操作在执行时,要保证这些操作要全部完成之后,才执行其他线程中的操作
- 要么全部执行,要么全部不执行
出现原子性的原子性的原因:
- 当前线程在执行多个操作时,被CPU切换到其他线程上执行,而其他线程执行时影响了当前线程的操作,造成数据脏乱
06_线程:volatile关键字
目标
- 使用volatile解决线程的可见性问题、有序性问题 【掌握】
路径
- volatile关键字介绍
- 使用volatile解决可见性问题
- 使用volatile解决有序性问题
- volatile对于原子性问题的验证
volatile关键字
volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次使用前都从主内存获取值,并能保证此变量不会被编译器优化(重排)
-
volatile能解决变量的可见性、有序性
-
volatile不能解决变量的原子性
volatile解决可见性问题
public class MyThread extends Thread {
//共享资源
volatile static int a = 0; //在变量时添加关键字:volatile
//volatile关键字的作用:
//1、不允许重排
//2、每次使用数据时,都是从主内存中获取
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将主内存的值,a修改为1");
a = 1;//在线程自己的栈空间中运算后,再把运算的结果写入到主内存中的共享变量
System.out.println("修改成功!");
System.out.println(a);
}
}
volatile解决有序性问题
当变量被修饰为volatile时,会禁止代码重排
测试:volatile能否解决原子性问题
public class MyThread extends Thread {
volatile static int num = 0;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
num++;
System.out.println(num);
}
System.out.println(Thread.currentThread().getName()+"线程执行结束");
}
}
public class Test1 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据的值:"+MyThread.num);
}
}
//运行结果: 缺少2(数据脏乱问题存在)
1
3
4
5
6
7
小结
针对线程中的可见性问题、有序性问题,可以使用volatile关键字解决
使用方式:volatile是一个变量修饰符号
class 类{
//共享变量
volatile static int num;
}
原理:
- 可见性:每次都是从主内存中获取数据
- 有序性:volatile关键字修饰的变量,不允许进行重排
volatile不能解决线程中原子性问题
07_线程:原子类
目标
- 使用AtomicInteger解决可见性,原子性,有序性问题 【掌握】
路径
- 原子类概述
- 原子类AtomicInteger的介绍
- 使用原子类解决原子性问题
原子类
原子类,可以保证数据的原子性,可见性,有序性(可以保证线程安全)
在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:
-
java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
-
java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
-
java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
简单理解:原子类作为共享变量的类型存在
原子类AtomicInteger
构造方法:
public AtomicInteger() //创建具有初始值 0 的新 AtomicInteger
public AtomicInteger(int initialValue) //创建具有给定初始值的新 AtomicInteger
常用方法:
public int getAndIncrement() //以原子方式将当前值加 1。
public int get() //获取当前值
AtomicInteger解决原子性问题
import java.util.concurrent.atomic.AtomicInteger;
public class MyThread4 extends Thread {
//使用原子类
static AtomicInteger num = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"="+num.getAndIncrement());
}
System.out.println(Thread.currentThread().getName() + "线程执行结束");
}
}
public class Test1 {
public static void main(String[] args) {
new MyThread4().start();
new MyThread4().start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据的值:"+MyThread4.num);
}
}
小结
原子类,是用来解决线程的可见性问题、可序性问题、原子性问题
原子类:AtomicInteger、AtomicLong 、Atomic…
08_线程:原子类CAS机制
目标
- 能够理解CAS思想 【了解】
路径
- CAS原理介绍
- 底层源码分析
CAS原理
原子类为什么能够实现原子性,底层的原理?
- 自旋+CAS机制(CAS:Compare And Swap 先比较然后再做交换)
源码分析
//AtomicInteger类:
public final int incrementAndGet() {
//this:就是原子类对象 AtomicInteger对象
//VALUE: 是原子类对象value在对象中的地址偏移量,目的是为了能够实时的获取对象中value的值
return U.getAndAddInt(this, VALUE, 1) + 1;
// Obj 地址 1
// this+VALUE = AtomicInteger对象中的value属性值
}
Unsafe类: // 当前对象 地址偏移量 1
public final int getAndAddInt(Object o, long offset, int delta) {
// o+offset=AtomicInteger对象中的value属性值
int v;
do {
//v = AtomicInteger对象中的value属性值 //假设v=10
v = getIntVolatile(o, offset); //实时获取获取原子类对象中value的值
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
// 当前对象 地址偏移量 10 10+1
return v;
}
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
//o: 就是原子类对象
//offset:原子类对象,value属性的位置
//expected: 期望值(旧值)
//x : 将要设置交换的值
return compareAndSetInt(o, offset, expected, x);
// 10 11
}
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
//o和offset :实时获取最新值
//expected : 获取的旧值 //之前读取主内存中的值
//x : 将要设置交换的值 //要写入到主内存中的值
//获取AtomicInteger对象中最新的值: o+offset
//拿获取到的value值 和 expected变量的值 进行比较
//相等: 把x的值 写入到主内存中
//不相等:自旋(重新读主内存中的值 => 修改 => CAS判断 ......)
小结
CAS原理 :
- 在把线程栈空间中的数据写入到主内存之前,先拿主内存中的值和当前栈空间中的值(旧值),进行比较
- 相等:(其他没有干扰) 把栈空间中修改后的数据写入到主内存中
- 不相等:(其他线程干扰,影响了主内存中的数据值)
- 重新从主内存中读取新的值
- 线程栈空间重新运算…
09_线程:操作数组的原子类
目标
- 使用数组原子类解决原子性问题 【掌握】
路径
- 数组原子类介绍
- 原子类AtomicIntegetArray
- 使用AtomicIntegetArray类解决数组原子性问题
数组相关的原子类介绍
java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类java.utio.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类
以上原子类型表示的数组,能够保证数据的安全
原子类AtomicIntegetArray
构造方法:
AtomicIntegerArray(int length) //创建给定长度的新 AtomicIntegerArray。
AtomicIntegerArray(int[] array) //创建与给定数组具有相同长度的新AtomicIntegerArray,并从给定数组复制其所有元素
常用方法:
int getAndIncrement(int i) //以原子方式将索引 i 的元素加 1。
int get(int i) //获取位置 i 的当前值。
解决原子性问题
普通数组中存在的原子性问题
//线程
class MyThread1 extends Thread {
public static int[] arr = new int[10];
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
arr[0]++;//对0索引对应的元素进行自增
}
System.out.println(getName() + "自增完成~~");
}
}
//测试类
public class Demo02 {
public static void main(String[] args) throws InterruptedException {
new MyThread1().start();
new MyThread1().start();
new MyThread1().start();
Thread.sleep(1000);
System.out.println("累加结果:" + MyThread1.arr[0]);
}
}
使用AtomicIntegerArray原子类解决数组的原子性问题:
//线程
public class MyThread extends Thread{
//使用普通类型的数组(共同资源)
//public static int[] arr = new int[10];
//使用数组原子类
static AtomicIntegerArray array = new AtomicIntegerArray(10);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
//arr[0]++;//对0索引对应的元素进行自增
array.getAndIncrement(0);
}
System.out.println(getName() + "自增完成~~");
}
}
//测试
public class Test01 {
public static void main(String[] args){
new MyThread().start();
new MyThread().start();
new MyThread().start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("累加结果:" + MyThread.array.get(0));
}
}
小结
在多线程中,如果共公资源是一个数组,那么数组也会存在:原子性问题
针对int类型数组中的原子性问题,需要使用:AtomicIntegerArray原子类解决int类型数组
原子类所属的包:java.util.concurrent.atomic
10_线程:多行代码原子性问题
目标
- 能够理解多行代码原子性问题及解决思路 【了解】
路径
- 代码演示:多行代码中出现的原子性问题
- 分析案例中原子性问题出现的原因
多行代码中出现的原子性问题
//线程
class TicketTask implements Runnable {
//原子类
private AtomicInteger tickets = new AtomicInteger(100);
@Override
public void run() {
while (true) {
if (tickets.get()> 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + ":" + tickets.get());
tickets.getAndDecrement();//卖完一张减1
} else {
break;
}
}
}
}
//测试类
public class Demo01 {
public static void main(String[] args) {
//卖票任务
TicketTask ticketTask = new TicketTask();//定义一个任务,让三个线程完成
Thread t1 = new Thread(ticketTask,"【窗口1】");
Thread t2 = new Thread(ticketTask,"【窗口2】");
Thread t3 = new Thread(ticketTask,"【窗口3】");
//执行
t1.start();
t2.start();
t3.start();
}
}
//运行结果:
【窗口2】 出售编号为:100的票
【窗口1】 出售编号为:100的票
【窗口3】 出售编号为:100的票
【窗口1】 出售编号为:97的票
【窗口2】 出售编号为:97的票
【窗口3】 出售编号为:97的票
【窗口2】 出售编号为:94的票
【窗口1】 出售编号为:94的票
以上程序中有多行代码都使用了共享资源(使用了原子类)
当有多行代码访问了共享资源时,在多线程环境下,即使使用原子类也无法解决安全问题
分析为什么多行代码情况下,原子类无法解决线程安全问题
在多线程中,有多个位置,针对原子类进行操作,此时程序中还是存在原子性问题:
tickNum.get()>0 、tickNum.get()、 tickNum.getAndDecrement()
以上3处代码要做为一个整体来执行(要么全部不成功、要么全部成功)
小结
当线程中,使用了原子类,但有多处位置针对原子类进行操作,此时多线程中还是会存在原子性问题
当有多行代码访问了共享资源时,在多线程环境下,即使使用原子类也无法解决安全问题
- 解决方案:同步 synchronized
11_线程:synchronized关键字
目标
- 能够使用synchronized代码块解决线程安全问题 【掌握】
路径
- synchronized关键字介绍
- synchronized的使用方式
- 使用synchronized代码块解决多行代码原子性问题
synchronized
- synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”:将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。
synchronized的意义:
- 把多行代码做为一个整体来执行
- 只有做为整体的多行代码全部执行完之后,才会执行其它线程
synchronized的使用方式
synchronized有两种应用方式:
-
同步代码块
-
synchronized(同步锁){ //存在原子性问题的多行代码 }
-
-
同步方法
同步机制的理念:
- 在同步代码中添加了一个锁,利用锁实现了同步机制
- 当线程A进入同步代码中执行时,会获取到一个锁(锁被线程A拿到)
- 当线程B被CPU切换并执行时,线程B要进入同步代码中执行,此时需要拿到锁才能执行,因为线程B没有锁,只能等待,等待线程A释放锁。
使用synchronized同步代码块,解决多行代码的原子性问题
import java.util.concurrent.atomic.AtomicInteger;
//线程任务
public class Ticket implements Runnable {
//原子类(票数)【共享资源】
AtomicInteger tickNum = new AtomicInteger(100);
//同步代码块上的锁只是一个概念,可以使用任意类型作为锁存在
Object lock = new Object();
@Override
public void run() {
while (true) {
//同步代码块上需要一个对象锁,可以使用任意类型的对象作为锁存在
synchronized (lock) {
if (tickNum.get() > 0) {//票还没卖完
// try {
// //Thread.sleep(50); //模拟出票时间
// //lock.wait(50);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
String name = Thread.currentThread().getName(); //获取当前线程的名称
System.out.println(name + " 出售编号为:" + tickNum.get() + "的票");
//修改原子类的值
tickNum.getAndDecrement();//相当于value-1
} else {//票已卖完
break;
}
}
}
}
}
public class Test1 {
public static void main(String[] args) {
//实例化线程任务
Ticket task = new Ticket();
//创建三个卖票窗口
Thread t1 = new Thread(task, "【窗口1】");
Thread t2 = new Thread(task, "【窗口2】");
Thread t3 = new Thread(task, "【窗口3】");
t1.start();
t2.start();
t3.start();
}
}
小结
当程序中有多行代码针对共享资源进行操作时,会存在:原子性问题
可以使用同步机制,来解决多行代码操作共享资源时存在的原子性问题
同步的使用:
-
同步代码块
-
Object lock = new Object(); synchronized(lock){ //多行操作共享资源的代码 }
-
-
同步方法
12_线程:同步方法
目标
- 能够使用同步方法解决线程安全问题 【掌握】
路径
- 同步方法的定义
- 使用同步方法解决线程安全问题
同步方法
使用关键字synchronized修饰的方法,称为:同步方法
同步方法的意义 :
- 书写在方法体中的代码,相当于一个同步代码块(方法上添加了锁)
同步方法的书写格式:
修饰符 synchronized 返回值类型 方法名(参数类型 参数){
}
使用同步方法解决线程安全问题
public class Ticket implements Runnable {
//原子类(票数)【共享资源】
AtomicInteger tickNum = new AtomicInteger(100);
@Override
public void run() {
while (true) {
//调用同步方法
ticket();
}
}
//自定义一个方法
public synchronized void ticket() {
if (tickNum.get() > 0) {
String name = Thread.currentThread().getName(); //获取当前线程的名称
System.out.println(name + " 出售编号为:" + tickNum.get() + "的票");
//修改原子类的值
tickNum.getAndDecrement();//相当于value-1
}
}
}
//测试类
public class Test1 {
public static void main(String[] args) {
//实例化线程任务
Ticket task = new Ticket();
//创建三个卖票窗口
Thread t1 = new Thread(task, "【窗口1】");
Thread t2 = new Thread(task, "【窗口2】");
Thread t3 = new Thread(task, "【窗口3】");
t1.start();
t2.start();
t3.start();
}
}
疑问:同步方法中的锁在哪里?
答案:同步方法上的锁会根据不同的方法使用不同的锁(隐式)
//非静态方法
public synchronized void method(){
//同步方法中的锁:this (拿当前对象做为锁)
}
//静态方法
public static synchronized void method(){
//静态方法中不能使用关键字:this、super
//静态同步方法中的锁:类名.class (这个知识点在反射技术中讲解)
}
小结
同步方法定义:
修饰符 synchronized 返回值类型 方法名(参数类型 参数){
}
- 同步方法在执行时,会获取到同步锁(this或类名.class),拿到锁后就进入方法体中执行
- 其他线程在调用同步方法时,判断是否有同步锁,如果没有拿锁,则在方法外等待
13_线程:Lock锁
目标
- 能够使用Lock锁解决线程安全问题 【掌握】
路径
- Lock介绍
- 使用Lock解决线程安全问题
Lock
java.util.concurrent.locks.Lock机制提供了比同步代码块和同步方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大。
Lock是一个接口,无法实例化,需要使用子类ReentrantLock进行实例化:
public ReentrantLock() //无参构造方法
//示例
Lock lock = new ReentrantLock();
Lock是一个接口,里面定义了获取锁及释放锁的功能:
public void lock() //添加同步锁
public void unlock() //释放同步锁
可以使用lock()和unlock()代替同步代码块
使用Lock锁解决线程安全问题
public class Ticket implements Runnable {
//原子类(票数)【共享资源】
AtomicInteger tickNum = new AtomicInteger(100);
//实例化Lock
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); //添加上Lock锁
if (tickNum.get() > 0) {//票还没卖完
String name = Thread.currentThread().getName(); //获取当前线程的名称
System.out.println(name + " 出售编号为:" + tickNum.get() + "的票");
//修改原子类的值
tickNum.getAndDecrement();//相当于value-1
} else {//票已卖完
break;
}
lock.unlock();//释放Lock锁
}
}
}
public class Test1 {
public static void main(String[] args) {
//实例化线程任务
Ticket task = new Ticket();
//创建三个卖票窗口
Thread t1 = new Thread(task, "【窗口1】");
Thread t2 = new Thread(task, "【窗口2】");
Thread t3 = new Thread(task, "【窗口3】");
t1.start();
t2.start();
t3.start();
}
}
小结
JDK中提供了一个比synchronized同步机制更强大的一个Lock锁
使用Lock的方式:
//创建Lock对象
Lock lock = new ReenTrantLock();
//在方法体中,添加lock锁、释放lock锁
public void method(){
//....
lock.lock();
//.......
lock.unlock();
}
在之前学习的集合:
- List:ArrayList //线程不安全
- Set:HashSet //线程不安全
- Map:HashMap //线程不安全
14_线程:CopyOnWriteArrayList类
目标
- 使用CopyOnWriteArrayList解决多线程中集合容器安全问题 【掌握】
路径
- 演示:多线程环境下ArrayList
- CopyOnWriteArrayList类介绍
- 演示:使用CopyOnWriteArrayList解决多线程中集合安全问题
多线程环境下ArrayList
ArrayList类是线程不安全的集合
//线程
class MyThread extends Thread {
public static ArrayList<Integer> list = new ArrayList<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
System.out.println(getName() + "添加完毕~~");
}
}
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
new MyThread().start();
new MyThread().start();
Thread.sleep(1000);
System.out.println("集合元素个数:"+MyThread.list.size());
}
}
CopyOnWriteArrayList类
java.util.concurrent.CopyOnWriteArrayList
- 是一个线程安全的List集合类(可以理解为是一个线程安全的ArrayList集合)
构造方法:
public CopyOnWriteArrayList() //创建一个线程安全的ArrayList集合对象
使用CopyOnWriteArrayList集合解决线程安全问题
import java.util.concurrent.CopyOnWriteArrayList;
public class MyThread1 extends Thread{
//创建集合(共享资源) //线程安全的ArrayList集合
static CopyOnWriteArrayList<Integer> list =new CopyOnWriteArrayList<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
System.out.println("集合添加完成!!!");
}
}
//测试类
public class Test01 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread1 t2 = new MyThread1();
t1.start();
t2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("集合的大小为:"+MyThread1.list.size());
}
}
小结
在开发中,如操作List集合时,遇到线程不安全的情况,需要使用:JUC并发包下的集合对象
- java.util.concurrnet.CopyOnWriteArrayList类
15_线程:CopyOnWriteArraySet类
目标
- 使用CopyOnWriteArraySet解决多线程中集合容器安全问题 【掌握】
路径
- 演示:多线程环境下HashSet
- CopyOnWriteArraySet类介绍
- 演示:使用CopyOnWriteArraySet解决多线程中集合安全问题
多线程环境下HashSet
- HashSet在多线程环境是不安全的,去重不彻底
//线程
class MyThread extends Thread {
public static HashSet<Integer> set = new HashSet<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
set.add(i);
}
System.out.println(getName() + "添加结束!!");
}
}
//测试类
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
new MyThread().start();
new MyThread().start();
new MyThread().start();
Thread.sleep(2000);
System.out.println("set集合元素大小:" + MyThread.set.size());//10884
}
}
CopyOnWriteArraySet类
java.util.concurrent.CopyOnWriteArraySet
- 是一个线程安全的Set集合类(可以理解为是一个线程安全的HashSet集合)
构造方法:
public CopyOnWriteArraySet() //创建一个线程安全的HashSet集合对象
使用CopyOnWriteArraySet类解决集合安全问题
import java.util.concurrent.CopyOnWriteArraySet;
public class MyThread2 extends Thread {
//创建集合(共享资源)
public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
set.add(i);
}
System.out.println(getName() + "添加结束!!");
}
}
//测试类
public class Test02 {
public static void main(String[] args) {
new MyThread2().start();
new MyThread2().start();
new MyThread2().start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("set集合元素大小:" + MyThread2.set.size());
}
}
小结
当程序中使用HashSet集合时,遇到线程不安全问题,使用:CopyOnWriteArraySet集合代替
16_线程:ConcurrentHashMap类
目标
- 使用ConcurrentHashMap解决多线程中集合容器安全问题 【掌握】
路径
- 演示:多线程环境下HashMap
- ConcurrentHashMap类介绍
- 演示:使用ConcurrentHashMap解决多线程中集合安全问题
多线程环境下HashMap
HashSet的底层是HashMap实现,HashSet是不安全的可以推断出HashMap也是不安全
//线程
class MyThread extends Thread{
public static HashMap<Integer, String> map = new HashMap<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
map.put(i, "");
}
System.out.println(getName()+"数据添加完毕");
}
}
//测试类
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
new MyThread().start();
new MyThread().start();
new MyThread().start();
Thread.sleep(2000);
System.out.println("集合元素大小:"+MyThread.map.size());
}
}
ConcurrentHashMap类
构造方法:
ConcurrentHashMap() //创建一个线程安全的HashMap集合对象
使用ConcurrentHashMap类解决线程安全问题
import java.util.concurrent.ConcurrentHashMap;
public class MyThread3 extends Thread{
//创建集合(共享资源)
public static ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
map.put(i, "");
}
System.out.println(getName()+"数据添加完毕");
}
}
public class Test03 {
public static void main(String[] args) throws InterruptedException {
new MyThread3().start();
new MyThread3().start();
new MyThread3().start();
Thread.sleep(2000);
System.out.println("集合元素大小:"+MyThread3.map.size());
}
}
小结
ArrayList类:List集合,属于线程不安全的集合对象
HashSet类:Set集合,属于线程不安全的集合对象
HashMap类:Map集合,属于线程不安全的集合对象
针对以上三个线程不安全的集合对象,如要保证线程是安全的,需要使用JUC并发包下的集合对象:
- CopyOnWriteArrayList , 是一个线程安全的List集合对象
- CopyOnWirteArraySet ,是一个线程安全的Set集合对象
- ConcurrnetHashMap , 是一个线程安全的Map集合对象
说明:线程不安全的集合执行效率高、线程安全的集合执行效率低
- 当开发中对线程安全没有要求时,就使用普通的集合对象
17_线程:CountDownLatch类
目标
- 能够使用并理解CountDownLatch类的功能
路径
- CountDownLatch类介绍
- CountDownLatch类常用方法
- 案例:CountDownLatch的应用
CountDownLatch类
-
java.util.concurrent.CountDownLatch 是一个同步辅助类
-
在完成一组正在其他线程中执行的操作之前,CountDownLatch允许一个或多个线程一直等待
-
例如: 线程1要执行打印:A和C 线程2要执行打印:B 但线程1在打印A后,要线程2打印B之后才能打印C, 所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行
-
CountDownLatch类中方法
构造方法:
public CountDownLatch(int count) //创建一个指定计数器的CountDownLatch对象
//参数:count就是要计数的次数
常用方法:
public void await() throws InterruptedException// 让当前线程等待
public void countDown() // 计数器进行减1(如果计数到达零,则释放所有等待的线程)
示例:打印A、B、C (按顺序打印)
线程1执行打印:A、C
线程2要执行打印:B
CountDownLatch c = new CountDownLatch(1);//要等待1个线程 count=1
线程1:
打印 A
c.await();//处于等待
//等到count=0时,结束等待,继续执行
打印 C
线程2:
打印 B
c.countDown();//对count进行-1 count=0
案例代码
需求:
-
甲,乙,丙三人在开发一个项目。甲和乙负责开发功能,丙负责发布项目
-
甲开发功能需要3秒
-
乙开发功能需要5秒
/*思路:
甲,乙分成一组:负责计数
丙 分组一组:负责等待 (甲乙执行完,再执行)
*/
//步骤:
//1. 定义一个CountDownLatch计数器,计数两次(甲,乙各自计数一次)
//2. 分别定义线程表示甲,乙,丙
import java.util.concurrent.CountDownLatch;
public class CountDownDemo1 {
public static void main(String[] args) {
//创建一个计数器对象(计数2次)
CountDownLatch downLatch = new CountDownLatch(2);
//匿名内部类
//丙
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("丙 进入项目组.....");
try {
//只要计数器不等于0,就处于一直等待状态
//当计数器等于0,结束等待,继续向下执行
downLatch.await();//等待甲乙开发结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("甲和乙开完功能,丙开始部署项目上线~~~~~");
}
}).start();
//甲
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("甲 进入项目组.....");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("甲 项目开发结束!");
downLatch.countDown();//计数器-1
}
}).start();
//乙
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("乙 进入项目组.....");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("乙 项目开发结束!");
downLatch.countDown();//计数器-1
}
}).start();
}
}
小结
java.util.concurrent.CountDownLatch类:是一个线程辅导类
- 应用场景:当执行的线程要等待其他线程中的任务完成后才继续执行时,可以使用CountDownLatch(线程计数器类)
18_线程:CyclicBarrier类
目标
- 能够使用并理解CyclicBarrier的功能
路径
- CyclicBarrier类介绍
- CyclicBarrier类常用方法
- 案例:CyclicBarrier的应用
CyclicBarrier类
- java.util.concurrent.CyclicBarrier 是一个同步辅助类
- CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。
- CyclicBarrier 允许一组线程(多个线程)互相等待,直到到达某个公共屏障点
- 它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
- 例如:公司召集5名员工开会,等5名员工都到了,会议开始
CyclicBarrier 应用场景:
- 当一组线程全部达到共同点(公共屏障点),才会执行另一个线程
- 当同一组线程中还有线程未达到共同点,已达到共同点的线程进入阻塞状态(等待)
CyclicBarrier类中方法
构造方法:
public CyclicBarrier(int parties, Runnable barrierAction) //创建一个循环屏障,定义指定相互等待的线程个数(parties),同时指定一个Runnable任务,当线程都达到屏障点时,先执行这个任务
参数:
int parties 要等待的线程个数
Runnable barrierAction 当所有线程都达到屏障点后,要执行任务
常用方法:
public int await() //每个线程调用await方法时告诉CyclicBarrier已经到达了屏障,然后当前线程被阻塞
案例代码
需求:用代码实现甲,乙,丙要开会,只有三人都同时到场才能开会。
- 甲达到现场需要3秒,乙达到现场需要2秒,丙达到现场需要5秒
/*步骤:
1. 定义循环屏障的对象,设定参与等待的数量为3
2. 使用三个线程模拟甲,乙,丙
*/
public class CyclicDemo1 {
// 需求:用代码实现甲,乙,丙要开会,只有三人都同时到场才能开会。
// 甲达到现场需要3秒,乙达到现场需要2秒,丙达到现场需要5秒
public static void main(String[] args) {
//创建循环屏障点类
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println("人到齐了,可以开会了");
}
});
//甲
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("甲 进入公司了....");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("甲 等待其他同事...");
try {
cyclicBarrier.await();//+1
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
//丙
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("丙 进入公司了....");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("丙 等待其他同事...");
try {
cyclicBarrier.await();//+1
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
//乙
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("乙 进入公司了....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("乙 等待其他同事...");
try {
cyclicBarrier.await();//+1
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
}
}
小结
CyclicBarrier类应用场景:
- 在执行某个线程任务之前,需要先达成其他任务时,使用CyclicBarrier
- 例如:开发时要求人员全部达到,才开会
19_线程:Semaphore类
目标
- 能够使用并理解Semaphore的功能
路径
- Semaphore类介绍
- Semaphore类常用方法
- 案例:Semaphore的应用
Semaphore类
- java.util.concurrent.Semaphore ,主要作用是控制线程的并发数量
- Semaphore可以设置同时允许几个线程执行,常用于控制访问特定资源的线程数目
事例:控制某个旅游景点,参观的人数。某个景点只允许同时2个人访问。可以使用Semaphore去实现
Semaphore类中方法
构造方法:
public Semaphore(int permits) //permits 表示许可线程的数量
public Semaphore(int permits, boolean fair) //fair表示公平性
//参数:
//permits 初始的许可线程数量
//fair 如果此信号量保证在争用时按先进先出的顺序授予许可,则为 true;否则为 false。
常用方法:
public void acquire() throws InterruptedException //表示获取许可
public void release() //表示释放许可
案例代码
需求:控制某个旅游景点的参观人数。
-
某个景点只允许同时2个人访问(使用Semaphore实现)
-
有10个人访问景点,每个人访问景点需要花费时间3秒
/*步骤:
1. 创建信号量对象,设定许可数为2
2. 创建线程执行访问资源的任务,使用信号量Semaphore去控制并发数量
*/
import java.util.concurrent.Semaphore;
//模拟实现,参观景点的过程
class VisitAction implements Runnable {
private Semaphore semaphore;
public VisitAction(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "来到了景点的门口~~~~~~~");
try {
semaphore.acquire();//获取许可
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "进入到景点,开始参观++++++++");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "离开景点,结束参观--------");
semaphore.release();//释放许可
}
}
//测试类
public class Demo01 {
public static void main(String[] args) {
//创建信号量,指定许可数为2
Semaphore semaphore = new Semaphore(2);
//参观景点任务
VisitAction visitAction = new VisitAction(semaphore);
//模拟10个线程表示10个参观者
for (int i = 0; i < 10; i++) {
new Thread(visitAction, "游客" + i).start();
}
}
}
小结
Semaphore类应用场景:
- 当针对共享资源,进行线程访问量的设置时,可以使用Semaphore类
JUC并发包下的类:
- CountDownLatch类
- 应用场景:
- 当前线程在执行时,可以等待其他线程先执行完,再执行当前线程
- 应用场景:
- CyclicBarrier类
- 应用场景:
- 多个线程一起执行,当多个线程达到某个设置的共同点时,此时激活另一个线程,执行激活的线程
- 应用场景:
- Semaphoer类
- 应用场景:
- 多个线程在访问共同资源时,可以针对共同资源进行线程并发数量的限制
- 应用场景:
20_线程:Exchanger类
目标
- 能够使用并理解Exchanger的功能
路径
- Exchanger类介绍
- Exchanger类常用方法
- 案例:Exchanger的应用
Exchanger类
- java.util.concurrent.Exchanger ,是一个用于线程间协作的工具类
- Exchanger可以实现两个线程之间的数据交换
- 这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方
Exchanger类中方法
构造方法:
Exchanger() //创建一个新的 Exchanger
常用方法:
V exchange(V x) //等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象
//参数:v 传递给另一个线程的数据
//返回值:V 接收到的另一个线程发送的数据
案例代码
需求:模拟两个人交换礼物
import java.util.concurrent.Exchanger;
//线程A
class ThreadA extends Thread{
Exchanger<String> ex ;
public ThreadA(Exchanger<String> exchanger){
this.ex = exchanger;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "把'礼物A'送给别人...");
try {
System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物A"));
//把 礼物A 发送给另一个线程
//当另一个线程也执行:exchange()方法,就会接收到'礼物A'
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//线程B
class ThreadB extends Thread{
Exchanger<String> ex ;
public ThreadB(Exchanger<String> exchanger){
this.ex = exchanger;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "把'礼物B'送给别人...");
try {
System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物B"));
//把 礼物B 发送给另一个线程
//当另一个线程也执行:exchange()方法,就会接收到'礼物B'
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test01 {
public static void main(String[] args) {
//创建对象
Exchanger<String> exchanger = new Exchanger<>();
//实例化两个线程对象(保证两个对象使用的是同一个Exchanger对象)
ThreadA t1 = new ThreadA(exchanger);
ThreadB t2 = new ThreadB(exchanger);
t1.start();
t2.start();
}
}
小结
Exchanger类:
- 当两个线程之间要进行数据的交互时,可以使用Exchaner类实现
- 要保证两个线程使用的是同一个Exchaner对象
20_线程:Exchanger类
目标
- 能够使用并理解Exchanger的功能
路径
- Exchanger类介绍
- Exchanger类常用方法
- 案例:Exchanger的应用
Exchanger类
- java.util.concurrent.Exchanger ,是一个用于线程间协作的工具类
- Exchanger可以实现两个线程之间的数据交换
- 这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方
Exchanger类中方法
构造方法:
Exchanger() //创建一个新的 Exchanger
常用方法:
V exchange(V x) //等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象
//参数:v 传递给另一个线程的数据
//返回值:V 接收到的另一个线程发送的数据
案例代码
需求:模拟两个人交换礼物
import java.util.concurrent.Exchanger;
//线程A
class ThreadA extends Thread{
Exchanger<String> ex ;
public ThreadA(Exchanger<String> exchanger){
this.ex = exchanger;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "把'礼物A'送给别人...");
try {
System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物A"));
//把 礼物A 发送给另一个线程
//当另一个线程也执行:exchange()方法,就会接收到'礼物A'
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//线程B
class ThreadB extends Thread{
Exchanger<String> ex ;
public ThreadB(Exchanger<String> exchanger){
this.ex = exchanger;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "把'礼物B'送给别人...");
try {
System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物B"));
//把 礼物B 发送给另一个线程
//当另一个线程也执行:exchange()方法,就会接收到'礼物B'
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test01 {
public static void main(String[] args) {
//创建对象
Exchanger<String> exchanger = new Exchanger<>();
//实例化两个线程对象(保证两个对象使用的是同一个Exchanger对象)
ThreadA t1 = new ThreadA(exchanger);
ThreadB t2 = new ThreadB(exchanger);
t1.start();
t2.start();
}
}
小结
Exchanger类:
- 当两个线程之间要进行数据的交互时,可以使用Exchaner类实现
- 要保证两个线程使用的是同一个Exchaner对象