常用辅助类
juc中有三个比较常用的辅助类,属于同步类。
1.CountDownLatch
这个辅助类是一个同步工具类,适用于某个线程可以一直等待不运行,知道几个线程执行完之后才运行。一般适用于主线程需要等某几个线程执行完后,再执行主线程。
例如减法器,如果线程运行完,便会减一,当所有线程都运行完,计数器会变成0,计数器变成0之后,才让主线程运行完。代码如下
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch =new CountDownLatch(10);//设置需要等待线程的个数
for (int i = 1; i <=10 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" "+"end");
countDownLatch.countDown();//每执行一个线程,就将计数器减一
}).start();
}
countDownLatch.await();//await只有到计数器为0,才不会阻塞。
System.out.println(Thread.currentThread().getName()+" "+"end");
}
CyclicBarrier
这个辅助类跟上面的CountDownLatch恰恰相反,是一个加法计其数。根据方法名,我们可形象的理解,barrier在中文译为栅栏,其实跟墙的作用一样,就是计数器达不到某个值,就会一直阻塞,每达到某个值就会执行某个线程中指定的操作。
例子:集齐7颗龙珠召唤神龙。即没获得一个龙珠,就加一,直到集齐。如果我们设置14个线程,线程会召唤两次神龙。7个线程的话就召唤一次神龙。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier =new CyclicBarrier(7,()->{
System.out.println("召唤神龙!");
});
for (int i = 1; i <=14 ; i++) {
final int tempInt=i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" "+"获得第"+tempInt+"个龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
这个方法适合用于抢车位。
如小区有3个车位,有6辆车,这样的话,肯定车位是先来先得,多的车只有当其他车出车库了才能入。(其实也可以类比,英雄联盟排位机制,人多的时候,限制排队的玩家数)
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore =new Semaphore(3); //三个车位
//六个线程模拟六个车子
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
try {
semaphore.acquire(); //车子进车库
System.out.println(Thread.currentThread().getName()+" "+"进");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+" "+"出");
semaphore.release();//车子出车库
}
}).start();
}
}
}
即入车库就调用acquire(),车库数减1,出车库relese()车库加1
JMM
java memory model (Java内存模型)是一个纯理论。其描述的都是线程是如何工作的。
内存交互操作有8种。即lock, unlock, read, loads, use,assign,store,write.
具体解释
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
一幅图解释
下图是三个线程工作的情况,每个线程都有自己的java内存空间,主内存由所有分线程共享。
注意:从主内存读到java线程内存的过程经过两步,先读(read),然后再加载(load),写回内存也是两步,先store后write
Volatile
Volatile作为java JUC必问知识点之一,是JUC的重点。
Volatile的内容还有原子类的一些知识点都围绕着volatile三个特性展开,即非原子性,可见行,禁止指令重排。
可见性
可见性:指的是某个线程修改了内存内容,其他线程是可以看见的。
import java.util.concurrent.TimeUnit;
public class VolatileDemo {
private volatile static int num=0;
public static void main(String[] args) {
for (int i = 1; i <=3 ; i++) {
new Thread(()->{
while(num==0){
}
}).start();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
}
如果不可见的话,上述三个线程会一直以为num=0,一直运行while不结束,但是事实是程序运行了一会就结束了,因为主函数修改了num,其他线程都看见了,所以while就结束了。
非原子性
即valatile关键字对非原子性操作,并不保证其安全性。如对1000个线程进行num++操作,其结果运行不会是2000;因为num++不是原子性操作。
如果不使用lock,synchorize锁的话如何解决上述问题。可以用原子类相关操作,后面原子类整个家族就是来解决非原子性的问题的
禁止指令重排
指令重排:指的是你写的程序不一定是你的程序跑的顺序.
源代码->编译器(优化重排)->指令并行重排->内存系统重排->最终执行!
如:
int x=1 (1)
int y=2 (2)
x=x+5 (3)
y=x*x (4)
很明显三这个操作和2没有依赖关系,顺序可以互换,在操作系统底层可能考虑某方面的性能,执行顺序可能会变换。
volatile可以禁止指令重排!
volatile如何禁止指令重排呢?volatile指令重排在底层实现是通过内存屏障实现。是一种CPU指令。内存屏障作用如图:
单例模式
常见的单例模式要很熟练才行。
懒汉式和饿汉式
// 单例的思想: 构造器私有
public class Hungry {
// 浪费空间
private byte[] data = new byte[10*1024*1024];
private Hungry(){}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstace(){
return HUNGRY;
}
}
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "111");
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
// 多线程下单例失效
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
但是在juc中单例模式就不会产生作用。
因为单例模式中new一个对象的操作也不是原子性操作,所以在并行过程中单例模式的懒汉式没作用了。
所以DCL懒汉式就出来。
DCL懒汉式,即加锁,双重判断。
package com.coding.single;
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "111");
}
private volatile static LazyMan lazyMan;
// DCL
public static LazyMan getInstance() {
if (lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan(); // 请你谈谈这个操作!它不是原子性的
// java创建一个对象
// 1、分配内存空间
// 2、执行构造方法,创建对象
// 3、将对象指向空间
// A 先执行13,这个时候对象还没有完成初始化!
// B 发现对象为空,B线程拿到的对象就不是完成的
}
}
}
return lazyMan;
}
单例模式之所以安全,是因为构造器私有化。但是私有化构造器,不一定安全。
可以利用反射进行攻击。先拿到类,通过类得到构造器,再通过构造器可以获得对象,不过可以通过枚举类保障反射的安全性。
package com.coding.single;
import com.sun.org.apache.bcel.internal.generic.DDIV;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
// enum 是什么, enum 枚举一个是一个类
public enum SingleEnum {
INSTANCE;
public SingleEnum getInstance(){
return INSTANCE;
}
}
// 至少在做一个普通的JVM时候,jdk源码没有被修改的时候,枚举就是安全的!
class Demo{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
// throw new IllegalArgumentException("Cannot reflectively create enum objects");
SingleEnum singleEnum1 = declaredConstructor.newInstance();
SingleEnum singleEnum2 = declaredConstructor.newInstance();
System.out.println(singleEnum1.hashCode());
System.out.println(singleEnum2.hashCode());
// 这里面没有无参构造! JVM 才是王道!
// "main" java.lang.NoSuchMethodException: com.coding.single.SingleEnum.<init>()
}
}
CAS
CAS是compare and swap比较并交换的英文缩写。是原子类的实现原理。
可以解决vovatile非原子性的缺点。
其实CAS正如它名字一样简单,就是比较并交换两个操作。
即多线程在操作的过程中时,原子类变量,在修改变量值前,需要进行比较内存中的值,是不是等于修改前的值,如果是就修改值,如果不等于,说明已经被别人修改了,线程就放弃修改。
原子类最底层都是采用CAS方法,调用JNI C++取操作底层硬件来保证原子性的。
CAS源码:
do { // 自旋锁(就是一直判断!)
// var5 = 获得当前对象的内存地址中的值!
var5 = this.getIntVolatile(this, valueOffset); // 1000万
// compareAndSwapInt 比较并交换
// 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
// 如果是期望的值,就交换,否则就不交换!
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
就是通过一个while循环语句进行的,一直while判断是否和自己预期相等,相等就操作,不相等就一直等下去。其实也相当于一个自旋锁
CAS缺点:1.循环开销大
2.内存操作,每次只能保证一个共享变量原子性。
3.出现ABA问题
原子引用
原子引用就是为了解决ABA问题,ABA问题其实就是原子类在运行过程中只关注内存中的值是否发生变换,变换了才被发现,没被变换就认为是安全的。但是没被变换就一定安全吗?
其实不然,内存中的数据如果没变可能是被人用了之后又换回来的结果。
例如:内存数值为100,甲没用,乙这个时候将100改成102后又改成100还回来,其实甲没发现,甲继续使用。100(A)->102(B)->100(A)这就是ABA问题。
通俗的说,你的100万块钱放在银行,你没动它,但是有一个很精明的人知道你的银行密码,他把你钱偷偷拿去投资,赚了很多很多的钱之后,把你的100万还到银行,到你缺钱去银行取钱的时候你发现钱总数没变,你美滋滋的走了。但是这个时候如果你知道,这钱的过程,你就会觉得很不安全,很亏,没人愿意这样。银行的流程大家都知道,如果银行不给你利息而且可能倒闭,你会存钱到银行吗?肯定不会。
言归正传,操作系统肯定不会给你利息这种东西,但是为了保证钱的安全性,我们就提取了时间戳的概念,就是给每个变量加一个时间戳。用的时候不仅比较变量的值,而且还要比较数据的时间。这个加了时间戳的类就是原子引用。
static AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1); //这里的100是变量值,1是时间戳