目录
1 概述
在java5.0中提供了java.util.current包(简称juc),中提供了许多在并发编程工程中常用的工具类,用于定义类似于线程的自定义子系统,包括线程池、异步IO、轻量级框架、还提供了多线程上下文的Collectiion的实现。
指java.util包下的三个工具类
java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks
实现多线程共有三种方式,我们传统的是:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
使用实现Runnable几口的方式,代码没有返回值,效率相比较于实现Callable也比较低
2 JUC结构

1,tools(工具类):又叫信号量三组工具类,包含有
1)CountDownLatch(闭锁) 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
2)CyclicBarrier(栅栏) 之所以叫barrier,是因为是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 ,并且在释放等待线程后可以重用。
3)Semaphore(信号量) 是一个计数信号量,它的本质是一个“共享锁“。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
2,executor(执行者):是Java里面线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService,里面包含的类有:
1)ScheduledExecutorService 解决那些需要任务重复执行的问题
2)ScheduledThreadPoolExecutor 周期性任务调度的类实现
3,atomic(原子性包):是JDK提供的一组原子操作类,
包含有AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,他们的实现原理大多是持有它们各自的对应的类型变量value,而且被volatile关键字修饰了。这样来保证每次一个线程要使用它都会拿到最新的值。
4,locks(锁包):是JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁包含的实现类有:
1)ReentrantLock 它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。
2)ReentrantReadWriteLock 它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。
3)LockSupport 它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。
5,collections(集合类):主要是提供线程安全的集合, 比如:
1)ArrayList对应的高并发类是CopyOnWriteArrayList,
2)HashSet对应的高并发类是 CopyOnWriteArraySet,
3)HashMap对应的高并发类是ConcurrentHashMap等等
3 volatile 关键字
- volatile 关键字: 当多个线程进行操作共享数据时,可以保证内存中的数据是可见的;相较于 synchronized 是一种
较为轻量级的同步策略; - volatile 不具备"互斥性";
- volatile 不能保证变量的"原子性";
// 使用 volatile 之前
public class TestVolatile{
public static void main(String[] args){
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while(true){
if(td.isFlag()){
System.out.println("########");
break;
}
}
}
}
class ThreadDemo implements Runnable{
private boolean flag = false;
public void run(){
try{
// 该线程 sleep(200), 导致了程序无法执行成功
Thread.sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
flag = true;
System.out.println("flag="+isFlag());
}
public boolean isFlag(){
return flag;
}
public void setFlag(boolean flag){
this.flag = flag;
}
}


// 解决问题方式一: 同步锁
// 但是,效率太低
public class TestVolatile{
public static void main(String[] args){
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while(true){
// 使用同步锁
synchronized(td){
if(td.isFlag()){
System.out.println("########");
break;
}
}
}
}
}
// 解决方式二: 使用 volatile 关键字
public class TestVolatile{
public static void main(String[] args){
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while(true){
if(td.isFlag()){
System.out.println("########");
break;
}
}
}
}
class ThreadDemo implements Runnable{
private volatile boolean flag = false;
public void run(){
try{
// 该线程 sleep(200), 导致了程序无法执行成功
Thread.sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
flag = true;
System.out.println("flag="+isFlag());
}
public boolean isFlag(){
return flag;
}
public void setFlag(boolean flag){
this.flag = flag;
}
}
4 i++ 的原子性问题
i++的操作实际上分为三个步骤: "读-改-写";- 原子性: 就是"i++"的"读-改-写"是不可分割的三个步骤;
- 原子变量: JDK1.5 以后,
java.util.concurrent.atomic包下,提供了常用的原子变量;- 原子变量中的值,使用
volatile修饰,保证了内存可见性; - CAS(Compare-And-Swap) 算法保证数据的原子性;
- 原子变量中的值,使用
int i = 10;
i = i++; // 此时, i=10
执行步骤:
int temp = i;
i = i + 1;
i = temp;
// 测试类
public class TestAtomicDemo{
public static void main(String[] args){
AtomicDemo ad = new AtomicDemo();
for(int i=0; i < 10; i++){
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable{
private int serialNumber = 0;
public void run(){
try{
Thread.sleep(200);
}catch(InterruptedException e){
}
System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
}
public int getSerialNumber(){
return serialNumber++;
}
}

// 改进: 使用原子变量
class AtomicDemo implements Runnable{
private AtomicInteger serialNumber = new AtomicInteger();
public void run(){
try{
Thread.sleep(200);
}catch(InterruptedException e){
}
System.out.println(Thread.currentThread().getName()+":"+getSerialNumber());
}
public int getSerialNumber(){
// 自增运算
return serialNumber.getAndIncrement();
}
}
现在jdk8已经改进
5 并发容器类
Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能;
ConcurrentHashMap
- ConcurrentHashMap 同步容器类是 Java5 增加的一个线程安全的哈希表;介于 HashMap 与 Hashtable 之间;
内部采用"锁分段"机制替代Hashtable的独占锁,进而提高性能; - 此包还提供了设计用于多线程上下文中的
Collection实现:ConcurrentHashMap,ConcurrentSkipListMapConcurrentSkipListSet,CopyOnWriteArrayList和CopyOnWriteArraySet;- 当期望许多线程访问一个给定collection时,
ConcurrentHashMap通常优于同步的HashMap;ConcurrentSkipListMap通常优于同步的TreeMap; - 当期望的读数和遍历远远大于列表的更新数时,
CopyOnWriteArrayList优于同步的ArrayList;
- 当期望许多线程访问一个给定collection时,
CountDownLatch(闭锁)
CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待;
// 测试类: 计算多线程的执行时间
public class TestCountDownLatch{
public static void main(String[] args){
final CountDownLatch latch = new CountDownLatch(10);
LatchDemo ld = new LatchDemo(latch);
long start = System.currentTimeMillis();
// 创建10个线程
for(int i=0; i<10; i++){
new Thread(ld).start();
}
try{
latch.await();
}catch(InterruptedException e){
}
long end = System.currentTimeMillis();
System.out.println("耗费时间为:"+(end - start));
}
}
class LatchDemo implements Runnable{
private CountDownLatch latch;
// 有参构造器
public LatchDemo(CountDownLatch latch){
this.latch = latch;
}
public void run(){
synchronized(this){
try{
// 打印50000以内的偶数
for(int i=0; i<50000; i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}finally{
// 线程数量递减
latch.countDown();
}
}
}
}
6 关于锁的八种现象
传统的线程模板
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("打电话");
}
}
只有当锁的对象或者模板是同一个的时候,才能够借助调用的顺序来执行。
7 不安全的集合类
List
List是不安全的集合类
如何将它变得安全?有如下几种方法:
- 将ArrayList集合变成Vector集合
- 使用Collections.synchronizedList( new ArrayList<>() )的方式创建集合
- 使用CopyOnWriteArrayList<>()
第三种方式为写入时复制。它相比较于Vector,效率更加高效,因为Vector底层使用的是synchronized锁,而在JDK1.8中CopyOnWriyteArrayList底层是Lock锁(不过JDK11中貌似使用的又是synchronized锁)
Set
Set也是不安全的集合类
同样,将不安全的集合变成安全集合的方法:
- 使用Collections.synchronizedList( new HashSet<>() )的方式创建集合
- 使用CopyOnWriteArraySet<>()
补充:Set的底层是什么?(后面我也会写一些关于这三个集合的底层源码)
//默认的空参初始化方法
public HashSet() {
map = new HashMap<>();
}
//使用HashSet的add方法,依然是调用HashMap的底层put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
即HashSet的底层就是HashMap
Map
Map也是不安全的集合类
将不安全的集合变成安全集合的方法:
- 使用Collections.synchronizedMap( new HashMap() )的方式创建集合
- 使用ConcurrentHashMap<>()
本文围绕Java的JUC(java.util.concurrent)包展开,介绍了其结构,包括工具类、执行者、原子性包、锁包和集合类等。还阐述了volatile关键字、i++原子性问题、并发容器类如ConcurrentHashMap,分析了锁的八种现象,最后给出将不安全集合类List、Set、Map变安全的方法。


1082

被折叠的 条评论
为什么被折叠?



