Synchronized
类的实例对象在内存中存储分为哪三块区域?(java对象在虚拟机中的布局?)
结论:对象头、实例数据、对齐填充
并发编程的三个问题:可见性、原子性、有序性
可见性(Visibility):保证共享变量的修改能够及时可见,是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
1 /** 2 * 案例演示: 3 * 一个线程对共享变量的修改,另一个线程不能立即得到最新值 4 */ 5 public class Test01Visibility { 6 /*多个线程都会访问的数据,我们称为线程的共享数据*/ 7 private static boolean flag = true; 8 9 public static void main(String[] args) throws InterruptedException { 10 new Thread(() -> { 11 while (flag) { 12 13 } 14 }).start(); 15 16 Thread.sleep(2000); 17 18 new Thread(() -> { 19 flag = false; 20 System.out.println("00000000000000"); 21 }).start(); 22 } 23 }
原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
1 /** 2 * 案例演示:5个线程各执行1000次 i++; 3 */ 4 public class Test02Atomicity { 5 private static int number = 0; 6 7 public static void main(String[] args) throws InterruptedException { 8 Runnable runnable = () -> { 9 for (int i = 0; i < 1000; i++) { 10 number++; 11 } 12 }; 13 List<Thread> lists = new ArrayList<>(); 14 for (int i = 0; i < 5; i++) { 15 Thread thread = new Thread(runnable); 16 thread.start(); 17 lists.add(thread); 18 } 19 for (Thread list : lists) { 20 list.join(); 21 } 22 System.out.println("number------->" + number); 23 } 24 }
有序性(Ordering):是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。
如果对一个变量执行lock操作,将会清空工作内存中此变量的值
主内存与工作内存之间的数据交互过程 :
lock -> read -> load -> use -> assign -> store -> write -> unlock
使用synchronized保证原子性:
1 /** 2 * 案例演示:5个线程各执行1000次 i++; 3 */ 4 public class Test02Atomicity { 5 private static int number = 0; 6 7 public static void main(String[] args) throws InterruptedException { 8 Runnable runnable = () -> { 9 for (int i = 0; i < 1000; i++) { 10 synchronized (Test02Atomicity.class) { 11 number++; 12 } 13 } 14 }; 15 List<Thread> lists = new ArrayList<>(); 16 for (int i = 0; i < 5; i++) { 17 Thread thread = new Thread(runnable); 18 thread.start(); 19 lists.add(thread); 20 } 21 for (Thread list : lists) { 22 list.join(); 23 } 24 System.out.println("number------->" + number); 25
synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。
使用synchronized保证可见性:
1 /** 2 * 案例演示: 3 * 一个线程对共享变量的修改,另一个线程不能立即得到最新值 4 */ 5 public class Test01Visibility { 6 /*多个线程都会访问的数据,我们称为线程的共享数据*/ 7 private static boolean flag = true; 8 private static Object object = new Object(); 9 10 public static void main(String[] args) throws InterruptedException { 11 new Thread(() -> { 12 while (flag) { 13 synchronized (object) { 14 15 } 16 } 17 }).start(); 18 19 Thread.sleep(2000); 20 21 new Thread(() -> { 22 flag = false; 23 System.out.println("00000000000000"); 24 }).start(); 25 } 26 }
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值
synchronized特性
1、可重入特性:
一个线程可以多次执行synchronized,重复获取同一把锁。
1 public class demo1 { 2 public static void main(String[] args) { 3 new MyThread().start(); 4 new MyThread().start(); 5 } 6 } 7 8 /*自定义一个新线程*/ 9 class MyThread extends Thread { 10 @Override 11 public void run() { 12 synchronized (MyThread.class) { 13 System.out.println(Thread.currentThread().getName() + "-------------- 1"); 14 15 synchronized (MyThread.class) { 16 System.out.println(Thread.currentThread().getName() + "-------------- 2"); 17 } 18 } 19 } 20 }
可重入的原理:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。
可重入的优点:
1、封装代码
2、避免死锁
小结
synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,知道计数器的数量为0,就释放这个锁。
2、不可中断特性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
synchronized不可中断演示:
1 /*目标:演示synchronized不可中断*/ 2 public class demo2 { 3 private static Object obj = new Object(); 4 5 public static void main(String[] args) { 6 // 1.定义一个Runnable 7 Runnable runnable = () -> { 8 // 2.在Runnable定义同步代码块 9 synchronized (obj) { 10 String name = Thread.currentThread().getName(); 11 System.out.println(name + "---------开始"); 12 try { 13 Thread.sleep(66666666); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 } 18 }; 19 // 3.先开启一个线程来执行同步代码块,保证不退出同步代码块 20 Thread thread1 = new Thread(runnable); 21 thread1.start(); 22 try { 23 Thread.sleep(1000); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 // 4.后开启一个线程来执行同步代码块(阻塞状态) 28 Thread thread2 = new Thread(runnable); 29 thread2.start(); 30 System.out.println("停止线程前"); 31 // 5.停止第二个线程 32 thread2.interrupt(); 33 System.out.println("停止线程后"); 34 System.out.println("Thread1:------------>" + thread1.getState()); 35 System.out.println("Thread2:------------>" + thread2.getState()); 36 } 37 }
ReentrantLock可中断演示:
1 /*目标:演示Lock不可中断和可中断*/ 2 public class demo3 { 3 private static Lock lock = new ReentrantLock(); 4 5 public static void main(String[] args) { 6 Runnable runnable = () -> { 7 lock.lock(); 8 System.out.println(Thread.currentThread().getName() + "获得锁,进入锁执行"); 9 try { 10 Thread.sleep(88888); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } finally { 14 lock.unlock(); 15 System.out.println(Thread.currentThread().getName() + "释放锁"); 16 } 17 }; 18 Thread thread1 = new Thread(runnable); 19 thread1.start(); 20 try { 21 Thread.sleep(1000); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 Thread thread2 = new Thread(runnable); 26 thread2.start(); 27 System.out.println("停止t2线程前"); 28 thread2.interrupt(); 29 System.out.println("停止t2线程后"); 30 System.out.println("thread1------------>" + thread1.getState()); 31 System.out.println("thread2------------>" + thread2.getState()); 32 } 33 }
synchronized属于不可被中断
Lock的lock方法是不可中断的
Lock的tryLock方法是可中断的
synchronized原理:

monitorenter:每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下: 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者) 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,而是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后,其他线程只能等待。
monitorexit:能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
当synchronized出现异常时,会释放锁。
synchronized与lock的区别:
synchronized是一个关键字,而lock是一个接口。
synchronized会自动释放锁,而lock必须手动释放锁。
synchronized是不可中断的,而lock是可中断也可不中断。
通过lock可以知道线程是否拿到所,而synchronized不能。
synchronized可以锁住方法和代码块,而lock只能锁住代码块。
synchronized是非公平锁(就是调用任意一个等待线程,不是先来先调),ReentrantLock可以控制是否公平。
lock可以使用读锁来提高多线程效率。
monitor监视器锁
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要数据结构如下:
1 ObjectMonitor() { 2 _header = NULL; 3 _count = 0; 4 _waiters = 0, 5 _recursions = 0; // 线程的重入次数 6 _object = NULL; // 存储该monitor的对象 7 _owner = NULL; // 标识拥有该monitor的线程 8 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet 9 _WaitSetLock = 0 ; 10 _Responsible = NULL; 11 _succ = NULL; 12 _cxq = NULL; // 多线程竞争锁时的单向列表 13 FreeNext = NULL; 14 _EntryList = NULL; // 处于等待锁block状态的线程,会被加入到该列表 15 _SpinFreq = 0; 16 _SpinClock = 0; 17 OwnerIsThread = 0; 18 }
_owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。 _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改cxq队列。修改前cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。 _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。 _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

monitor竞争
通过CAS尝试把monitor的owner字段设置为当前线程。
如果设置之前的owner指向当前线程,说明当前线程再次进入到monitor,即是重入锁,执行 recursions++,记录重入的次数。
如果当前线程是第一次进入monitor,设置recursions为1,_owner为当前线程,该线程成功获得并返回。
如果获取锁失败,则等待锁的释放。
monitor等待
当前线程被封装成ObjectWaiter对象node,状态设置为ObjectWaiter::TS_CXQ。
在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取锁,则通过park将当前线程挂起,等待被唤醒。
当前线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁。
minotor释放
退出同步代码块时,_recursions--,当_recursions的值变为0时,说明线程释放了锁。
monitor是重量级锁
ObjectMonitor的函数调用会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会被park()挂起,竞争到锁的线程会被unpark()唤醒。这个时候会存在操作系统用户态与内核态的转换,反复的切换会消耗大量的系统资源,因此synchronized是java语言中的一个重量级(Heavyweight)的操作。
JDK6 synchronized优化
CAS:Compare And Swap(比较相同再交换),是现代CPU广泛支持的一种对内存中的共享数据进行操作的特殊指令。
CAS作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证,CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V、旧的预估值X、要修改的新值B,如果旧的预估值X等于内存中的值,就将新的值B保存到内存中。
1 public class demo2 { 2 private static AtomicInteger atomicInteger = new AtomicInteger(); 3 4 public static void main(String[] args) { 5 Runnable runnable = () -> { 6 for (int i = 0; i < 1000; i++) { 7 atomicInteger.incrementAndGet(); 8 } 9 }; 10 List<Thread> list = new ArrayList<>(); 11 for (int i = 0; i < 5; i++) { 12 Thread thread = new Thread(runnable); 13 thread.start(); 14 list.add(thread); 15 } 16 for (Thread thread : list) { 17 try { 18 thread.join(); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 } 23 System.out.println("atomicInteger-------------->" + atomicInteger.get()); 24 } 25 }
Unsafe类
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

乐观锁与悲观锁:
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
CAS在获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile操作,可以实现无锁并发,适用于竞争不激烈、多核CPU的场景下。
因为没有使用synchronized,所以线程不会陷入阻塞,这是提升效率的重要因素之一。
但如果竞争激烈,就会导致重试频繁发生,进而降低效率。
小结:
CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理器保证。
synchronized锁升级过程
无锁 --> 偏向锁(Biased Locking)--> 轻量级锁(Lightweight Locking)--> 重量级锁
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据、对齐填充


对象头:对象头包含两个部分,第一类是用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等这部分数据在32位和64位的虚拟机分别对应32bit集合64bit,官方称为Mark Word,对象需要存储的运行时数据很多,已经超出了32bit或64bit的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,便于在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间;对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例,如果对象是个数组,那对象头必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据获取java对象的大小,但是无法获取数组的长度
实例数据:对象真正存储的有效信息,即程序中定义的各种类型的字段内容
对齐填充:这部分不是必然存在的,也没有特别的含义,仅仅起着占位符的作用,因为虚拟机内存管理系统的起始地址必须是8字节的整数倍,对象头是被精心设计成正好是8字节的倍数,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来
对象头:在普通实例对象中,oopDesc的定义包含两个成员,分别为_mark与_metadata
_mark表示对象标记,数据markOop类型,也就是Mark Word,记录了对象和锁有关的信息
_metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针,_compressed_klass表示压缩类指针
对象头由两部分组成,一部分用于存储自身的运行时数据,称之为Mark Word,另一部分是类型指针,及对象的指向它的类元数据的指针。
对象头= Mark Word + 类型指针
在64位系统中,Mark Word = 8 bytes,类型指针 = 8 bytes,对象头 = 16 bytes = 128 bits
偏向锁:在JDK 6引进,也就是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步代码块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。
偏向锁过程:
虚拟机将会把对象头的标志为设为 “ 01 ”,即偏向模式
同时使用CAS操作把获取到这个锁的现成的ID记录在对象的Mark Word 之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步代码块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
持有偏向锁的线程以后每次进入这个锁的相关的同步块时,虚拟机都可以不再进行同步操作,偏向锁的效率高。
偏向锁的撤销:
偏向锁的撤销必须要等待全局安全点
暂停拥有偏向锁的线程,判断锁对象是否处于被锁定的状态
撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态
轻量级锁:在JDK 6 加入的新型锁机制,在多线程交替执行同步块的情况下,尽量避免重复级锁引起的性能消耗,到那时如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁。
轻量级锁的过程:
当关闭偏向锁或多个线程竞争偏向锁而导致锁升级为轻量级锁,就会尝试获取轻量级锁。
判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word 的拷贝(官方把这份拷贝加了一个Displace前缀,即Displace Mark Word),将对象的Mark Word 复制到栈帧中的。
JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变为00,执行同步操作。
如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是,则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其它线程抢占了,这时候轻量级锁需要膨胀为重量级锁,锁标志位变为10,后面等待的线程将会进入阻塞状态 。

轻量级锁的释放:
取出在获取轻量级锁保存在Displaced Mark Word 中的数据。
用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功。
如果CAS操作替换失败,说明其他线程尝试获取锁,则需要将轻量级锁需要膨胀为重量级



浙公网安备 33010602011771号