JUC与锁——精华篇

JUC

线程的安全性问题

需要考虑原子性、有序性、可见性

java内存模型

JMM内存规定,每个线程在创建时都有自己单独的工作内存,所有的变量都存储在主内存中,是共享的,线程首先读取主内存的变量到自己工作内存的变量副本上,对数据的修改只能在工作内存上进行,操作完成后再将新的变量写会主内存,各工作内存间不能直接交流。
JMM的三种特性:可见性---线程修改变量及时通知其余线程、原子性、有序性

volatile

volatile关键字、如何保证可见性、是否是强一致性

volatile是轻量级的同步机制,保证可见性、不保证原子性、禁止指令重排

保证可见性:volatile确保线程在操作变量之前,重新读取主内存中的变量值。
不保证原子性:会出现写覆盖的情况
如何保证原子性:1.使用sync 2.使用juc下atmic包下面的类

volatile为什么不保证原子性volatile如何保证可见性和有序性

volatile是如何做的禁止指令重排的

指令重排

对于volatile修改变量的读写操作,都会加入内存屏障。
每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile/写重排。
每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排。

禁止指令重排

happens-before原则规定了volatile修饰的变量的写一定在读前面

1、程序顺序规则
程序顺序原则,指的是在一个线程内,按照程序代码的顺序,前面的代码运行的结果能被后面的代码可见。(准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。)
2、传递性
传递性,指的是如果A Happens-BeforeBB Happens-BeforeC,则A Happens-BeforeC。这个是很好理解的。用白话说就是,如果A的操作结果对B可见,B操作结果对C可见,则A的操作结果对C也是可见的。
3volatile变量规则
指对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。如果单单是理解这句话的意思,就是我们熟悉的禁用cpu缓存的意思,使得volatile修饰的变量读到永远是最新的值。
4、锁规则
锁规则,指的是一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
5、线程start()规则
指的是主线程A启动子线程B后,子线程B能看到主线程在启动线程B前的操作。
6、线程join()规则
这个规则跟上一条规则有点类似,只不过这个规则是跟线程等待相关的。指的是主线程A等待子线程B完成(B线程join()调用),当子线程B操作完成后,主线程A能看到B线程的操作。
7、线程的interrupt()规则
指的是线程A调用线程Binterrupt()方法,Happens-Before 于线程B检测中断事件(也就是Thread.interrupted()方法)。这个也很容易理解
8finalize()规则
指的是对象的构造函数执行、结束 Happens-Beforefinalize()方法的开始。

volatile应用场景

单体结构中单例模式只需要加sync即可,单分布式架构中,有很多个jvm,该锁对象是不同的。

//单例模式
public class SingletoneDemo {
    private static volatile SingletoneDemo instance = null;
    //为什么使用volatile 修饰了singleton 引用还用synchronized 锁?
    //volatile 只保证了共享变量 singleton 的可见性,但是 singleton = new Singleton();
    // 这个操作不是原子的,可以分为三步:
    //步骤1:在堆内存申请一块内存空间;
    //步骤2:初始化申请好的内存空间;
    //步骤3:将内存空间的地址赋值给 singleton;

    //私有化构造方法
    private SingletoneDemo() {
    }
   //为什么进行DCL(Double Check)
    // A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),
    // B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,
    // 虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 	   可能会初始化二次。
    public static SingletoneDemo getInstance() {
        if (instance == null) {
            synchronized (SingletoneDemo.class) {
                if (instance == null) {
                    instance = new SingletoneDemo();
                }
            }
        }
        return instance;
    }
}

破坏单例的方式
反射:发现发射生成的一个新的对象 ,此时单例模式被破坏
序列化:通过先序列化再反序列化的方式,可获取到一个新的单例对象,这就破坏了单例。
克隆

