在介绍同步容器之前,让我们先回顾一下有哪些常见的线程安全的类、有哪些非线程安全的类:
对于这些线程不安全的类,我们可以主动使用synchronized关键字或Lock锁来使其线程安全,但是这样一来效率就变低了;而如果不使用synchronized关键字或Lock锁,那么又线程不安全。JUC就给我们提供了一些不需要我们主动使用synchronized或Lock来保证线程安全(内部已经写好了synchronized或lock了),又比(Vector、Propertues、StringBuffer、Hashtable等)高效率的类:
◎ HashMap ---> ConcurrentHashMap 分段锁Map
◎ ArrayList ---> CopyOnWriteArrayList 写复制(读不复制)List
◎ HashSet ---> CopyOnWriteArraySet 写复制(读不复制)Set
……
提示:---> 右边的和左边的,用法基本一致。
注:CopyOnWriteArrayList、CopyOnWriteArraySet 我们一般简称为COW。
声明1:ConcurrentHashMap与CopyOnWriteArrayList用得相对较多,本文也只简单介绍ConcurrentHashMap
与CopyOnWriteArrayList,CopyOnWriteArraySet的原理思想与CopyOnWriteArrayList是一样的,其用
法与HashSet基本一致,本文就不介绍CopyOnWriteArraySet了。
声明2:本文为浅层次介绍,后面等笔者不那么忙有时间精力了,会深入源码的学习并分享给大家。
ConcurrentHashMap
ConcurrentHashMap原理简述(举例说明):
我们知道,程序员主动通过synchronized或Lock可实现多线程下安全使用HashMap,而ConcurrentHashMap则不需要程序员主动保证线程安全(其内部已经用synchronized、lock保证了线程安全,不需要程序员编程时主动加锁);如果将程序员主动加锁实现线程安全的HashMap比作是一辆加起来总共只有一扇门的十七节火车,所有人进出都只能走这扇门,同一时刻只能有一个人进出,其余人只能等待,这就会造成排很长的队,效率非常低;同样的,ConcurrentHashMap也可以比喻为一辆拥有使其节车厢的火车,不过这两火车的每一节车厢都拥有属于自己的一扇门。人们进出时,只需要找到自己要进出的车厢,在那个车厢的门外面排队就行。这就是分段锁,将一整个分为各个小段,每段都有自己的锁,对于不同的小段可以并发读写等。一个线程对小段A的操作,并不影响另一个线程对小段B的操作,此时是可以同时进行的;一个线程正在对小段C进行操作,另一个线程同时也想对小段C进行操作的话,那么此时就需要等待了。
注:ConcurrentHashMap的核心即为采用分段锁实现线程安全并且高效率的。
HashMap与ConcurrentHashMap安全性、效率比较测试:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
/**
* ConcurrentHashMap与HashMap测试比较
*
* @author JustryDeng
* @date 2018/10/24 0:22
*/
public class ConcurrentHashMapDemo {
private static int threadNum = 8000;
/** 为保证不干扰,fa1()-fa4()各自使用一个 */
private static Map<String, Object> hashMap1 = new HashMap<>(16);
private static Map<String, Object> concurrentHashMap2 = new ConcurrentHashMap<>(16);
private static Map<String, Object> hashMap3 = new HashMap<>(16);
private static Map<String, Object> concurrentHashMap4 = new ConcurrentHashMap<>(16);
/** 为保证不干扰,fa1()-fa4()各自使用一个 */
private static CyclicBarrier cyclicBarrier1 = new CyclicBarrier(threadNum);
private static CyclicBarrier cyclicBarrier2 = new CyclicBarrier(threadNum);
private static CyclicBarrier cyclicBarrier3 = new CyclicBarrier(threadNum);
private static CyclicBarrier cyclicBarrier4 = new CyclicBarrier(threadNum);
/** 为保证不干扰,fa1()-fa4()各自使用一个 */
private static CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);
private static CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);
private static CountDownLatch countDownLatch3 = new CountDownLatch(threadNum);
private static CountDownLatch countDownLatch4 = new CountDownLatch(threadNum);
/** fa3()会使用到的竞争锁对象 */
private static final Object lock = new Object();
/**
* HashMap安全性测试
* 注:由于HashMap并不安全,运行此方法时,除了不一定能得到理想的结果外,还可能会出现ClassCastException等异常
*/
private static void fa1() throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) {
final int index = i;
executorService.execute(() -> {
try {
cyclicBarrier1.await();
hashMap1.put("key" + index, "value" + index);
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} finally {
countDownLatch1.countDown();
}
});
}
executorService.shutdown();
countDownLatch1.await();
System.out.println("fa1() -> hashMap1尺寸为:" + hashMap1.size());
}
/**
* ConcurrentHashMap安全性测试
*/
private static void fa2() throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) {
final int index = i;
executorService.execute(() -> {
try {
cyclicBarrier2.await();
concurrentHashMap2.put("key" + index, "value" + index);
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} finally {
countDownLatch2.countDown();
}
});
}
executorService.shutdown();
countDownLatch2.await();
System.out.println("fa2() -> concurrentHashMap2尺寸为:" + concurrentHashMap2.size());
}
/**
* HashMap性能测试
*/
private static void fa3() throws InterruptedException {
long startTime = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) {
final int index = i;
executorService.execute(() -> {
try {
cyclicBarrier3.await();
synchronized (lock) {
hashMap3.put("key" + index, "value" + index);
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} finally {
countDownLatch3.countDown();
}
});
}
executorService.shutdown();
countDownLatch3.await();
long endTime = System.currentTimeMillis();
System.out.print("fa3() -> hashMap3尺寸为:" + hashMap3.size());
System.out.println("\t 耗时:" + (endTime - startTime) * 1.0 / 1000 + "秒!");
}
/**
* ConcurrentHashMap性能测试
*/
private static void fa4() throws InterruptedException {
long startTime = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) {
final int index = i;
executorService.execute(() -> {
try {
cyclicBarrier4.await();
concurrentHashMap4.put("key" + index, "value" + index);
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} finally {
countDownLatch4.countDown();
}
});
}
executorService.shutdown();
countDownLatch4.await();
long endTime = System.currentTimeMillis();
System.out.print("fa4() -> concurrentHashMap4尺寸为:" + concurrentHashMap4.size());
System.out.println("\t 耗时:" + (endTime - startTime) * 1.0 / 1000 + "秒!");
}
/**
* 主函数
*/
public static void main(String[] args) throws InterruptedException {
System.out.println("[线程安全] ---> 测试");
fa1();
fa2();
System.out.println();
System.out.println("[效率] ---> 测试");
fa3();
fa4();
}
}
运行主函数,(某次)输出结果为:
说明:多次运行主函数,会发现绝大多数结果都是类似于上面这样的。从效果上来讲,在多线程且(如有
必要,通过硬编程保证)线程安全的情况下,HashMap是低效的;而ConcurrentHashMap是高效的。
ConcurrentHashMap应用示例之多线程读取多个excel文件并以ConcurrentHashMap为容器存储数据:
准备工作
第一步:先在pom.xml中引入需要的依赖
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
第二步:准备两个excel
现有excelA:
现有excelB:
我们开两个线程同时读取这两个excel的数据:
/**
* 应用示例
* 之
* 多线程读取多个excel文件并以ConcurrentHashMap存储数据
*
* 注:重点是在多线程并发读取数据并操作同一个ConcurrentHashMap实例,所以这里Excel导入就简化处理了;
* 对excel导入感兴趣的可以去看我的这篇博客 https://blog.csdn.net/justry_deng/article/details/82833508
*
* @date 2018/10/25 10:54
*/
class ApplicatioExample {
/** 以ConcurrentHashMap作为数据容器 */
private static ConcurrentHashMap<String, List<Object>> concurrentHashMap = new ConcurrentHashMap<>(16);
/** 线程数 */
private static int threadNum = 2;
/** 要导入的excel文件 */
private static File[] files = new File[threadNum];
/** 倒计时锁 */
private static CountDownLatch countDownLatch = new CountDownLatch(threadNum);
/**
* 多线程时推荐使用实现Callable<>接口的方式,这里为了快速示例,就简单处理了
*/
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
files[0] = new File("C:/Users/JustryDeng/Desktop/excelA.xlsx");
files[1] = new File("C:/Users/JustryDeng/Desktop/excelB.xlsx");
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
for (int i = 0; i < threadNum; i++) {
final int index = i;
executorService.execute(() -> {
try {
readExcel(files[index]);
} catch (IOException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
executorService.shutdown();
countDownLatch.await();
long endTime = System.currentTimeMillis();
for (Map.Entry<String, List<Object>> entry :concurrentHashMap.entrySet()) {
System.out.println(entry.getKey() + "\t" + entry.getValue());
}
System.out.println("多线程读取多个excel文件结束!\t 耗时:" + (endTime - startTime) * 1.0 / 1000 + "毫秒!");
}
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
/**
* excel读取(只读取第一个Sheet)
*/
public static void readExcel(File excelFile) throws IOException {
String fileName = excelFile.getName();
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
Workbook wb = null;
try {
if ("xls".equals(suffix)) {
wb = new HSSFWorkbook(new FileInputStream(excelFile));
} else if ("xlsx".equals(suffix)) {
wb = new XSSFWorkbook(new FileInputStream(excelFile));
} else {
throw new IllegalArgumentException("Invalid excel version");
}
Sheet sheet = wb.getSheetAt(0);
List<Object> rowDataList = new ArrayList<>(8);
int totalRowCount = sheet.getPhysicalNumberOfRows();
int alreadyReadRowTotal = 0;
for (int i = sheet.getFirstRowNum(); alreadyReadRowTotal < totalRowCount; i++) {
Row row = sheet.getRow(i);
if (row == null || row.getFirstCellNum() < 0) {
continue;
}
int readColumnCount = (int) row.getLastCellNum();
// 以excel每一行的第一列的cell值为key,对应的行数据集合为value
Object keyFlag = getCellValue(row.getCell(0));
List<Object> rowValue = new ArrayList<>();
for (int k = 0; k < readColumnCount; k++) {
Cell cell = row.getCell(k);
rowValue.add(getCellValue(cell));
}
if (keyFlag != null) {
// 将数据放入ConcurrentHashMap中
concurrentHashMap.put(keyFlag.toString(), rowValue);
}
alreadyReadRowTotal++;
}
} finally {
if (wb != null) {
wb.close();
}
}
}
private static Object getCellValue(Cell cell) {
Object value = null;
if (cell != null) {
CellType ct = cell.getCellTypeEnum();
if (ct == CellType.STRING) {
value = cell.getStringCellValue();
} else if (ct == CellType.NUMERIC) {
if(HSSFDateUtil.isCellDateFormatted(cell)) {
value = simpleDateFormat.format(cell.getDateCellValue());
return value;
}
cell.setCellType(CellType.STRING);
value = cell.getStringCellValue();
} else if (ct == CellType.BOOLEAN) {
value = cell.getBooleanCellValue();
} else if (ct == CellType.BLANK) {
// 如果Cell中无内容,则设置其值为null;
value = null;
} else {
value = cell.toString();
}
}
return value;
}
}
提示:上面的代码中,很多地方都是简化写的,如果对excel导入感兴趣的可以去看我的这
篇博客 https://blog.csdn.net/justry_deng/article/details/82833508。
运行主函数,输出结果为:
CopyOnWriteArrayList
CopyOnWriteArrayList原理简述(举例说明):
假设ArrayList是一个一期上线了的项目,现在同时有开发者在一期基础上二期开发、同时用户在使用这个开发者正开发着的二期的项目,那么此时使用者使用时可能就会出现问题。即:多线程多同一资源同时操作时很可能会出问题;同样的假设CopyOnWriteArrayList是一个一期上线了的项目。现在开发者要进行二期开发,那么开发者会单独的将已经一起开发完了的项目复制一份(CopyOnWriteArrayList在写时,会采用副本加锁的方式来实现),在这个副本上开发,这时,如果用户要使用的这个项目,那么其使用的将会还是那个一期的项目(而不是开发者正在开发的项目)(CopyOnWriteArrayList在读时,读取的还是原来的那个,读的时候是不加锁的),当开发者二期开发完毕之后,会将项目上线,这时用户使用这个项目时,就使用的是新的(二期上线后的)了。
注:CopyOnWriteArrayList更像是一种读写分离的实现,比较适合用于写耗时较长的场景等。
注:COW只能保证最终数据的准确性,并不能保证即时性(因为其是写完之后才刷新到缓存的)。
给出源码中的两个典型方法(更多详见源码):
ArrayList与CopyOnWriteArrayList安全性简单测试:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* CopyOnWriteArrayListD使用示例
*
* @author JustryDeng
* @date 2018/10/23 12:26
*/
public class CopyOnWriteArrayListDemo {
private static List<Integer> nonSafeList = new ArrayList<>(4);
private static List<Integer> safeList = new CopyOnWriteArrayList<>();
private static CountDownLatch countDownLatch1 = new CountDownLatch(10000);
private static CountDownLatch countDownLatch2 = new CountDownLatch(10000);
/**
* 多线程时不安全的类ArrayList测试
*
* @date 2018/10/23 12:35
*/
private static void fa1() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10000; i++) {
final int index = i;
executorService.execute(() -> {
try {
nonSafeList.add(index);
} finally {
countDownLatch1.countDown();
}
});
}
executorService.shutdown();
countDownLatch1.await();
System.out.println(nonSafeList.size());
}
/**
* 多线程时安全的类CopyOnWriteArrayList测试
*
* @date 2018/10/23 12:35
*/
private static void fa2() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10000; i++) {
final int index = i;
executorService.execute(() -> {
try {
safeList.add(index);
} finally {
countDownLatch2.countDown();
}
});
}
executorService.shutdown();
countDownLatch2.await();
System.out.println(safeList.size());
}
/**
* 主函数
*
*/
public static void main(String[] args) throws InterruptedException {
fa1();
fa2();
}
}
运行主函数,控制台(某次)输出结果为:
注:多次运行,会发现fa2()总是输出的是正确的值10000,而fa1()输出的几乎总是错误的小于10000的值,由此
可见CopyOnWriteArrayList的线程安全性。
COW小拓展:在循环中删除元素
我们知道,如果直接在foreach循环中删除ArrayList集合中的元素的话,会导致ConcurrentModificationException等异常,如:
如果直接在fori循环中删除ArrayList集合中的元素的话,会导致数组下标越界异常或删除元素和我们要删除的元素不一致等异常,如:
提示:无论任何场景,都不推荐在fori循环中删除元素。
以前如果我们要在遍历时删除,我们可以通过Iterator迭代器来删除,如:
输出结果为:
现在了解了COW的副本机制后,我们也可以使用COW来删除,如:
输出结果为:
笔者将本人多线程一栏中博客涉及到的所有代码示例(Lock分开放在一个专门的项目、synchronized的代码附在该文章末尾),放在GIT上了(链接见本文末),这里先给出一个所涉及内容图:
声明:本文是学习笔记,主要学习自以下视频