【Java并发编程】之synchronized关键字

锁定的是对象

public class T {
	private Object o=new Object();
	private int count=10;
	public void m() {
		synchronized(o) {
			count--;
			System.out.println(Thread.currentThread().getName()+"count="+count);
		}
	}
	
}

1.当一个线程想要去执行这段代码,必须要获得o的锁,当o被其他线程占用时,该线程必须要等其他线程释放o的锁,再去获得o的锁,才能执行。

2.synchronized关键字锁定的是对象不是代码块,demo中锁的是object对象的实例

3.可能锁对象包括: this 临界资源对象,Class 类对象

4.关于线程安全:加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。

5.synchronized关键字修饰普通方法等同于synchronized(this)

静态方法上锁

/*
 * 静态方法加锁相当于给T.class文件枷锁
 * */
public class T {
	private static int count=10;
	public synchronized static void m() {
		count--;
		System.out.println(Thread.currentThread().getName()+"count="+count);
	}
}

给静态方法上锁,锁定的是类对象,类的.class文件是唯一的,所以说synchronize修饰静态方法或者锁定的对象是类的.class文件的时候在多线程中是可以实现线程安全的.。

这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系

/*
 * 静态方法加锁相当于给T.class文件枷锁
 * */
public class T {
	private static int count=10;
	public synchronized static void m() {
		count--;
		System.out.println(Thread.currentThread().getName()+"count="+count);
	}
	public static void main(String args[]) {
		
		for(int i=0;i<5;i++) {
			T t=new T();
			new Thread(()->t.m(),"Thread"+i).start();
		}
	}
}

输出结果: 

 

这里因为sychronized修饰的是静态方法,所以不管new多少个对象,锁定的都是同一个类对象,所以是线程安全的。

要是将static去掉,那么就不是线程安全的了。

同步和非同步方法同时调用

public class T {
	public synchronized void  m1(){
		System.out.println("m1 start------");
		try {
			Thread.sleep(10000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("m1 end--------");
	}
	public void m2() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("m2-----");
	}
	public static void main(String args[]) {
		T t=new T();
		//相当于new 一个线程,在run方法里执行m1 lamda表达式
		/*new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				
			}
			
		});
		*/
		new Thread(()->t.m1(),"t1").start();
		new Thread(()->t.m2(),"t1").start();
	}
}

 线程t1首先获得了当前对象t的锁,并执行m1。因为m2非同步的,不需要获得锁就可以执行,所以t2不需要获得锁就可以直接执行m2.只有执行synchronized方法才需要申请那把锁。

可重入锁 

public class Test1 {
	synchronized void m1() {
		System.out.println(Thread.currentThread().getName()+" m1");
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		m2();
	}
	synchronized void m2() {
		System.out.println(Thread.currentThread().getName()+" m2");
	}
	public static void main(String[] args) {
		Test1 t=new Test1();
		// TODO Auto-generated method stub
		new Thread(()->{
			t.m1();
		},"t1").start();
		new Thread(()->{
			t.m1();
			
		},"t2").start();;
	}

}



输出:
t1 m1
t1 m2
t2 m1
t2 m2

所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的,synchronized和ReentrantLock都是可重入锁。可重入锁的意义在于防止死锁。实现原理实现是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,jvm讲记录锁的占有者,并且讲请求计数器置为1 。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。可重入锁锁定的必须得是同一个对象。

父类和子类的可重入锁:

public class Father {
	synchronized void m1() {
		System.out.println(" father");
	}
}


public class Son extends Father {
	@Override
	synchronized void m1() {
		// TODO Auto-generated method stub
		super.m1();
		System.out.println(" sun");
	}
}


	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Son s=new Son();
		s.m1();
	}


输出:
 father
 sun

如果一个线程有子类对象的引用loggingWidget,然后调用loggingWidget.doSomething方法的时候,会请求子类对象loggingWidget 的对象锁;又因为loggingWidget 的doSomething方法中调用的父类的doSomething方法,实际上还是要请求子类对象loggingWidget 的对象锁,那么如果synchronized 关键字不是个可重入锁的话,就会在子类对象持有的父类doSomething方法上产生死锁了。正因为synchronized 关键字的可重入锁,当前线程因为已经持有了子类对象loggingWidget 的对象锁,后面再遇到请求loggingWidget 的对象锁就可以畅通无阻地执行同步方法了。

读脏数据

public class Account {
	String name;
	double balance=0;
	public synchronized void set(String name,Double balance) {
		this.name=name;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.balance=balance;
	}
	public double getBalance() {
		return balance;
	}
	public static void main(String args[]) {
		Account a= new Account();
		new Thread(()->a.set("aaaa",100.0)).start();
		System.out.println(a.getBalance());
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(a.getBalance());
	}
}

再给getBalance加锁就可以解决该问题了。

电商领域中,不考虑业务逻辑上的脏读,只考虑数据上的。比如说订单并不是实时展现的。

程序执行过程中,如果出现异常,默认情况下锁会被释放。

所以,在并发执行过程中,有异常处理要多加小心,不然可能会发生不一致的情况。

比如,多个servlet线程访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程会进入同步代码区,有可能访问到异常数据。

Java对象头

synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成 。Class Metadata Address存储的是该对象属于类的地址,即可以判断这个对象属于哪一个类。

MarkWord有五种类型:

MarkWord:

重量级锁(sychronized):

锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联。只有获取到对象的monitor的线程,才可以执行方法或代码块,其他获取失败的线程会被阻塞,并放入同步队列中,进入BLOCKED状态。

Monitor

当我们使用synchronized修饰方法名时,编译后会在方法名上生成一个ACC_SYNCHRONIZED标识来实现同步;当使用synchronized修饰代码块时,编译后会在代码块的前后生成monitorenter和monitorexit字节码来实现同步。

无论使用哪种方式实现,本质上都是对指定对象相关联的monitor的获取,只有获取到对象的monitor的线程,才可以执行方法或代码块,其他获取失败的线程会被阻塞,并放入同步队列中,进入BLOCKED状态。

为了解决线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一问题内只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。

举一个例子:

我们可以把监视器理解为包含一个特殊的房间的建筑物,这个特殊房间同一时刻只能有一个客人(线程)。这个房间中包含了一些数据和代码。

如果一个顾客想要进入这个特殊的房间,他首先需要在走廊(Entry Set)排队等待。调度器将基于某个标准(比如 FIFO)来选择排队的客户进入房间。如果,因为某些原因,该客户客户暂时因为其他事情无法脱身(线程被挂起),那么他将被送到另外一间专门用来等待的房间(Wait Set),这个房间的可以可以在稍后再次进入那件特殊的房间。如上面所说,这个建筑屋中一共有三个场所。

总之,监视器是一个用来监视这些线程进入特殊的房间的。他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。

Monitor的实现

数据结构:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

关键属性:

_owner:指向持有ObjectMonitor对象的线程

_WaitSet:存放处于wait状态的线程队列

_EntryList:存放处于等待锁block状态的线程队列

_recursions:锁的重入次数

_count:用来记录该线程获取锁的次数

当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。

若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

monitor

 

 

sychronized代码块底层实现 

monitorenter monitorexit

sychronized方法底层实现 

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

等待唤醒机制与synchronized


所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

synchronized的可重入性


从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,monitor的count会加一,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。


参考:

https://blog.csdn.net/javazejian/article/details/72828483#%E7%90%86%E8%A7%A3java%E5%AF%B9%E8%B1%A1%E5%A4%B4%E4%B8%8Emonitor

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值