Java并发编程实战笔记二(加锁机制)

2.2 加锁机制

       当多个线程对于共享变量进行并发访问时我们需要通过加锁来保证它的线程安全性。

 

2.2.1 内置锁

     Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

简单的来说就是synchronized的两种用法:

  1. 对象锁:方法(锁对象默认this)和同步代码块
  2. 类锁:静态方法和锁Class对象
//对象锁
	public synchronized void test1(){
		
	}
	//默认为this
	public void test2(){
		synchronized (this) { //手动指定类对象
		}
	}
	
//类锁
	public static synchronized void test3(){
		
	}
	public void test4(){
		synchronized (SynchronizedTest.class) {
		}
	}

    每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块(无论是正常流程退出还是异常退出)时自动释放锁。获得内置锁的唯一途径就是进入由这个锁保护代码的同步代码块或方法。

   Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放锁,如果线程B永远不释放,那么线程A将永远地等待下去。

 

2.2.2 重入

      一个线程可以多次获取同一把锁。如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”。重入的一种实现方法是:为每个锁关联一个获取计数值和一个所有者线程,当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将计下锁的持有者,并且将计数值加1;如果同一个线程再次获得这个锁,计数值将递增,而线程退出同步代码块时,计数器会相应递减。当计数值为0时,这个锁将被释放。

模拟示例:

// 重入锁 
	public synchronized void reentrantLock() { // count + 1 = 1
		synchronized (this) { // count + 1 = 2	
		} // 代码块执行完毕 count - 1 = 1
	} // 方法执行完毕 count - 1 = 0  释放锁

   如果内置锁是不可重入的,那么上面这段代码将产生死锁,线程进入方法时获取到当前对象锁,执行代码块时因为这个对象锁已经被持有,从而线程将永远的停顿下去,等待一个永远也无法获得的锁,重入则避免了这种死锁的发生。  

      重入进一步的提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。

 

2.2.3 实现原理

  1. 用在同步方法时会在字节码生成一个 ACC_SYNCHRONIZED 标识符,当一个线程去访问方法时,会去检查是否存在这个标识,如果存在,则先要获取对应的 monitor 锁,然后执行方法,在方法执行结束(正常/异常)会释放对应的 monitor 锁
  2. 用在同步代码块时会在字节码生成一对 monitorenter/monitorexit 指令 ;当线程执行到 monitorenter 指令时,就会去尝试获取对应的 monitor 锁,当执行到 monitorexit 指令时,会释放对应的 monitor 锁
  • 它们底层都是通过 monitor 对象来实现的,每个对象都会与一个 monitor 对象关联,monitor 包含的属性 owner(持有对象的线程)、count(重入次数)、waitSet(waiting 队列)、entryList(blocking 队列);
  • 在获取到锁时(count+1,owner = 当前线程),同一个线程再次获取则(count+1),释放锁时(count-1,owner = null)
  • 在锁被其他线程持有时,未获取到锁的线程会放入 EntryList 队列中去阻塞并进入 BLOCKING 状态,当持有锁的线程释放锁之后会从这个队列尾部(先进后出)中唤醒一个线程去获取锁
  • 持有锁的线程在调用 wait() 将释放锁,该线程会放入 WaitSet 队列中去阻塞并进入 WAITING 状态,当持有锁的线程调用 notify() / notifyAll() 时会把 WaitSet 队列中的 一个/全部 线程放入到 EntryList 队列中,释放锁后(同上步骤)唤醒一个线程去获取锁

 

tips:

     对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

     当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或耗时任务),一定不要持有锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值