1.synchronized介绍
- 关键字synchronized可以保证在同一时刻只有一个线程可以执行某个方法或者某个代码块,同时synchronized可以保证一个线程的变化(共享数据的变化)被其他线程所看到的。
2.synchronized用法
2.1同步普通方法
- 作用于当前实例加锁,进入同步代码前要获得当前实例的锁,线程正在访问该方法时,其他试图访问该对象该方法的线程会被阻塞。
public class SyncronizedTest implements Runnable {
static int i = 0;
public synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increase();
}
}
public static void main(String[] args)throws InterruptedException {
SyncronizedTest test = new SyncronizedTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
20000
- 开启两个线程t1和t2操作test的同一个共享资源i,对i进行i++操作,该操作不具备原子性,首先需要读取i的值,然后再给i+1。如果increase()不使用synchronized关键字,那么很有可能在t1读取i值和写i值的期间,t2也读取i值,此时i值为旧值(即t1没写之前),然后t1对i加1后,t2也对i加1,最终i的值会比20000小,这就造成了线程不安全。
- 因此在increase()前使用了synchronized关键字,当t1对i进行操作时,会拿到test的锁,如果此时t2也要进行操作,但是test没有释放锁,一个对象只有一把锁,那么test只能等待t1操作完,释放锁后才能进行。这样就保证了同一时刻只有一个线程可以执行increase()方法。
- 但是如果是两个对象分别调用increase()方法,那么是允许的,比如下面代码里面的,线程t1需要访问test对象调用increase(),线程t2需要访问test1对象调用increase(),t1与t2会进入各自的对象锁,所以线程是不安全的,最后i的值还是小于20000。
public class SyncronizedTest implements Runnable {
static int i = 0;
public synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increase();
}
}
public static void main(String[] args)throws InterruptedException {
SyncronizedTest test = new SyncronizedTest();
SyncronizedTest test1 = new SyncronizedTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
19851
2.2同步静态方法
- 作用于当前类对象加锁,进入同步代码之前要获得当前类对象的锁。
- 鉴于以上问题,可以将increase()方法设为static的,那么无论实例化多少对象,锁对象为当前类的class对象,这样当t1运行时,t2线程如果也想运行,需要获取当前class对象的锁,此时锁被占用,t2线程只能等待,这样就保证了线程安全。
public class SyncronizedTest implements Runnable {
static int i = 0;
public static synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increase();
}
}
public static void main(String[] args)throws InterruptedException {
SyncronizedTest test = new SyncronizedTest();
SyncronizedTest test1 = new SyncronizedTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
20000
2.3同步方法块
- 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
- 但是有时一个方法体比较大,需要同步的代码却很少,此时就可以使用同步方法块。如下所示,每当线程进入到此代码块时,如果持有test对象的锁,那么其他线程就必须等待,这样就保证只有一个线程可以进行i++操作。
public class SyncronizedTest implements Runnable {
static SyncronizedTest test = new SyncronizedTest();
static int i = 0;
@Override
public void run() {
//其他一些操作......
synchronized(test){
for(int j=0;j<10000;j++){
i++;
}
}
}
public static void main(String[] args)throws InterruptedException {
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
20000
3.synchronized原理
3.1堆内存
-
在JVM中,对象在内存中的布局分为:对象头、实例变量和填充数据。
- 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度。
- 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
- 对象头:对象头中有两类信息,一是mark word,二是类型指针,如果是数组,还有记录数组长度的数据。类型指针是指向该对象所属类的指针,虚拟机通过这个指针来确定这个对象是哪个类;mark word用于存储对象的hashcode、GC分代年龄、锁状态等信息。
-
32位JVM的Mark Word默认存储结构
-
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:
重量级锁也就是synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构有:_count,_WaitSet,_EntryList等。
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。
3.2synchronized原理
同步代码块如下:
public class SyncBlock {
public int i;
public void syncTask(){
//同步代码库
synchronized (this){
i++;
}
}
}
编译上述代码并使用javap反编译后得到字节码如下:
Compiled from "SyncBlock.java"
public class SyncBlock {
public int i;
public SyncBlock();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void syncTask();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
}
- 从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor,重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
4.synchronized原子性,可见性和有序性保障
4.1synchronized原子性
原子性是指一个操作是不可中断的,要么全部执行,要不就都不执行。
- 线程是CPU调度的基本单位,CPU会根据不同的调度算法进行线程调度,当一个线程获得时间片之后开始执行,在时间片耗尽后,就会失去CPU使用权,所以在多线程情况下,由于时间片在线程间轮换,就会发生原子性问题。
- 但是synchronized修饰的代码,通过monitorenter 和monitorexit 指令,保证代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问。因此,在java中可以使用4.1synchronized来保证方法和代码块内操作的原子性。
4.2synchronized可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- synchronized为了保证可见性,对一个变量解锁前,必须先把此变量同步回主存中,这样解锁后,后续线程就可以访问到被修改后的值。
4.2synchronized有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。
- 编译器和处理器遵守as-if-serial语义,保证单线程中指令排序是有一定的限制的,可以认为单线程程序是按照顺序执行的,所以由synchronized修饰的代码,同一时间只能被同一线程访问,也就是单线程执行,所以可以保证有序性。
5.synchronized的可重入性
定义:若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
- synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。
- synchronized可重入原因:
每个锁关联一个monitor,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
6.synchronized和volatile的区别
- 粒度不同:volatile针对变量 ,synchronized锁对象和类
- synchronized阻塞,volatile线程不阻塞
- synchronized保证三大特性,volatile不保证原子性
- synchronized编译器优化,volatile不优化
7.synchronized与Lock的区别
- synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
引用
https://blog.csdn.net/qq_41247433/article/details/79433831
https://blog.csdn.net/zfy163520/article/details/89138218
https://blog.csdn.net/u010647035/article/details/82320571
https://blog.csdn.net/qq_33173608/article/details/88202474
https://www.cnblogs.com/iyyy/p/7993788.html
https://www.cnblogs.com/cielosun/p/6684775.html
以上仅为笔记,如有错误,接受指正。