java锁的相关术语及synchronized原理详解
1、几种锁的概念
(1)自旋锁
CPU循环的使用CAS技术对数据尝试更新,直至成功。
(2)悲观锁
线程假定会发生并发冲突,同步所有对数据的操作,从读操作开始就上锁。
(3)乐观锁
线程假定没有冲突,在修改数据时发现数据和一开始获取的不一致,则读取最新数据并再次尝试修改。
(4)独享锁(写)
线程给资源加上写锁,并且可以修改资源值,其他线程不能;(单写)
(5)共享锁(读)
线程给资源加上读锁后只能读不能写,其他线程也只能加读锁,不能加写锁;(多读)
(6)可重入锁/不可重入锁
线程拿到锁以后,可以自由进入同一把锁所同步的其他代码,则为可重入锁;反之,则为不可重入锁。
(7)公平锁/非公平锁
争抢锁的顺序 ,如果按照先来后到,则为公平锁;反之,则为非公平锁。
2、同步关键字synchronized
写在前面
CAS:传入一个旧值A(期望操作前的值)和一个新值B,在操作前比较之前存储的一开始的旧值A和现在读取的旧值A’是否发生变化(因为在操作前可能会有其他线程改变了旧值),没有发生变化则将旧值交换为新值,发生了变化则不交换。
synchronized原理
synchronized是基于对象监视器实现的。java中的每个对象都与一个监视器关联,一个线程可以锁定或者解锁监视器,从而拿到对应对象监视器的操作权;一次只有一个线程可以锁定监视器;当一个线程锁定一个监视器以后,其他所有试图锁定该监视器的线程都会被阻塞,直到它们可以锁定该监视器为止。
-
特性:可重入、独享、悲观锁
-
锁的范围:类锁、对象锁(此处理解为“实例锁”更为恰当,因为在java中一切皆对象,包括类)、锁消除、锁粗化
public class ObjectSyncDemo1 { static Object temp = new Object(); public static synchronized void test1() { try { System.out.println(Thread.currentThread() + " 我开始执行"); Thread.sleep(3000L); System.out.println(Thread.currentThread() + " 我执行结束"); } catch (InterruptedException e) { } } // public synchronized void test1() { // synchronized(ObjectSyncDemo1.class){ // 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 { new Thread(() -> { new ObjectSyncDemo1().test1(); }).start(); Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来 new Thread(() -> { new ObjectSyncDemo1().test1(); }).start(); } }
public class ObjectSyncDemo1 {
static Object temp = new Object();
//改为非静态方法
public synchronized void test1() {
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
// public synchronized 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 {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
synchronized在非静态方法上:锁的对象 是 类的一个实例 ; 在静态方法上:锁的对象 是 类本身,即类锁。除此之外,public static synchronized void test1() 等价于 synchronized (ObjectSyncDemo1.class),都是类锁;public synchronized void test1() 等价于 synchronized (this),都是实例锁。
实例锁可以理解为锁住的是一个类的一个实例,当再建一个实例的时候,之前一个实例的锁对新建的这个实例对象没有锁的作用。所以第二段代码中两个线程可以同时执行,不用等到第一个线程中的实例方法(Thread.sleep(3000L))执行完再执行第二个线程中的方法;同理,类锁锁住的是类,无论建多少个类的实例,只要一个实例中的类锁没有释放,同一个类的实例都要阻塞直至上一个实例的类锁释放,这也是为什么第一段代码中的两个线程会先后执行的原因。
package com.study.lock.sync;
// 可重入
public class ObjectSyncDemo2 {
public synchronized void test1(Object arg) {
// 继续执行,保证能读取到之前的修改 JMM
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);
}
}
这个例子就体现了synchronized的可重入特性。当一个线程拿到锁后,其他线程就不能再进入锁的对象进行读或写操作,体现了synchronized的独享和悲观锁特性。
synchronized不仅实现了同步,根据JMM(Java Memory Model)还实现了可见性(读取最新主内存数据,并写入主内存)
锁粗化:可以理解为锁的范围扩大
package com.study.lock.sync;
// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看
public class ObjectSyncDemo3 {
int i;
public void test1(Object arg) {
synchronized (this) {
i++;
// }
// 两次锁中间什么事情也没干,而且解锁、加锁也会浪费资源,导致性能不高,因此jvm就会优化,把锁范围扩大,即 锁粗化
// synchronized (this) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
new ObjectSyncDemo3().test1("a");
}
}
}
锁消除:在一些情况下不需要锁,jvm就会把锁去掉以提高性能
public void test1(Object arg) {
// jit 优化, 消除了锁 (StringBuffer是方法内的 局部变量,没有竞态条件,所以不需要锁,但是StringBuffer本身就是线程安全的,它的实现里面是有锁的,所以jvm在真正执行过程中会把锁消除)
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}
锁升级过程
图中tag有4种状态,分别是01(无锁)、00(轻量级锁)、10(重量级锁)、11(GC废弃)
每个对象实例头部都有一个mark word,里面包含很多位信息,可以理解成一张表,里面包含很多字段信息,但是这些字段是根据锁状态动态生成的,并不是一直不变的。
一个对象在被创建的时候,根据配置生成对象的内存结构,也就是上面所说的mark word。这个配置可以通过参数 -XX: -UseBiasedLocking 来禁用使用偏置锁定,也就是禁止开启偏向锁(偏向锁本质就是无锁),默认是开启偏向锁的。
默认开启偏向锁时,偏向锁标记为1,tag状态为01(无锁)。当只有一个线程访问此对象时,会直接把“线程ID”设置为自己的线程ID,以后再次访问这个对象的时候,就直接比对“线程ID”的值是否与自己相同,如果相同就可以直接对对象进行操作,此时即为偏向锁;如果不同则说明是另外一个线程(或者多个线程)来访问对象,就会产生竞争,此时表结构就会发生变化,如上图所示。此时就升级为轻量级锁,偏向锁标记为0,并且多个线程将执行CAS操作将tag状态改为00(轻量级锁),当其中一个线程修改状态成功后,对象内存结构的表字段变成上图所示,会出现一个“lock锁记录地址”的 字段,它指向当前修改tag状态成功的线程栈。但是当线程比较多的时候, 频繁的进行CAS操作会消耗大量的CPU资源,所以当修改状态失败的线程CAS自旋一定次数以后,对象锁会升级为重量级锁。此时tag状态改为10,表结构中会出现一个“监视器对象地址”的字段,用来存储对象的监视器地址。并且线程都会去争抢这个监视器对象的锁,没有抢到锁的线程就会进入锁池中等待,以避免出现多线程循环CAS空转浪费资源的问题。
当一个类的对象经常发生多线程争抢操作时,JVM会优化不会开启偏向锁了,直接从轻量级锁开始。
等待队列WaitSet是为wait/notify所设置的一个队列,用来放置wait的线程并等待notify。
为什么需要偏向锁?
根据统计,当线程操作对象,对对象定义了锁之后,并发情况较少,所以为了减少资源开销,避免不必要的资源浪费,JVM定义了偏向锁,这样当只有一个线程操作时,不用浪费资源为对象创建监视器对象,不会有加锁、解锁的操作,性能也更高。
总结
同步关键字,优化内容:
偏向锁(减少在无竞争情况下的JVM资源消耗)
–> 出现两个及以上的线程争抢–>轻量级锁(CAS修改状态)
–>线程CAS自旋一定次数之后,升级为重量级锁(对象的mark word 内部会保存一个监视器锁的一个地址)