锁和synchronizeed关键字
学习材料来源于网络
如有侵权,联系删除
锁的概念
自旋锁:为了不放弃CPU执行事件,循环的使用CAS技术对数据尝试进行更新,直至成功。
悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)
共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)
可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。
几种重要的锁实现方式:synchronized、ReentrantLock、ReentrantReadWriteLock
同步关键字synchronized
属于最基本的线程通信机制,基于对象监视器实现的。
Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。一次只有一个线程可以锁定监视器。
试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。
特性:可重入、独享、悲观锁
锁的范围:类锁、对象锁、锁消除、锁粗化
提示:同步关键字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)。
案例1、悲观锁、独享锁
package icu.shaoyayu.multithreading.chapter3;
/**
* @author shaoyayu
* @E_Mail
* @Version 1.0.0
* @readme :
*/
// 锁 方法(静态/非静态),代码块(对象/类)
public class ObjectSyncDemo1 {
static Object temp = new Object();
public void test1() {
synchronized (this) {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
ObjectSyncDemo1 objectSyncDemo1=new ObjectSyncDemo1();
new Thread(() -> {
objectSyncDemo1.test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
objectSyncDemo1.test1();
}).start();
}
}
结果
Thread[Thread-0,5,main] 我开始执行
Thread[Thread-0,5,main] 我执行结束
Thread[Thread-1,5,main] 我开始执行
Thread[Thread-1,5,main] 我执行结束
synchronized作用在方法是,默认的是使用当期的对象作为锁。
synchronized+static作用在方法上,使用的是类锁。
案例2、可重入锁
// 可重入
public class ObjectSyncDemo2 {
public synchronized void test1(Object arg) {
System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
if (arg == null) {
test1(new Object());
}
System.out.println(Thread.currentThread() + " 我执行结束" + arg);
}
public static void main(String[] args) throws InterruptedException {
new ObjectSyncDemo2().test1(null);
}
}
结果:
Thread[main,5,main] 我开始执行 null
Thread[main,5,main] 我开始执行 java.lang.Object@1540e19d
Thread[main,5,main] 我执行结束java.lang.Object@1540e19d
Thread[main,5,main] 我执行结束null
案例3、锁粗化
// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看
public class ObjectSyncDemo3 {
int i;
public void test1(Object arg) {
synchronized (this) {
i++;
}
synchronized (this) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
new ObjectSyncDemo3().test1("a");
}
}
}
这是在编译的时候,对代码进行优化执行,
public void test1(Object arg) {
synchronized (this) {
i++;
i++;
}
}
案例4、锁消除
// 锁消除(jit)
public class ObjectSyncDemo4 {
public void test3(Object arg) {
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append(arg);
builder.append("c");
System.out.println(arg.toString());
}
public void test2(Object arg) {
String a = "a";
String c = "c";
System.out.println(a + arg + c);
}
public void test1(Object arg) {
// jit 优化, 消除了锁
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new ObjectSyncDemo4().test1("123");
}
}
}
stringBuffer是一个线程安全的,源码
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
在编译优化的时候,会把append方法的锁去掉。
同步关键字加锁原理
HotSpot中,对象前面会有一个类指针和标题,储标识哈希码的标题字以及用于分代垃圾收集的年龄和标记位
默认情况下JVM锁会经历:偏向锁->轻量级锁->重量级锁这四个状态
参考来源: https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
同步和对象锁定
Java编程语言的主要优势之一是其对多线程程序的内置支持。可以锁定在多个线程之间共享的对象,以便同步其访问。Java提供了用于指定关键代码区域的原语,这些关键代码区域作用于共享对象,并且一次只能由一个线程执行。进入该区域的第一个线程将锁定共享库。当第二个线程即将进入同一区域时,它必须等待,直到第一个线程再次将对象解锁。
在Java HotSpot™VM中,每个对象前面都有一个类指针和一个标题字。头字存储了身份哈希码以及年龄和标记位以进行代际垃圾收集,还用于实现瘦锁方案[ Agesen99,Bacon98 ]。下图显示了标题字的布局以及不同对象状态的表示。
图的右侧说明了标准锁定过程。只要对象被解锁,最后两位的值将为01。当方法在对象上同步时,标头字和指向该对象的指针将存储在当前堆栈帧内的锁定记录中。然后,VM尝试通过比较和交换操作在对象的标题字中安装指向锁定记录的指针。如果成功,则当前线程将拥有该锁。由于锁定记录始终在字边界对齐,因此标头字的最后两位为00,然后将对象标识为已锁定。
如果比较交换操作由于对象之前被锁定而失败,则VM首先测试标头字是否指向当前线程的方法堆栈。在这种情况下,线程已经拥有对象的锁,可以安全地继续执行它。对于这种递归锁定的对象,将锁定记录初始化为0而不是对象的标题字。仅当两个不同的线程在同一个对象上同时同步时,精简锁才必须膨胀到重量级监视器,以管理等待的线程。
精简锁比膨胀锁便宜很多,但是它们的性能受到以下事实的困扰:尽管大多数对象只能由一个特定的线程锁定和解锁,但每个比较和交换操作都必须在多处理器计算机上以原子方式执行。在Java 6中,此缺陷已通过所谓的“无存储偏向锁定”技术[ Russell06 ]得到解决,该技术使用类似于[ Kawachiya02 ]的概念。只有第一个锁获取会执行原子比较和交换,以将锁定线程的ID安装到标头字中。然后说该物体有偏斜朝向线程。同一线程将来对对象的锁定和解锁不需要任何原子操作或标题字的更新。即使堆栈上的锁定记录也未初始化,因为它将永远不会检查有偏差的对象。
当线程在偏向另一个线程的对象上同步时,必须通过使该对象看起来像已被常规方式锁定一样来消除该偏见。遍历偏压所有者的堆栈,根据精简锁定方案调整与对象关联的锁定记录,并在对象的标头字中安装指向它们中最旧的指针。此操作必须暂停所有线程。当访问对象的身份哈希码时,由于哈希码位与线程ID共享,因此也消除了偏差。
明确设计为在多个线程之间共享的对象(例如生产者/消费者队列)不适合于偏向锁定。因此,如果某个类的实例过去频繁发生吊销,则该类将禁用偏向锁定。这称为批量撤销。如果在禁用了偏向锁定的类的实例上调用锁定代码,它将执行标准的精简锁定。该类的新分配实例被标记为不可偏的。
一种类似的机制,称为批量重新偏置(bulk rebiasing),可以优化类对象由不同线程锁定和解锁但绝不并发锁定的情况。它使类的所有实例的偏向无效,而不会禁用偏向锁定。类中的一个纪元值用作指示偏差有效性的时间戳。在分配对象时,此值将复制到标题字中。然后,可以在适当的类中有效地将历时重新偏置作为历元的增量来实现。下次要锁定此类的实例时,代码将在标头字中检测到一个不同的值,并将该对象偏向当前线程。
参考文献:https://wiki.openjdk.java.net/display/HotSpot/Synchronization
轻量级加锁
使用CAS修改mark word完毕,加锁成功。
则mark word中的tag进入00状态。解锁的过程,则是一个逆向恢复mark word的过程
tag:锁的位置状态
01、对象没有被锁定
00、对象被cas锁锁定,也就是轻量级锁锁定对象
10、重量级锁,也就是监视器锁锁定的状态
11、代表的是状态码
偏向锁
偏向标记第一次有用,出现过争用后就没用了。-XX: -UseBiasedLocking禁用使用偏置锁定,
偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步(jvm为了少干活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作)
偏向锁在第二个锁争抢锁的时候,是升级为轻量级锁。也就是修改对象的首部信息成轻量级锁信息。
重量级锁
重量级锁-监视器(monitor)
修改mark word如果失败,会自旋CAS一定次数,该次数可以通过参数配置:
超过次数,仍未抢到锁,则锁升级为重量级锁,进入阻塞。
monitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitoro