作者前面也写了几篇关于Java并发编程,以及线程和volatil的基础知识,有兴趣可以阅读作者的原文博客,今天关于Java中的两种锁进行详解,希望对你有所帮助
本文受赵sir原创发布,转载请联系原创
https://blog.csdn.net/qq_3609...在上一章中说了volatile,在多线程下可以保证变量的可见性,但是不能保证原子性,下面一段代码说明:
运行上面代码,会发现输出flag的值不是理想中10000,虽然volatile写入时候会通知其他线程的工作内存值无效,从主内存重写读取。i++是三步操作,读取-赋值-写入不能保证原子性。 原子性:不能被中断要么成功要么失败。
比如此时主内存的flag值10,线程1和线程2读取到自己工作内存都是10,然后线程1在进行赋值的时候,线程2执行了,这时线程2发现自己内存的值和主内存的值一样,并没有修改,然后赋值写入11,此时线程1运行,因为之前读过了,会往下继续运行写入也是11。那么两个线程相当于只增加了一次。要想达到理想值,只需要修改 public synchronized void increase() { flag++; }
就行了。
编译后执行 javap -v Test.class 就会发现两条指令。
synchronized在1.6以前是重量级锁,当前只有一个线程执行,其他线程阻塞。为了减少获得锁和释放锁带来的性能问题,而引入了偏向锁、轻量级锁以及锁的存储过程和升级过程。在1.6后锁分为了无锁、偏向锁、轻量锁、重量锁,锁的状态在多线程竞争的情况下会逐渐升级,只能升级而不能降级,这样是为了提高锁获取和释放的效率。
synchronized的锁是存贮在Java对象头里的,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。1个字宽等于4个字节。
Java对象头中的Mark Word里默认存储了对象是HashCode、分代年龄、和锁标记。
在运行的时候,Mark Word里存储的数据会随着锁标志位的变化而变化,可能会变化为存储以下四种形式。
偏向锁的意思未来只有一个线程使用锁,不会有其他线程来争取。
撤销锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。需要等待全局安全点,它首先暂停原持有偏向锁的线程,然后检查线程是否还在活着,如果线程处于未活动状态,则释放锁标记,如果处于活动状态则升级为轻量级锁。
线程在执行同步代码块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
解锁:
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
它是在1.5之后提供的一个独占锁接口,它的实现类是ReentrantLock,相比较synchronized这种隐式锁(不用手动加锁和释放锁)的便捷性,但是提供了更加锁的可操作性、可中断的获取锁以及超时获取锁等多种synchronized不具备的特性。
在finally中释放锁,目的保证获取锁最终被释放。不要在获取锁写在try里,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故释放。
AQS是队列同步器(AbstractQueuedSynchronizer),是用来构建锁或者其他同步器的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取的线程排队工作问题。AQS在内部维护了一个单一的状态信息state,可以通过getState、setState、compareAndSetState(CAS操作)修改此值,对于ReentrantLock来说,state可以用来表示当前线程获取锁的可重入次数。ReentrantLock中当一个线程获取了锁,在AQS的内部会进行compareAndSetState将state变为1,如果再次获取就设置为2,释放锁也会去修改state值,只有当值变为0时,其他线程才能获得锁。
AQS底层维护state和队列来实现独占和共享两种锁。
独占锁:每次只能有一个线程能持有锁,如lock、synchronized。
共享锁:允许多个线程同时获取锁,并发访问共享资源,如ReadWriteLock。
lock分为公平锁和非公平锁,实现了AQS接口,通过FIFO设置锁的优先级。
公平锁:根据线程获取锁的时间来判断,等待时间越久的线程优先被执行。Lock中初始化的时候ReentrantLock(true),默认为false,效率较低因为需要判断线程的等待时间。
非公平锁:抢占锁资源,不能保证获取锁的线程优先级,效率较高,因为获取锁是竞争的。
还记得在 Java并发二 中有一道生产者消费者,使用的是synchronized+wait(notify),lock中也提供了这种等待通知类型的方法await和signal,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁,Condition是依赖于Lock对象,调用lock对象中的newCondition。
老样子还是先定义一个容器:
生产者:启5个线程往容器里添加数据。
消费者:启10线程消费数据
注释基本明确,就不多说了。wait和notify是配合synchronized使用,await和signal是配合lock使用,区别在于唤醒时notify不能指定线程唤醒,signal可以唤醒具体的线程,更小的粒度控制锁。
金三银四,2019最新面试实战总结
如何通过抓包实战来学习Web协议?
动画:一招学会TCP的三次握手和四次挥手
上两个月,15家面试,几个offer , 我的面试历程!
在这里获得的不仅仅是技术!