一. JUC介绍
在java 5 中提供了java.util.concurrent包(普遍简称JUC),改包中包含在并发编程中常用的实用工具类,用于定义类似于线程的自定义子系统,包含线程池,异步IO(NIO)和轻量级任务框架.并还提供了设计用于多线程上下文的Collection实现等.
二. 内存可见性
在实际使用线程时,会存在当某个线程正在使用对象的成员变量而另一个线程在同时改变该状态,而此时被修改的成员变量的值无法被正在使用的线程的正确的读取到的问题.(该状况被称为可见性错误)
内存可见性是指在俩个或多个线程对同一对象的成员变量,是否能够确保当一个线程修改了对象的成员变量后,其他线程能够看到发生的成员变量的变化.
解决:
- 加锁(synchronized)(添加在需要获取数据代码上)
- 问题:效率过低,高耦合
- 给成员变量加volatile关键字
- 优势:效率较高(无法进行jVM底层的重排序),低耦合
三. volatile关键字
是java提供的一种较弱的同步机制,可以确保将变量的更新操作通知到其他线程.虽然volatile可以看做一个轻量级的锁,但是又于锁有不同.
- 对于多线程,不存在互斥关系(并不是在同一时间内只有一个线程可以获得被修饰的变量)
- 不能保证变量的原子性
普通变量的原子性问题:
- 在使用i++等操作时,底层是分为三步进行赋值的,先将值取出再进行+1操作,然后赋值给原变量,是分布操作的,那么在多线程情况下就可能出现在未执行完时,被另一线程进行操作.出现变量与预计情况不一致问题.(例如:一个是线程是i++,而另一也是线程是i++,就出现明明结果应该是2,而实现结果为1的情况)
四. 变量的原子性
其主要的是指原子操作,原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。通常所说的原子操作包括对非long和double型的primitive(基本类型)进行赋值,以及返回这两者之外的primitive。之所以要把它们排除在外是因为它们都比较大,而JVM的设计规范又没有要求读操作和赋值操作必须是原子操作
五. CAS算法
CAS(Compare-And-Swap)是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问.
-
是一种无锁的非阻塞算法的实现
-
包含3个操作数:
- 需要读写的内存值V
- 进行比较的预期值A
- 拟写入的新值B
-
当且仅有V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作.
六. 原子变量
一般我们在程序中修改一个代码的值会分为3步:读取==> 修改 ==> 回写。在允许多线程的程序中而对于某一资源我们只希望有一个线程操作它,我会想使用全局变量去作为标记变量。两个线程A、B,当A读取完该全局变量后正在修改它的值,而这时B进程正在读取该全局变量,对于B进程而言此全局变量的值和A进程是一样,这样A、B都可以正常进行,就与我们的意愿相违了,所以我们会想是不是可以定义一个变量,将对一个变量值的读取、修改、回写变成一个不可打断的操作,于是我们就有了原子变量。
java.util.concurrent.atomic包下提供了一些原子操作的常用类
- AtomicBoolean 、AtomicInteger 、AtomicLong 、 AtomicReference (常用基本类型对应的原子变量)
- AtomicIntegerArray 、AtomicLongArray (对应的数组)
- AtomicMarkableReference (对象引用原子变量)
- AtomicReferenceArray (对象引用原子变量数组)
- AtomicStampedReference(带有时间戳的对象引用原子变量)
支持在单个变量上解除锁的线程安全编程。事实上,此包中的类可将 volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类。
类 AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。
AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问,这对于普通数组来说是不受支持的。
存在核心方法:
- boolean compareAndSet(expectedValue, updateValue)
- expectedValue 预计值
- updateValue 修改值
当然原子变量内部使用了CAS算法保证原子性与volatile保证内存可见性,有效的解决了变量的原子性问题与内存可见性问题.
七. ConcurrenHashMap
在java 5.0 中的java.util.concurrent包中提供了多种并发容器类来改进同步容器的性能,而ConcurrentHashMap同步容器类是一个线程安全的哈希表.对于多线程的支持,性能介于HashMap与Hashtable之间.在jdk7包括jdk7以上版本内部采用"锁分段"机制,优于Hashtable的独占锁.提高了性能.而在jdk8后采用CAS算法与synchronized机制进一步优化性能.
在java.util.concurrent包还提供了用于多线程的上下文的Collection实现:
- ConcurrentHashMap(当期望许多线程访问一个给 定 collection 时,优于HashMap)
- ConcurrentSkipListMap(当期望许多线程访问一个给 定 collection 时,优于TreeMap)
- ConcurrentSkipListSet(以上类似)
- CopyOnWriteArrayList (当期望的读数和遍历远远 大于列表的更新数时,同步的 ArrayList)
- CopyOnWriteArraySet(以上类似)
CopyOnWriteArrayList 在进行添加操作时,效率低所以才有上述条件,而原因是在每次添加时都会进行集合复制操作,系统开销非常大,所以建议在进行并发迭代操作多时使用
.
八. CountDownLatch 闭锁
java 5.0 在java.uril.concurrent包中提供了多种并发容器类改进容器的性能.而CountDownLatch是一个同步辅助类,可以在完成一组正在其他线程中的执行的操作之前,它允许一个或多个线程一直等待.
闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行.(例如)
- 确保某个计算在需要的所有资源都被初始化之后才继续执行
- 确保某个服务在其依赖的所有其他服务都启动之后才启动(例如多个微服务)
- 等待直到某个操作所有参与者都准备就绪再继续执行(多个线程同时计算)
例子:
import java.util.concurrent.CountDownLatch;
/**
* @author 嘿嘿嘿1212
* @version 1.0
* @date 2020/1/8 20:13
* 计算多线程的执行时间
*/
public class TestCountDownLatch {
public static void main(String[] args) {
//创建闭锁,并指定线程数
final CountDownLatch latch = new CountDownLatch(5);
LatchDemo ld = new LatchDemo(latch);
long start = System.currentTimeMillis();
for (int i = 0; i < 5; 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;
}
@Override
public void run() {
try {
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
} finally {
//闭锁计算减一
latch.countDown();
}
}
}