如何防止破坏单例
防止反射	定义一个全局变量,当第二次创建的时候抛出异常
防止序列化	(在单例中加入readResolve方法,因为在反序列化执行过程中会执行到ObjectInputStream#readOrdinaryObject方法,这个方法会判断对象是否包含readResolve方法,如果包含的话会直接调用这个方法获得对象实例)
防止克隆	克隆破坏单例模式需要继承Colneable接口并且重写克隆方法

什么是MESI缓存一致性协议

synchronized

synchronized是如何保证可见性、原子性及有序性的

保证可见性是线程在释放锁之前,必须把工作内存中的值刷新到主内存中(fresh),获得锁之后,把工作内存中的变量清除,重新从主内存读取(refresh)。
保证原子性是线程在执行同步代码块时首先获得锁monitorenter,执行完释放锁monitorexit,有两次释放锁,是为了避免死锁。
有序性不代表禁止指令重排,sync锁住当前线程,每个线程单独执行,就可以保证有序性

synchronized可重入原理

可重入锁:线程可以进入任意一个它已经拥有锁的可重复代码块
作用是避免死锁
每个对象关联一个monitor,monitor有一个计数器,刚开始为0,对这个对象加锁,每次加锁之后指数都加1,释放锁则减1,当减到0的时候,其余线程可以抢占这个对象。

Synchronized和监视器(monitor)有什么关系?为什么Synchronized可以使用任意对象?

首先,每个对象都可以被认为是一个“监视器monitor”,对象里面有个指针指向monitor,这个监视器由三部分组成:独占锁、入口队列,等待队列。
注意:一个对象只能有一个独占锁,但是任意线程都可以拥有这个独占锁(说白了,独占锁就是一个标记)。
Synchronized需要获取对象锁,实际上就是获取的是对象中的独占锁,通过这个标记来判断是否已有线程进入占用(所以synchronized无论使用什么对象都可以,每个对象在堆中都有独占锁)。
而入口队列中放的则是要竞争锁资源的其他线程,如果线程使用了wait方法,则进入对象的等待列队中。

synchronized保证原子性synchronized保证可见性和有序性

Synchronized中优化

锁消除
jit编译器在编译的时候发现,只有一个线程来抢占资源的时候,就可以消除这个锁了,提升效率。
锁粗化
jit编译器在编译过程中发现,代码里连续多次加锁释放锁,就合并为一个锁。
偏向锁
如果可能只有一个线程来竞争锁,但也有其他可能来竞争锁,但是其他线程概率小,那么就会给这个锁维护一个偏好(Bias),如果这个偏好指向这个线程,就不需要moniterenter和moniterexit这种底层操作,而是基于Bias,如果有其他线程来占用这把锁,则把之前的Bias收回。
轻量级锁
就是说在偏向锁没有成功实现,会尝试采用轻量级锁的方式来加锁,会把对象头里面的一个轻量级锁指针尝试指向持有锁的线程,判断在moniter是否是自己持有锁,如果是自己持有锁就不用moniterenter和moniterexit。失败则用重量锁。
自旋锁
是指在尝试获取锁的时候不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自适应自旋锁
自选的时间不是固定的,而是基于上次在同一个锁上的自旋时间和锁的持有状态来决定。
公平锁和非公平锁
*可以在ReentrantLock构造方法中设置,
*空参构造为非公平:默认使用的是非公平锁,减少一定的上下文切换,保证系统更大的吞吐量,一上来就抢占锁,抢不到则排序。
*参数为true为公平锁:先来后到(FIFO)
*synchronized是非公平锁
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

synchronized 锁升级

无锁-->偏向锁-->轻量锁-->重量锁

synchronized和Lock区别

  1. synchronized是关键字,可以加在方法,代码块上,Lock是一个接口,需要使用其实现的类。

  2. synchronized可以自动上锁释放锁,Lock手动上锁释放锁。

  3. Lock可以根据Condition精准唤醒线程,synchronized只能随机唤醒一个或者全部唤醒。

  4. synchronized默认非公平锁,Lock可以选择公平锁和非公平锁。

  5. synchronized不可中断,Lock可中断。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UiFwvhOt-1660891177244)(img\synchronized和Lock区别.png)]

CAS

CAS自旋及自旋存在的问题

CompanyAndSwap比较然后替换
AtomicIntegter底层依靠cas算法和unsafe方法unsafe方法用native调用的是系统的底层原理
CAS主要依靠三个值,主内存值、工作内存读取的旧值、工作内存修改的新值,每次工作内存修改完之后、将旧值和主内存中的值比较,若一样则将新值赋值到主内存,若不一样则不赋值,重新读取主内存值到变量副本,做do while循环。
CAS存在的问题:
1.循环时间开销大
2.ABA问题 
3.只能保证一个共享变量的原子性,多个共享变量时候还是得用锁。
如何解决ABA问题
使用AtomicStampReference版本号原子引用,每次修改值版本号也会修改,版本号不一样也不会swap。

自旋锁demo

public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.myLock();
            try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
            spinLockDemo.myUnLock();}, "AA").start();

        try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
        new Thread(() -> {
            spinLockDemo.myLock();
            spinLockDemo.myUnLock();}, "BB").start();
    }


    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "come in");
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }

    public void myUnLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "   out");
        atomicReference.compareAndSet(thread, null);
    }

}

Lock

AQS机制(Abstract Queued Synchronizer)

AQS的核心原理是,如果资源空间空闲,就设请求线程为有效的工作线程,并锁定该线程;
如果资源已被占用,AQS就把当前线程以及等待状态信息构造成一个Node(节点)到同步队列中,同时再阻塞该线程。
当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

AQS机制

ReetrantReadWriteLock读写锁和RenntrantLock有什么区别?

ReetrantReadWriteLock读共享锁写独占锁

RenntrantLock读写都是独占锁

其他

CountDownLatch

CountDownLatch是基于AQS实现的,通过构造函数给AQS的state设置一个参数,当线程调用await方法会被阻塞,state!=0时会进入阻塞队列,每次调用CountDown方法state-1,当state=0时候,因调用await方法被阻塞的线程会被唤醒,继续执行。

CountDownLatch

CyclicBarrier

又叫做回环屏障,作用是让一组线程全部达到一个状态之后在全部同时执行,而且他可以重用,
CyclicBarrier也是基于AQS实现的,会维护一个parties记录总线程数,count用于计时,最开始count=parties,当调用await()方法count-1,当count=0,再次将parties赋值为count,达到复用。
![CyclicBarrier](https://img-blog.csdnimg.cn/8721cff5672e41ea97774d1c9751f33d.png)

Semaphore

semaphore(int,boolean)默认为false,非公平锁。

有线程进来int-1,释放资源则+1。

信号量主要用于两个目的,一个是共享资源互斥,一个是并发资源的控制。

如何查看死锁

  • 在当前目录下 jps -l 查看当前进程
  • 在当前目录下jstack 线程编号
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值