Synchronized

synchronized原理剖析与优化

同时访问synchronized的静态和非静态方法,能保证线程安全吗?

  结论:不能,两者的锁对象不一样。前者是类锁(XXX.class),后者是this

同时访问synchronized方法和非同步方法,能保证线程安全吗?

  结论:不能,因为synchronized只会对被修饰的方法起作用

两个线程同时访问两个对象的非静态同步方法能保证线程安全吗?

  结论:不能,每个对象都拥有一把锁。两个对象相当于有两把锁,导致锁对象不一致。(PS:如果是类锁,则所有对象共用一把锁)

若synchronized方法抛出异常,会导致死锁吗?

   结论: JVM会自动释放锁,不会导致死锁问题

若synchronized的锁对象能为空吗?会出现什么情况?

  结论:锁对象不能为空,否则抛出NPE(NullPointerException)

类的实例对象在内存中存储分为哪三块区域?(java对象在虚拟机中的布局?

  结论:对象头、实例数据、对齐填充

 

:对象监视器(Object Monitor)

并发编程的三个问题:可见性、原子性、有序性

可见性(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在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

Java内存模型

Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。 Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。 主内存:是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。 工作内存:每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

                         

如果对一个变量执行lock操作,将会清空工作内存中此变量的值 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

主内存与工作内存之间的数据交互过程 :

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方法而被阻塞的线程会被放在该队列中。

每一个Java对象都可以与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。 我们的Java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:monitor并不是随着对象创建而创建的。我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。每个线程都存在两个ObjectMonitor对象列表,分别为free和used列表。同时JVM中也维护着global locklist。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,若不存在则从global list中申请。

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中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和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操作替换失败,说明其他线程尝试获取锁,则需要将轻量级锁需要膨胀为重量级

              

  

           

posted @ 2020-07-31 15:32  王余阳  阅读(462)  评论(0)    收藏  举报