在上篇中我们深入学习了JUC中的lock锁与synchronized关键字的区别,以及一些关键问题,特点的探讨,这一篇我们继续进行JUC的学习。
线程安全是什么意思呢?
线程安全是指在多线程运行的情况下,不会导致代码逻辑顺序发生异常。
比如我们常常听说的超卖情况,明明100件产品却卖给了110个甚至更多的人,这就是线程不安全导致的,所以我们这篇文章就是要解决这个问题。
集合的安全性问题
我先附上一段代码,希望小伙伴们先理解如下代码
package com.test.rabbitmq.lockTest;
import java.util.ArrayList;
import java.util.UUID;
/**
* @author ME
* @date 2022/2/5 20:31
*/
public class Test {
public static void main(String[] args) {
// 创建集合对象
ArrayList<String> list = new ArrayList<>();
// 循环添加元素
for (int i = 1; i <= 10; i++) {
// 添加一个五位的随机数
list.add(UUID.randomUUID().toString().substring(0, 5));
//打印集合内容
System.out.println(list);
}
}
}
上图代码正常执行是没有问题的,但是当我们将for循环中逻辑放入到多个线程中是否会有问题呢?代码如下图
package com.test.rabbitmq.lockTest;
import java.util.ArrayList;
import java.util.UUID;
/**
* @author ME
* @date 2022/2/5 20:31
*/
public class Test {
public static void main(String[] args) {
// 创建集合对象
ArrayList<String> list = new ArrayList<>();
// 循环添加元素
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
// 添加一个五位的随机数
list.add(UUID.randomUUID().toString().substring(0, 5));
//打印集合内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
执行如上代码后,会出现ConcurrentModificationException(并发修改异常),说明ArrayList是不安全的,那么此时我们应该如何解决呢?
解决方法如下
package com.test.rabbitmq.lockTest;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author ME
* @date 2022/2/5 20:31
*/
public class Test {
public static void main(String[] args) {
// 创建集合对象
// 解决ArrayList线程不安全问题方案
// 方案一:使用Vector集合,List<String> list = new Vector<>();
// 方案二:使用集合工具类中的同步方法,List<String> list = Collections.synchronizedList(new ArrayList<>());
// 方案三:List<String> list = new CopyOnWriteArrayList<>();
List<String> list = new CopyOnWriteArrayList<>();
// 循环添加元素
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
// 添加一个五位的随机数
list.add(UUID.randomUUID().toString().substring(0, 5));
//打印集合内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
Vector集合如何实现线程安全的呢?
add方法上添加了synchronized关键字
Collections.synchronizedList如何实现线程安全呢?
他锁住了整个集合对象
CopyOnWriteArrayList如何实现线程安全呢?
他使用了JUC可重入锁,效率比synchronized关键字效率高
Set集合同样是线程不安全的,想要让Set集合变得安全,与ArrayList同理
Collections.synchronizedSet方法,使用同步代码块。
CopyOnWriteArraySet方法,底层与CopyOnWriteArrayList调用同一个方法,都是用lock锁实现线程安全的。
Map集合也是线程不安全的,线程安全的Map集合为ConcurrentHashMap
CountDownLatch
减法计数器,当我们需要确定子线程全部结束后继续向下执行时,我们就可以使用CountDownLatch计数器,当计数器清零后继续向下执行。代码如下
package com.test.rabbitmq.lockTest;
import java.util.concurrent.CountDownLatch;
/**
* @author ME
* @date 2022/2/5 20:31
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
// 创建线程计数器
CountDownLatch count = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
// 循环创建线程执行逻辑
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程执行完毕");
// 每一次子线程执行完毕后进行计数器的-1
count.countDown();
}).start();
}
//等待线程计数器为0时继续向下执行,如果不写的话不会校验计数器是否清零,直接继续执行
count.await();
System.out.println("子线程执行完毕");
}
}
CyclicBarrier
加法计数器,当我们执行await()方法时,计数器加1,当达到了对象设置的最大值时,可以进行返回并执行对象设置的线程逻辑。代码如下
package com.test.rabbitmq.lockTest;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
/**
* @author ME
* @date 2022/2/5 20:31
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
// 线程计数器,参数:1.线程计数器计数到多大的值时会结束,2.当计数器达到了参数一的值后,我们可以执行再去执行一个线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, ()->{System.out.println("5个子线程全部执行结束"); });
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行完毕");
try {
// 计数器进行+1后进入等待
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
线程信号量,可以用来限流,确定程序中可存在的最大线程数量,当线程数量达到了最大线程数量时,其他线程进行等待。
package com.test.rabbitmq.lockTest;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* @author ME
* @date 2022/2/5 20:31
*/
public class Test {
public static void main(String[] args) {
// 限制了最大的线程数量为2个,可以添加第二个参数是否为公平锁/非公平锁
// 当线程达到2个的时候,则开始等待,待一个线程运行结束后,再开启下一个线程
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
// 将线程加入执行的队列
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "加入队列");
// 使线程睡眠两秒
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开队列");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 将线程从执行的队列中释放掉
semaphore.release();
}
}).start();
}
}
}
ReadWriteLock读写锁
当我们多个线程进行读写的时候,一般时候希望读的时候可以多个线程一起读取,但是写入的时候只能有一个线程进行写入,所以需要使用读写锁。
我们先来看一下不使用读写锁的情况
1.创建一个写入方法与读取方法
package com.test.lock.readWriteLock;
import java.util.HashMap;
import java.util.Map;
/**
* @author ME
* @date 2022/2/6 17:18
*/
public class MyData {
private volatile Map<String, String> data = new HashMap<>();
// 写入方法
public void put(String key, String value) {
System.out.println(Thread.currentThread().getName() + "写入了" + key);
data.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完成");
}
// 读取方法
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取了" + key);
String s = data.get(key);
System.out.println(Thread.currentThread().getName() + "读取完成");
}
}
2.多线程进行读写
package com.test.lock.readWriteLock;
/**
* @author ME
* @date 2022/2/6 17:18
*/
public class ReadWriteLock {
public static void main(String[] args) {
MyData myData = new MyData();
// 10个线程进行写入操作
for (int i = 0; i < 10; i++) {
final int temp = i;
new Thread(() -> {
myData.put(String.valueOf(temp), String.valueOf(temp));
}, String.valueOf(i)).start();
}
// 10个线程进行读取操作
for (int i = 0; i < 10; i++) {
final int temp = i;
new Thread(() -> {
myData.get(String.valueOf(temp));
}, String.valueOf(i)).start();
}
}
}
执行结果如下
使用读写锁进行改造
package com.test.lock.readWriteLock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author ME
* @date 2022/2/6 17:18
*/
public class MyData {
private volatile Map<String, String> data = new HashMap<>();
// 创建一个读写锁,相比于ReentrantLock,颗粒度更小
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 写入方法
public void put(String key, String value) {
// 进行写锁上锁
reentrantReadWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入了" + key);
data.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完成");
} finally {
// 进行写锁解锁
reentrantReadWriteLock.writeLock().unlock();
}
}
// 读取方法
public void get(String key) {
// 进行读锁上锁
reentrantReadWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取了" + key);
String s = data.get(key);
System.out.println(Thread.currentThread().getName() + "读取完成");
} finally {
// 进行读锁解锁
reentrantReadWriteLock.readLock().unlock();
}
}
}
此时执行结果如下
读写锁总结:
看到这里的同学我觉得是赚到了,因为我在学习读写锁的时候想到了一个问题,奈何比较愚笨,思考了比较长的时间,接下来我就把这个问题分享出来。
我们要先提出一个概念——读锁是共享锁,写锁是排他锁
共享锁:可以多个线程进行共享的锁
排他锁:同一时刻只能由一个线程运行的锁
了解到了上面的概念后我想提出两个问题:
1.共享锁既然可以多个线程共享,那么为什么还需要加锁?
2.排他锁既然约束力同一时刻只能有一个线程运行,那他跟普通的锁有什么区别?
答:
1.我们要注意创建的读写锁是同一个对象,只是分别调用了不同方法才分出来读锁与写锁。读锁是共享锁,说明上了读锁后的线程,可以一起使用,这个锁是用来锁定此时的状态是读取状态,只有读取数据的线程才能一起共享,此时如果有写锁来争抢锁,则需要等待,而不是共享。
2.上面问题一的答案中说过了,读写锁是同一个对象,相比于普通的锁,他的颗粒度更细,不会像普通锁一样锁住数据,只有在写锁的时候才会锁定数据,且写入的时候不可读取,读取的时候不可写入,但是当写锁争抢到锁以后,他就与普通的锁没有了区别。
BlockingQueue阻塞队列
阻塞队列这里我单拿出了一篇文章,因为跟这篇文章的学习重点不是很一致,有想了解的请戳下面链接↓
SynchronousQueue同步队列
同步队列与普通队列的区别在于同步队列中只能有一个元素,只有元素被取出后才能放入下一个元素。
新增元素put()方法;取出元素take()方法。