初识并发编程【3】之JUC
本文源自Recently祝祝,创自Recently祝祝。转载请标注出处。
此解决方式在企业中有所应用,适合Java初级开发学习,参考。
本文字数与行数,耐心阅读必有收获。
1.JUC说明
JUC就是java的一个包java.util.concurrent的简称,java中用于并发编程常用的一个工具包,具有强大的功能,能弥补synchronized缺陷,synchronized不能够设置阻塞时长,JUC不仅仅可以设置超时时间而且还可以主动的中断线程。而且在读多写少的场合JUC可以有多种解决方式,而避免了synchronized加锁带来的开销。
JUC是解决多线程安全一个工具包,提供了非常好用的工具以及类,比如接下来要讲的原子类(Automic),锁(Lock)等解决一些不满足多线程特性的问题,提高程序性能和安全性,跟方便的管理与调度。JUC是Java并发编程不可或缺的部分。
2.JUC使用在哪里
JUC用于支持高效、安全的多线程编程。具体来说,JUC 包括线程池、原子类、锁(Lock)、并发集合、并发容器、阻塞队列
- 线程池:通过 Executor、ExecutorService、ThreadPoolExecutor 等类,实现线程池的创建、管理和调度,避免了线程频繁创建和销毁的开销,提高了多线程程序的效率。
- 原子类:Automic包,java.util.concurrent.automtic,通过 AtomicInteger(整数原子型)、AtomicLong(长整型原子类)、AtomicReference(引用类型原子类) 等类,提供线程安全的原子操作,用法简单性能高效,有效避免Volatie原子性操作变量的问题,避免了多线程并发情况下的数据竞争和安全问题。
- 并发集合:通过 ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentSkipListMap 等类,提供线程安全的集合操作,避免了多线程并发情况下的数据竞争和安全问题。
- 阻塞队列:通过 DelayQueue 类,实现基于时间的任务调度,支持在指定时间后执行任务,避免了使用 Thread.sleep() 的效率问题。
- 锁和条件变量:通过 Lock、Condition 等接口和实现类,提供更灵活、更高效的线程同步机制,支持更细粒度的锁控制。
3.JUC主要使用
3.1原子类
原子类可以提供线程安全的原子操作,避免多线程并发情况下的数据竞争和安全问题。
Aotumtic包共13个类,4种类型的原子更新方式(原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段))Atomic包里的类基本都是使用Unsafe实现的包装类。
举例:
public class VolatileNoAtomicity {
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 2; i++) {
Thread t = new Thread(demo);
t.start();
}
Thread.sleep(1000);
System.out.println("count = "+demo.count);
}
static class VolatileDemo implements Runnable {
public volatile int count;
//public volatile AtomicInteger count = new AtomicInteger(0);
public void run() {
addCount();
}
public void addCount() {
for (int i = 0; i < 10000; i++) {
count++;//但是实际情况是三条汇编指令
}
}
}
}
你会发现结果不是20000,因为volatile不具备原子性。
原因分析:
-
线程1读取count的值为5
-
线程2读取count的值为5
-
线程2加1操作
-
线程2最新count的值为6
-
线程2写入值到主内存的最新值为6
-
线程1执行加1count=6,写入到主内存的值是6。
-
结果:对count进行了两次加1操作,主内存实际上只是加1一次。结果为6
-
这样的情况出现多次之后,就会放结果与预期结果不一致。
解决使用原子操作:
你就会发现,结果变成了20000,这就是原子类的保证了原子性。
3.2什么是CAS
CAS(Compare and Swap:比较替换)一种无锁算法(乐观锁机制),可以保证在多线程并发情况下对变量的操作是原子性的。同步组件中大量使用CAS技术实现了Java多线程的并发操作。整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。
CAS 的基本思想是比较当前内存中的值和期望值是否相等,如果相等,则执行操作并更新内存中的值,否则不执行任何操作。本质一个方法调用一行CPU原子操作执行函数:CAS(V,E,N)
当且仅当内存地址V中的值等于 预期值E 时,将内存V中的值改为N,进行暂停一会儿。
3.3Lock锁
Lock 锁,锁所锁住的是一个对象显式锁机制,与 synchronized 关键字相对应,使用起来粒度更细致,并且更灵活,可以实现公平锁、非公平锁、可重入锁等,同时还可以实现多个条件变量等高级功能。可以实现异步变同步的转换。
3.4什么是AQS
AQS是一个并发基础框架类,用于构建lock、同步器等的底层框架模型。AQS是一个队列,为了让线程来排队,其中每个节点表示一个等待线程,当一个线程请求锁资源时,如果锁已被占用,该线程将被加入到队列中等待,称为线程同步器。本身并不是一种具体的同步器,而是一个提供了同步器实现所需的底层机制的抽象类。实现自定义服务器,也可以使用AQS提供的一些模板来实现同步器的基本功能。
3.5JUC工具类
线程协作工具类,控制线程协作的工具类,让线程之间的协作变得更加简单
1) CountDownLatch倒数门栓
倒数结束之前,一直处于等待状态,直到数到0结束,此线程才继续工作。
场景:购物拼团,大巴人满发车,分布式锁
主要方法:
- 构造函数:new CountDownLatch(int count):只有一个构造函数,参数count为需要倒数的数值。
- await():当一个或多个线程调用await()时,这些线程会阻塞。
- countDown():其他线程调用countDown()会将计数器减1,调用countDown方法的线程不会阻 塞。当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
/**
* CountDownLatch案例:6个程序猿加班
* 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
*/
public class Demo11CountDownLatch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try { TimeUnit.SECONDS.sleep(5); } catch
(InterruptedException e) {e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "\t上
完班,离开公司");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
new Thread(()->{
try {
countDownLatch.await();//卷王也是有极限的,设置超时时间
System.out.println(Thread.currentThread().getName()+"\t卷王最
后关灯走人");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "7").start();
}
}
2) Semaphore信号量:限流
用来限制或管理数量有限资源的使用情况。 (雪崩)
场景:Hystrix、Sentinel限流
信号量的作用就是维护一个”许可证”的计数,线程可以”获取”许可证,那信号量剩余的许可证就减少一个,线程也可以”释放”一个许可证,那信号量剩余的许可证就可以加一个。当信号量拥有的许可证数为0时,下一个还要要获取许可证的线程就需要等待,直到有另外的线程释放了许可证。
主要方法: 核心方法不多
- 构造函数:
new Semaphore(int permits,Boolean fair)
:可以设置是否使用公平策略,如果传入true,则Semaphore
会把之前等待的线程放到FIFO队列里,以便有了新许可证可以分给之前等待时间最长 的线程。 acquire()
:获取许可证,当一个线程调用acquire操作时,他要么通过成功获取信号量(信号量减 1),要么一直等待下去,直到有线程释放信号量,或超时。release()
:释放许可证,会将信号量加1,然后唤醒等待的线程。
/**
* Semaphore案例:三辆小汽车抢车位
* Semaphore信号量主要作用:1.用于多个共享资源的互斥使用,2.用于并发线程数的控制
*6个线程抢3个车位
*/
public class Demo12Semaphore {
public static void main(String[] args) {
//模拟资源类,有3个空车位
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try{
//占有资源
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t
抢到车位");
try { TimeUnit.SECONDS.sleep(3); } catch
(InterruptedException e) {e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t
停车3秒后离开车位");
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放资源
semaphore.release();
}
}, "Thread-Car-"+String.valueOf(i)).start();
}
}
}
3)CyclicBarrier循环栅栏
线程会等待,直到线程到了事先规定的数目,然后触发执行条件进行下一步动作
场景:并行计算
当有大量线程互相配合,分别计算不同任务,并且需要最后统一汇总时,就可以用CyclicBarrier,它可以构造一个集结点,当某一个线程执行完,它就会到集结点等待,直到所有线程都到集结点,则该栅栏 就被撤销,所有线程统一出再,继续执行剩下的任务。
主要方法:
-
构造函数:new CyclicBarrier(int parties, Runnable barrierAction),设置聚集的线程数量和集齐线程 数的结果之后要执行的动作。
-
await():阻塞当前线程,待凑齐线程数量之后继续执行
import java.util.concurrent.CyclicBarrier;
/**
* 案例:集齐7龙珠召唤神龙
*/
public class Demo13CyclicBarrier {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("======召唤神龙");
});
for (int i = 1; i <= 7; i++) {
final int tempInt = i;
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() +
"\t收集到第" + tempInt + "颗龙珠");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() +
"\t第" + tempInt + "颗龙珠飞走了");
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-"+String.valueOf(i)).start();
}
}
}
文章全为个人理解,如果发现部分跟你所知道的有出入,欢迎在评论区指出,欢迎探讨
转载自:https://juejin.cn/post/7231362257337237559