保证主内存与工作内存间数据交互不会出错的8个原子操作:
lock -> read ->load ->use -> assign -> store ->write ->unlock
注意:1.如果对一个变量执行lock操作,会清空工作内存中这个变量的值
2.对一个变量执行unlock操作前,必须把这个变量同步回主内存中。
一.synchronized怎么保证并发编程中的三大特性的?
1.保证原子性:synchronized保证只有一个线程拿到锁,进入同步代码块
//定义一个共享变量number
private static int number=0;
private static Object obj=new Object();
public static void main(String[] args) throws InterruptedException {
Runnable increment=()->{
for(int i=0;i<1000;i++)
{
synchronized(obj){
number++;
}
}
};
List<Thread>list=new ArrayList<>();
for(int i=0;i<5;i++)
{
Thread t=new Thread(increment);
t.start();
list.add(t);
}
for(Thread t:list)
{
t.join();//t线程执行完后才调用main线程方法,就是5个线程执行完再输出number值
}
System.out.println("number为"+number);
}
(为什么obj是static的呢?因为没有static就是两个对象了)
2.保证可见性:这里的synchronized就相当于lock,会让工作内存中的变量清空,然后读取主内存修改后的变量值。
//1.创建一个共享变量
private static boolean flag=false;
private static Object obj=new Object();
public static void main(String[] args) throws InterruptedException {
//2.创建一个线程不断读取共享变量
Thread t1=new Thread(()->{
while(!flag)
{
synchronized(obj)
{
}
}
});
t1.start();
Thread.sleep(2000);
Thread t2=new Thread(()->{
flag=true;
System.out.println("修改了变量的值");
});
t2.start();
}
3.保证有序性:加了synchronized后,依然会发生重排序,只是有同步代码块,可以保证只有一个线程执行同步代码中的代码。
二.synchronized的特性
1.可重入性
(1)什么是可重入?
一个线程可以多次执行synchronized,重复获取同一把锁。
(2)演示synchronized的可重入:
原理:synchronized的锁对象中有个计数器(recursion变量)会记录线程获得几次锁,在执行完同步代码块时,计数器数量会减一,直到计数器数量为0就释放这个锁。
好处:可以避免死锁;可以让我们更好地封装代码
/**演示synchronized的可重入
* 1.自定义一个线程类
* 2.在线程类的run方法中使用嵌套的同步代码块
* 3.使用两个线程来执行
*/
public class Demo01 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
class MyThread extends Thread{
@Override
public void run()
{
synchronized (MyThread.class) {
System.out.println(getName()+"进入同步代码块1");
synchronized (MyThread.class) {
System.out.println(getName()+"进入同步代码块2");
}
}
}
}
2.不可中断性
(1)什么是不可中断性?
一个线程获取锁后,另一个线程必须处于阻塞或等待状态。如果第一个线程不释放锁,另一个线程会一直阻塞等待,且这种状态的不可被中断的。
synchronized是不可中断的
Lock的lock()是不可中断的
Lock的tryLock()是可中断的
(2)代码演示:
public class Demo02 {
private static Object obj=new Object();
public static void main(String[] args) {
Runnable run=()->{
synchronized (obj) {
String name=Thread.currentThread().getName();
System.out.println(name+"进入同步代码块");
try {
Thread.sleep(88000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
Thread t1=new Thread(run);
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Thread t2=new Thread(run);
t2.start();
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
Lock的可中断(test02)和不可中断(test01):
public class Demo03 {
private static Lock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//test01();
test02();
}
public static void test01() throws InterruptedException{
Runnable run=()->{
String name=Thread.currentThread().getName();
try{
lock.lock();
System.out.println(name+"获得锁,进入锁执行");
Thread.sleep(6000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
System.out.println(name+"释放锁");
}
};
Thread t1=new Thread(run);
t1.start();
t1.sleep(1000);
Thread t2=new Thread(run);
t2.start();
System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
public static void test02() throws InterruptedException
{
Runnable run=()->{
String name=Thread.currentThread().getName();
boolean flag=false;
try{
flag=lock.tryLock(3, TimeUnit.SECONDS);
if(flag)
{
System.out.println(name+"拿到锁,进入锁执行");
Thread.sleep(8888);
}
else
{
System.out.println(name+"没有拿到锁,做其他操作");
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
if(flag)
{
lock.unlock();
System.out.println(name+"释放锁");
}
}
};
Thread t1=new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2=new Thread(run);
t2.start();
}
}
三.synchronized的原理
同步代码块:
1.monitorenter指令:每个对象都会和一个监视器monitor关联,监视器被占用时会被锁住,其他线程无法获取该monitor。
当JVM执行到某个线程的某个方法内部的monitorenter时,它会尝试获取当前对象的monitor的所有权。过程如下:
(1)若monitor进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner。
(2)若线程已拥有monitor的所有权,允许它重入monitor,则monitor的进入数+1。
(3)若其他线程已占用monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数为0,才能重新尝试获取monitor的所有权。
总结:
synchronized锁对象会关联一个monitor,monitor才是真正的锁,这个monitor不是我们主动创建的,是JVM线程执行到这个同步代码块,发现锁对象没有monitor,JVM就会创建一个monitor对象,而且这个monitor对象是个c++对象。monitor有两个成员变量:(1)owner:拥有这把锁的线程(2)recursions:记录获取锁的次数。当JVM执行到monitorenter时,他会找到这个对象的monitor,看这个锁是不是被别人拿走了,如果没有别人拿,自己拿走。首先会把owner变成自己,获取锁次数=1.如果里面还有同步代码块,锁还是原来那个,锁就会重入,然后recursions=2.出一个同步代码块,次数就-1.如果有别的线程进来看到owner不是自己就会进入阻塞等待。
2.monitorexit指令:释放锁,它插入在方法结束处或异常处。
(1).能执行monitorexit指令的线程一定是拥有当前对象的monitor所有权的线程。
(2).执行monitorexit时会将monitor的进入数-1.当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试获取这个monitor的所有权。
提问:synchronized出现异常会释放锁吗?
会的,有个Exception table会记录从第几行到第几行,如果有异常会跳过它,最终是会释放锁的。
同步方法:
同步方法会有一个标识:ACC_SYNCHRONIZED.会隐式地调用monitorenter和monitorexit。在执行同步方法之前调用monitorenter,执行后调用monitorexit。
四.深入JVM源码分析synchronized原理
1.在hotspot虚拟机中,monitor是由ObjectMonitor实现的.其源码是用c++实现的。ObjectMonitor主要数据结构如下:
ObjectMonitor(){
_header =NULL;
_count =0;
_waiters =0;
_recursions =0;//线程重入次数
_object =NULL;//存储该monitor的对象
_owner =NULL;//标识拥有该monitor的线程
_WaitSet =NULL;//处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock =0;
_Responsible =NULL;
_succ =NULL;
_cxq =NULL;//多线程竞争锁时的单项链表
FreeNext =NULL;
_EntryList =NULL;//处于等待block状态的线程,会被加入到该列表
_SpinFreq =0;
_SpinClock =0;
_OwnerIsThread =0;
}
2.何时会出现monitor竞争?
emm源码自己去看吧
流程:(1)通过CAS尝试把monitor的owner字段设置为当前线程。
(2)如果设置成功,说明竞争到这个锁,如果以前是这个线程,现在又竞争到就recursions++,记录重入次数
(3)如果当前线程是第一次竞争到这个锁,recursions就=1,owner为当前线程。
(4)如果没竞争到就处于阻塞状态,进入cxq队列。
3.monitor等待:调用的是ObjectMonitor对象的Enterl方法
流程:(1)把当前线程封装成ObjectWait对象node,状态设置成ObjectWaiter::TS_CXQ
(2)在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
(3)node节点push到_cxq列表中,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒。
(4)当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁
4.monitor释放
具体实现位于ObjectMonitor的exit方法中。
(1)退出同步代码块时会_recursion值-1,当为0时,说明线程释放了锁。
(2)根据不同策略(QMode决定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作由unpark完成。【QMode==2,绕过EntryList从cxq队列中获取线程用于竞争锁;QMode==3,cxq队列插入EntryList尾部;QMode==4,cxq队列插入到EntryList头部】
五.monitor是重量级锁
执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的会unpark()唤醒。park(),unpark()属于内核函数。这时候就存在os用户态和内核态的转换,这种切换会消耗大量系统资源。所以synchronized是一个重量级的操作。
六.jdk1.6中对synchronized的优化
CAS:compare and swap,比较并交换。是一种对内存中的共享数据进行操作的一种指令。
作用:CAS可以将比较和交换转为原子操作,这个原子操作由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS有三个操作数:内存值V,旧的预期值A,要修改的更新值B。如果旧的预期值A=内存值V,那就把内存值V改为更新值B。
CAS——AtomicInteger:保证原子性
public class Demo04 {
private static AtomicInteger atomicInteger=new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable run=()->{
for(int i=0;i<1000;i++)
atomicInteger.incrementAndGet();
};
List<Thread>list=new ArrayList<>();
for(int i=0;i<5;i++)
{
Thread t=new Thread(run);
t.start();
list.add(t);
}
for(Thread t:list)
{
t.join();
}
System.out.println(atomicInteger);
}
}
(出于本人的好奇去查了一下:AtomicInteger中的getAndIncrement()和incrementAndGet()有什么区别?
字面解释上没什么区别都是以原子方式将当前值+1。getAndIncrement()返回的是旧值current,incrementAndGet()返回的是新值next。)
CAS原理:
Unsafe类提供原子操作。Unsafe类能直接操作内存空间,但是它不太安全。所以java官方不推荐用。Unsafe对象不能直接调用,只能通过反射获得。
AtomicInteger成员变量有:value用volatile修饰,valueOffset偏移量:它可以根据对象的内存地址+偏移量就能找到value的内存地址,就能找到对应的值,并且修改它。
incrementAndGet()源码:
private voltatile int value=0;
public final int incrementAndGet(){
return unsafe getAndAddInt(this,valueOffset,1)+1;
}
getAndAddInt源码:
public final int getAndAddInt(Object var1,long var2,int var4){
int var5;
do{
var5=this.getIntVolatile(var1,var2);
}while(!this.compareAndSwap(var1,var2,var5,var5+var4);
return var5;
}
根据var1+var2找到value最新值,var5是旧的预估值,var4是更新值,var1是这个对象,var2是偏移量。如果根据var1+var2找到的最新值=var5旧的预期值,就比较成功,返回true,前面有个!,跳出这个循环。反之会接着进入while循环接着比较,直到相同。
CAS获取共享变量时,为了保证变量的可见性,要用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU场景下。如果竞争激烈,一直比较频繁发生,效率会受到影响。
Synchronized锁升级过程:
synchronized在jdk1.6之后不会直接变成重量级锁。
它会这么个过程:无锁 - > 偏向锁 - >轻量级锁 - >重量级锁
在JVM中,对象在内存中的布局分为三块:对象头(分为Mark Word,klass pointer)、实例数据、对齐数据。
在64位虚拟机下,Mark Word是64bit大小的,存储结构如下:
锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
|
| Cms_free | 分代年龄 | 偏向锁 | 锁标志位 | |
无锁 | unused | hashCode |
|
| 0 | 01 |
偏向锁 | ThreadID(54bit) Epoch(2bit) |
|
| 1 | 01 | |
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||
重量级锁 | 指向重量级锁的指针 | 01 |
klass pointer:JVM通过这个指针确定对象是哪个类的实例。64位的JVM它就为64位,它可以压缩成32位。
对齐数据:如果java对象整体大小是8的整数倍,就不用对齐。如果不是就加点对齐的填充数据,变成8的整数倍。这样方便操作系统寻址。
1.偏向锁(适用没有竞争的状态)
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
偏向锁的意思是:偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只用检查是否为偏向锁、锁标志位、ThreadID。
但是一旦出现多个线程竞争时就得撤销偏向锁。
偏向锁原理:(1)线程第一次访问同步块获取锁时,JVM把对象头标志位改为01,偏向锁改为1
(2)用CAS操作将获取这个锁的线程ID记录在Mark Word中,如果CAS成功,持有偏向锁的线程以后每次进入这 个锁的同步块时,JVM可以不进行任何同步操作,从而效率提高。
(3)有两个线程竞争锁的时候就会撤销偏向锁。
2.轻量级锁(多线程交替执行同步块,不存在同时竞争)
当关闭偏向锁或者多个线程竞争偏向锁就会导致锁升级为轻量级锁,会尝试获取轻量级锁,步骤:
(1)判断当前对象是否处于无锁状态,如果是:JVM在当前线程的栈帧中建一个锁记录(Lock Record)的空间,将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Record中的owner指向当前对象。
(2)JVM利用CAS操作将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,将锁的标志位变为00,执行同步操作。
(3)如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,是:表示当前线程已持有当前对象的锁,直接执行同步代码块。否:该锁对象被其他对象抢占,轻量级锁升级为重量级锁,标志位变10,后面等待的线程进入阻塞状态。
原理:将对象的Mark Word复制到栈帧中的Lock Record中,Mark Word更新为指向Lock Record的指针。
3.自旋锁
自旋等待本身避免了线程切换的开销,但它要占用处理器时间,如果锁被占用时间很短,效果会很好,反之,自旋的线程只会白白消耗CPU。自旋次数默认10,超过了就升级为重量级锁。这个默认次数改为多少合适?多了,浪费CPU,少了没抢到锁。很难判断。由此引入适应性自旋锁。
3.1适应性自旋锁
自旋时间不再固定,由前一次在同一个锁的自旋时间及拥有者状态决定。假设一个线程自旋10次后获得锁,JVM就会认为这个同步代码块自旋容易获得锁,在之后的也会自旋,且允许自旋时间长一点。假设一个线程自旋从来没成功过,JVM就认为这个同步代码块很难抢到锁,就不自旋了。
4.锁消除
JVM即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
5.锁粗化
如果JVM检测到那种零散的用一个对象加锁很多次的,就会把加锁同步范围扩展(粗化)到整个操作序列的外部,这样只加锁一次就行。
比如:
StringBuffer sb=new StringBuffer();
for(int i=0;i<100;i++)
sb.append("aa");
StringBuffer的append方法本身是个同步方法,这样写就意味着sb调用100次循环,记录同步代码块100次,出来100次。JVM就会粗化它,将append()方法的synchronized抹掉,粗化成这样:
StringBuffer sb=new StringBuffer();
synchronized("")
{
for(int i=0;i<100;i++)
sb.append("aa");
}
七.平时写代码对synchronized的优化:
(1)减少synchronized的范围:同步代码块中尽量短,减少同步代码块中代码执行时间,减少锁竞争。
(2)降低synchronized锁的粒度:将一个锁拆分成多个锁,提高并发度。(eg:Hashtable和ConcurrentHashMap)
(3)读写分离。读取时不加锁,写和删除时加锁。