Java学习之synchronized

保证主内存与工作内存间数据交互不会出错的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)读写分离。读取时不加锁,写和删除时加锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值