在并发编程中存在线程安全问题,主要缘由有:1.存在共享数据 2.多线程共同操做共享数据。关键字synchronized能够保证在同一时刻,只有一个线程能够执行某个方法或某个代码块,同时synchronized能够保证一个线程的变化可见(可见性),便可以代替volatile。java
synchronized能够保证方法或者代码块在运行时,同一时刻只有一个方法能够进入到临界区,同时它还能够保证共享变量的内存可见性,它能够:git
Java中每个对象均可以做为锁,这是synchronized实现同步的基础:github
在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是相似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。编程
每一个对象分为三块区域:对象头、实例数据和对齐填充数组
锁状态 | 25bit | 4bit | 1bit是不是偏向锁 | 2bit锁标记位 |
---|---|---|---|---|
无锁 | 对象的haahcode | 分代年龄 | 0 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 合并第一列 | 合并第一列 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 合并第一列 | 合并第一列 | 10 |
GC标志 | 空 | 合并第一列 | 合并第一列 | 11 |
偏向锁 | 线程ID(23bit)和Epoch(2bit) | 对象分代年龄 | 1 | 01 |
如上表在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息安全
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争状况逐渐升级。锁能够升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提升得到锁和释放锁的效率。bash
从语法上讲,Synchronized能够把任何一个非null对象做为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。能够把它理解为 一个同步工具,也能够描述为 一种同步机制,实现了在一个时间点,最多只有一个线程在执行管程的某个子程序,这个机制的保障来源于监视锁Monitor,每一个对象都拥有本身的监视锁Monitor。数据结构
咱们能够把监视器理解为一个医院,医院里面只要一个医生,每次只能看一个病人(线程),若是一个病人想看病,他首先要在走廊里面排队(Entry Set),依次进入看病,可是假如某个正在看病的人可能晕血或者血糖低不能暂时继续看病(线程被挂起),这时候不能强行给他看,也不能让后面的病人等他一个,因而就要送他到休息室去休息(Wait Set),休息室里面呆的都是由于各类缘由不能继续看病的病人,等休息好了,还能够继续去看病。以下图多线程
在Java虚拟机(HotSpot)中,Monitor是基于C++实现的ObjectMonitor,其主要数据结构以下并发
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //指向当前持有ObjectMonitor对象的线程
_WaitSet = NULL; //存放wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //这是一个和_WaitSet相似存等待线程的地方,
//可是是否存在这里是要根据Policy的值(这里不知道说的对不对,顺便说下,这玩意儿每次看都觉得是cxk)
FreeNext = NULL ;
_EntryList = NULL ; //存放处于等待锁的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
复制代码
当多个线程同时访问一段同步代码时,首先进入 _EntryList
,当某个线程获取到对象的monitor
后进入_Owner
区域并把monitor
中的_owner
变量设置为当前线程,同时_count
加一,得到对象锁。 若是持有monitor的线程被挂起(例如调用wait方法),将释放当前持有的monitor,_owner
变量回复为null,_count
减一,同时该线程进入_WaitSet
队列中等待被唤醒(notify),若是当前线程顺利执行完代码块后会释放monitor
并复位变量的值,以便下一个线程进来获取monitor
锁,下面看个例子。
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
System.out.printf("synchronized");
}
function();
}
private static void function() {
System.out.printf("function");
}
}
上面的代码中有一个同步代码块,锁住的是类对象,而且还有一个同步静态方法,锁住的依然是该类的类对象。下面是字节码文件
public class com.example.javalib.SynchronizedDemo {
public com.example.javalib.SynchronizedDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/example/javalib/SynchronizedDemo
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method function:()V
18: return
复制代码
上面的4,6,12行就是须要注意的部分了,这是添加Synchronized关键字以后才会出现的。执行同步代码块首先要执行monitorenter
,退出的时候执行monitorexit
指令。 使用Synchronized之因此可以进行同步,其关键就是对对象的监视器monitor
的获取,当执行线程获取到monitor后才能继续执行下去,不然只能继续等待。 上面的demo中同步代码块后还有一个静态方法,这个方法是同步的,并且该方法锁的对象依然是这个类对象,那么执行线程就没必要再去获取这个锁,从字节码中能够看到,有一条monitorenter
指令和两条monitorexit
指令,并无第二次获取锁的指令,这就是锁的重入性:即在同一个锁程中,线程不须要去再次获取同一把锁,Synchronized先天具备重入性。每一个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。 任意一个对象都拥有本身的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,若是没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态,关于线程的状态能够看这篇文章
从上面咱们知道了sychronized
加锁的时候,会调用objectMonitor
的enter
方法,解锁的时候会调用exit方法。事实上,只有在JDK1.6以前,synchronized
的实现才会直接调用ObjectMonitor
的enter
和exit
,这种锁被称之为重量级锁。为何说这种方式操做锁很重呢? 由于Java的线程是映射到操做系统原生线程之上的,若是要阻塞或唤醒一个线程就须要操做系统的帮忙,这就要从用户态转换到核心态,所以状态转换须要花费不少的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,因此说synchronized是java语言中一个重量级的操纵。 因此,在JDK1.6中出现对锁进行了不少的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有 只不过默认的是关闭的,jdk1.6是默认开启的),这些操做都是为了在线程之间更高效的共享数据 ,解决竞争问题。
以上文章是解决一个同步问题时发现synchronized知识点只知其一;不知其二后查找资料后摘抄的笔记,算是本身我的的整理,漏了什么欢迎指出来。