前言

本篇博客是《Java锁深入理解》系列博客的第三篇,建议依次阅读。
各篇博客链接如下:
Java锁深入理解1——概述及总结Java锁深入理解2——ReentrantLockJava锁深入理解3——synchronizedJava锁深入理解4——ReentrantLock VS synchronizedJava锁深入理解5——共享锁

Demo

//作用在方法上。锁对象就是当前对象,临界区就是整个方法
	public synchronized void m1() {
		System.out.println("----------do something...");
	}
	
	//作用在代码块
	final Object lock = new Object();//定义一个锁对象
	public void m2() {
		synchronized (lock) {
			System.out.println("----------do something...");
		}
	}

这两个方法被调用时,在synchronized的包围圈内(方法内或者,代码块内),都可以保证线程安全(但线程要调用同一个对象的这两个方法,否则锁对象都变了,就不存在保证线程安全了)。
效果和ReentrantLock的lock-unlock一样。

/**
	 * 锁对象:当前类的对象
	 * 功能:阻塞
	 */
	public synchronized void m4(){
		System.out.println("----------m4");
		try {
			this.wait();
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 锁对象:lockObj
	 * 功能:阻塞
	 */
	public  void m5(){
		synchronized(lockObj) {
			System.out.println("----------m5");
			try {
				lockObj.wait();
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
		}
	}

这两个方法演示了wait方法都用法:wait写在synchronized包围的临界区内。
用法和功能和ReentrantLock的await方法一样。甚至底层实现方法都是park(给操作系统一个信号量,将线程上下文保存之后挂起来)

public synchronized void m6() {
		this.notify();
	}

上面这个方法演示了notify方法都用法:同样要写在synchronized包围的临界区内。用法和功能和ReentrantLock的signal方法一样。

注意:synchronized作为一个关键字,可以写在很多地方:

作用位置


方法上

当前对象

静态方法上

当前类

成员变量上

成员变量

只要同一个锁,那么同一时刻只能有一个线程访问这个锁管控的资源。即便是两块不同的资源,只要被同一个锁 锁住,那么同时只能一个线程访问。
典型的就是一个类中多个方法都用了synchronized,那么同一时刻只有一个方法可以被一个线程访问。(没有synchronized的方法不受锁限制)

内部机制

从synchronized的发展来讲,可以这么理解:
一开始synchronized就相当于ReentrantLock(原理也基本一样),也就是现在所说的其中的重量级锁。后来发现性能太低,所以增加了轻量级锁和偏向锁。增加了锁升级机制。

深层原因,synchronized和ReentrantLock都是MESA管程模型的不同实现。下面是他们各自一些关键概念的对照:

MESA模型

同步队列

条件队列

wait

notify

AQS

state

同步队列(sync queue)

条件队列(condition queue)

await

signal

synchronize

count

_EntryList

_WaitSet

wait

notify

synchronized和ReentrantLock不一样的地方

  1. synchronized由JVM实现,是C++写的。AQS由JDK实现,Java写的。
  2. synchronized只有非公平锁。可以认为synchronized和ReentrantLock的非公平锁实现的原理基本一致。
  3. synchronized可以把所有对象当成锁。因为每个对象都可生成一个对应的ObjectMonitor(类似AQS),而且每个对象头上都还有锁标记。
  4. synchronized存在锁升级过程(后面详细讲这一点)
  5. synchronized退出锁(解锁)同样需要CAS。因为前面的”锁升级“。自己还在临界区里没出来,外面拿不到锁”气急败坏“的线程,可能就把锁升级了。
  6. synchronized不会被中断。(后面会讲)

synchronized锁升级

Java起初并没有这个机制,使用synchronized就是重量级锁,需要在用户态-内核态之间切换,性能消耗较大。但实际应用场景中,多数情况下,锁竞争并没有那么激烈。重量级锁带来的性能问题显得没有必要。自1.6后优化的功能。

  1. 无锁状态
    如果没有线程访问,那么这个锁就是无锁状态。
  2. 偏向锁
    当一个线程发现锁是“无锁”状态,那么它就会进行自旋CAS获得锁(改锁状态为“偏向锁”,同时把自己的线程ID写入锁对象头中)
  3. 轻量级锁
    当一个线程发现锁是“偏向锁”状态,那么它会进行CAS尝试获得锁(一般情况下都能获得到)。
    但如果它CAS失败,那么它就会把这个锁升级为轻量级锁(把锁状态改为“轻量级”,然后在自己帧栈中新增一块空间,来存锁头内容。把自己这块空间的地址存到锁对象头中)。
  4. 重量级锁
    当一个线程发现锁是“轻量级锁”,它同样会进行CAS尝试获得锁。
    如果它CAS失败,那么它就会把这个锁升级为重量级别锁。

锁升级后会降级吗

无论说不会(第一层传统认知),还是会(第二层)都不完全对

说不会的,那就是网上的普遍说法。然后这还导致很多人产生这样的认知:锁对象被改成重量级锁之后,之后就算没线程访问,依然保持重量级锁的状态。

我们做实验就很容易发现这个认知是错误的。如这篇博客就指出了这个问题。但其实这种认知也是不完全正确的。也就是说,一开始被广泛传播的说法并非空穴来风。只是表述有问题,才引起了大家误解。

正确的解释是:它不会降级,但会在释放锁之后直接回归无锁状态(之后有线程访问,还会继续升)。
但是,并不是说前一个线程释放锁之后,锁立马回归无锁状态,而是要等个几秒。如果前一个线程释放完锁,然后唤醒了后面的线程继续运行,说明此时处于激烈竞争时期,锁是不会回归无锁状态的,毕竟升降都很消耗性能。

(在上面那篇博客的评论区里,我也指出了这个问题,并给出了一个佐证)

大家可以根据那个博客自己做实验验证,挺简单的。

关于synchronized,我就不多讲了,因为它的重量级锁(也就是它最核心的锁)和ReentrantLock的非公平锁是一样的,都是管程的实现。代码逻辑也几乎一样,这里就不再重复了。如果你想看详细分析一下,请看这篇文章(关于synchronized,这篇文章讲的挺详细的)。

synchronized是怎么释放锁的

看一下demo的反编译

每个用户 加锁 java java锁的对象_Java

使用synchronized关键字后,代码逻辑有两个出口(monitorexit):一个正常出口,一个异常出口。
所以:synchronized无论是正常结束,还是异常结束,都会有JVM自动释放锁