从开始学架构(6)--juc(下)

常用辅助类

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是时间戳
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值