1、为什么要用锁?
这是为了解决锁定-并发操作导致的脏读、数据不一致的问题。
2、锁定实现的基本原理
2.1、卷
在Java编程语言中,线程可以访问共享变量。 线程必须通过独占锁分别获取此变量,以确保共享变量正确且一致地更新。 Java语言提供了volatile,在某些情况下比锁定更有用。
volatile在多处理器开发中保证了共享变量的“可见性”。 可见性意味着当一个线程更改共享变量时,另一个线程可以读取该更改的值。
结论: volatile变量修饰符的使用成本低于同步的使用和运行成本,因为如果合适的话,不会引起线程上下文的切换和调度。
2.2、已同步
同步通过锁定机制实现同步。
首先,让我们看看使用同步的同步基础。 Java中的所有对象都充当锁。
具体表现为以下三种形式。
在常规同步方法中,锁定是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是设置在同步括号中的对象。
如果线程尝试访问同步代码块,则必须首先获取锁定,然后在退出或抛出异常时解除锁定。
2.2.1同步实现原理
同步基于监视器实现同步。
Monitor从两个方面支持线程之间的同步。
排他执行
合作
1、Java使用对象锁(使用同步获取对象锁)来确保在共享数据集上运行的线程互斥地执行。
2、使用notify/notifyAll/wait方法协调不同线程之间的工作。
3、Class和Object都与监视器相关。
线程将进入同步方法。
要继续关键代码,线程必须获取监视器锁定。 成功获取锁定后,您将成为该监视者的目标所有者。 在任何时间点,监视者对象都只属于一个活动线程
具有监视者对象的线程可以调用wait ()进入等待集合(Wait Set ),同时解除监视锁定进入等待状态。
其他线程调用notify ()/notifyAll ()接口以启动等待集中的线程,在运行wait () )或更高版本的代码之前必须重新获取监视锁。
同步方法执行完毕。 线程退出关键节并解除监视锁定。
参考文献: https://www.IBM.com/developer works/cn/Java/j-lo-synchronized
2.2.2同步的具体实现
1、同步代码块采用monitorenter、monitorexit指令明确实现。
2、同步方法使用ACC_SYNCHRONIZED标志隐式实现。
让我们来看看具体的实现示例。
publicclassSynchronizedTest {
publicsynchronizedvoid方法1 (
system.out.println('Helloworld!' );
}
公共语音方法2 () {
同步(this ) {
system.out.println('Helloworld!' );
}
}
}
javap编译后的字节码如下:
监视器输入器
每个对象都有一个监视器,一个监视器只能由一个线程拥有。 当线程执行monitorenter指令时,它会尝试获取相应对象的monitor。 获取规则如下:
如果监视器条目数为0,则线程可以进入监视器;如果将监视器条目数设置为1,则线程成为监视器的所有者。
如果当前线程已经有此监视器,并且只是重新条目,则同步关键字实现的锁定是可重新条目的锁定,因为该监视器的条目数加1。
如果监视器已为其他线程所有,则当前线程将一直处于阻止状态,直到监视器条目数为0,然后重试检索监视器。
莫妮卡托丽熙
只有具有相应对象的monitor线程才能运行monitorexit命令。 如果每次运行命令monitor时条目数减去1,并且当前线程在条目数为0时释放monitor,则其他被阻止的线程将尝试检索monitor。
2.2.3锁定仓库
锁定标记存储在Java对象头上任性的手册Word中。
2.2.3 synchronized的锁优化
JavaSE1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。在JavaSE1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
偏向锁:
无锁竞争的情况下为了减少锁竞争的资源开销,引入偏向锁。
轻量级锁:
轻量级锁所适应的场景是线程交替执行同步块的情况。
锁粗化(Lock Coarsening):
也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
锁消除(Lock Elimination):
锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。
适应性自旋(Adaptive Spinning):
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
2.2.4 锁的优缺点对比
2.3、CAS
CAS,在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。
1、CAS是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。
2、JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。
优点:
竞争不大的时候系统开销小。
缺点:
循环时间长开销大。
ABA问题。
只能保证一个共享变量的原子操作。
3、Java中的锁实现
3.1、队列同步器(AQS)
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架。
3.1.1、它使用了一个int成员变量表示同步状态。
3.1.2、通过内置的FIFO双向队列来完成获取锁线程的排队工作。
同步器包含两个节点类型的应用,一个指向头节点,一个指向尾节点,未获取到锁的线程会创建节点线程安全(compareAndSetTail)的加入队列尾部。同步队列遵循FIFO,首节点是获取同步状态成功的节点。
未获取到锁的线程将创建一个节点,设置到尾节点。如下图所示:
首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点。如下图所示:
3.1.3、独占式/共享式锁获取
独占式:有且只有一个线程能获取到锁,如:ReentrantLock。
共享式:可以多个线程同时获取到锁,如:CountDownLatch
独占式
每个节点自旋观察自己的前一节点是不是Header节点,如果是,就去尝试获取锁。
独占式锁获取流程:
共享式:
共享式与独占式的区别:
共享锁获取流程:
4、锁的使用用例
4.1、ConcurrentHashMap的实现原理及使用(1.7)
ConcurrentHashMap类图
ConcurrentHashMap数据结构
结论:
ConcurrentHashMap使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
来源 | www.jianshu.com/p/e674ee68fd3f