synchronized

synchronized作用范围

  1. 对于普通同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前类的Class对象。
  3. 对于同步方法块,锁是synchronized括号里面配置的对象。

对于上面的作用范围,该如何理解呢?可以看一下下面的例子。

对于普通同步方法,锁是当前实例对象

对于普通同步方法,锁是当前实例对象。说明同一时刻内的同一个实例只能有一个线程来拥有。

public class SynchronizedExample1 implements Runnable{
	
	private static int i = 0;
	
	public synchronized void increase() {
		System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
		i++;
	}

	@Override
	public void run() {
		for(int i=0; i<10; i++) {
			
			increase();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		SynchronizedExample1 runnable = new SynchronizedExample1();
		
		Thread thread1 = new Thread(runnable);
		Thread thread2 = new Thread(runnable);
		
		thread1.start();
		thread2.start();
		
        // 目的:为了T1和T2全部执行完成之后再打印i的值
		thread1.join();
		thread2.join();
		System.out.println("final i = " +i);
	}
}

每个线程输出结果可能不一样,但是最终结果一定会是20。

Thread-0 - current i ==> 0
Thread-1 - current i ==> 1
Thread-1 - current i ==> 2
Thread-1 - current i ==> 3
Thread-1 - current i ==> 4
Thread-1 - current i ==> 5
Thread-1 - current i ==> 6
Thread-1 - current i ==> 7
Thread-1 - current i ==> 8
Thread-1 - current i ==> 9
Thread-1 - current i ==> 10
Thread-0 - current i ==> 11
Thread-0 - current i ==> 12
Thread-0 - current i ==> 13
Thread-0 - current i ==> 14
Thread-0 - current i ==> 15
Thread-0 - current i ==> 16
Thread-0 - current i ==> 17
Thread-0 - current i ==> 18
Thread-0 - current i ==> 19
final i = 20

可以看出每个线程获得的 i 都是上一个线程执行完的结果。

你一定很奇怪,难道结果不是这样吗?或者说结果就应该是这样。为了凸显出 synchronized 的作用,我把 increase 的synchronized 关键词去掉,同时为了效果明显将循环次数提高到1000000并且增加竞争线程数量。

public class SynchronizedExample1 implements Runnable{
	
	private static int i = 0;
	
	public void increase() {
		System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
		i++;
	}

	@Override
	public void run() {
		for(int i=0; i<200000; i++) {
			
			increase();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		SynchronizedExample1 runnable = new SynchronizedExample1();
		
		Thread thread1 = new Thread(runnable);
		Thread thread2 = new Thread(runnable);
		Thread thread3 = new Thread(runnable);
		Thread thread4 = new Thread(runnable);
		Thread thread5 = new Thread(runnable);
		Thread thread6 = new Thread(runnable);
		Thread thread7 = new Thread(runnable);
		Thread thread8 = new Thread(runnable);
		Thread thread9 = new Thread(runnable);
		
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
		thread5.start();
		thread6.start();
		thread7.start();
		thread8.start();
		thread9.start();
		
		thread1.join();	
		thread2.join();
		thread3.join();
		thread4.join();	
		thread5.join();
		thread6.join();
		thread7.join();	
		thread8.join();
		thread9.join();
		System.out.println("final i = " +i);
	}
}

此时最终结果就不一定是1800000。

这是为什么呢?

原因是 i++ 这个操作不是原子操作。

i++ 实际具体的操作可以分为三个步骤:

  1. 先将i从主内存中取出,放到线程的工作内存中。每个线程的工作内存都是不共享的。
  2. 在进行i+1操作,更新线程工作内存中的i值。
  3. 最后根据工作内存中的值,改变主内存中的i值。

i++ 并不是我们想想中的那样,一个线程改完,另一个线程能够直接获取最新的值。在没有锁的并发环境下,很可能出现的情况就是两个线程都获取了主内存中的 i 值,接着随后分别进行 increase 方法,最后可能的结果就是执行一次 increase 的结果。

对于静态同步方法,锁是当前类的Class对象。

普通同步方法,是针对于类的某一个实例对象。而静态同步方法可以理解为针对类的 所有 实例对象。

我们稍微改一下Example 1 中的代码。

public class SynchronizedExample2 implements Runnable{
	
	private static int i = 0;
	
	public synchronized void increase() {
		System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
		i++;
	}

	@Override
	public void run() {
		for(int i=0; i<1000000; i++) {
			
			increase();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		
        // 这里的T1和T2用的是2个不同实例的线程。
		Thread thread1 = new Thread(new SynchronizedExample2());
		Thread thread2 = new Thread(new SynchronizedExample2());
		
		thread1.start();
		thread2.start();
		
		thread1.join();	
		thread2.join();
		
		System.out.println("final i = " +i);
	}
}

多运行几次,你会发现结果都是不一样的。为了让方法在多实例下的并发结果一致,我们可以将其方法设置为 静态

public class SynchronizedExample2 implements Runnable{
	
	private static int i = 0;
	
    // 将 increase 方法改为静态方法
	public synchronized static void increase() {
		System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
		i++;
	}

	@Override
	public void run() {
		for(int i=0; i<1000000; i++) {
			
			increase();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		
		Thread thread1 = new Thread(new SynchronizedExample2());
		Thread thread2 = new Thread(new SynchronizedExample2());
		
		thread1.start();
		thread2.start();
		
		thread1.join();	
		thread2.join();
		
		System.out.println("final i = " +i);
	}
}

结果就是2000000。

对于同步方法块,锁是synchronized括号里面配置的对象

什么叫括号里面配置的对象呢?先看一下代码的形式。

// object 可以是 实例 也可以是class类
synchronized([object]) {
    // code block
}

简单点可以理解为同步代码块可以实现锁某一个实例,可以锁一个类的所有实例。

让我们看看同步代码块的实现效果。

实现普通方法同步效果

public class SynchronizedExample3 implements Runnable{
	
	private static int i = 0;
	
	public void increase() {
		System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
		i++;
	}

	@Override
	public void run() {
        // 锁了当前实例
		synchronized (this) {
			for(int i=0; i<1000000; i++) {
				increase();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		// 用一个实例
		SynchronizedExample3 runnable = new SynchronizedExample3();
		
		Thread thread1 = new Thread(runnable);
		Thread thread2 = new Thread(runnable);
		
		thread1.start();
		thread2.start();
		
		thread1.join();	
		thread2.join();
		
		System.out.println("final i = " +i);
	}
}

结果为2000000。

实现静态同步方法效果

public class SynchronizedExample3 implements Runnable{
	
	private static int i = 0;
	
	public void increase() {
		System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
		i++;
	}

	@Override
	public void run() {
        // 锁了SynchronizedExample3所有的实例
		synchronized (SynchronizedExample3.class) {
			for(int i=0; i<1000000; i++) {
				increase();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		// 用不同的实例来启动线程
		Thread thread1 = new Thread(new SynchronizedExample3());
		Thread thread2 = new Thread(new SynchronizedExample3());
		
		thread1.start();
		thread2.start();
		
		thread1.join();	
		thread2.join();
		
		System.out.println("final i = " +i);
	}
}

结果为2000000。

锁的优化与升级

在讲锁之前,先来了解一下 java 对象的结构。

由此可见 Mark Word 是对象头的一部分。

JDK6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。在 JDK6 中锁的状态一共有 4 种:“无锁状态”、“偏向锁”、“轻量级锁”和“重量级锁”。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

4种锁状态下的标志位

Mark Word 里面存储的数据会随着锁标志位的变化而变化。

锁名称 锁标志位 偏向锁标志
无锁 01 0
偏向锁 01 1
轻量级锁 00 0
重量级锁 10 0

偏向锁

特点:

  1. 减少 同一线程 重复获取对象锁所带来的性能消耗。

  2. 偏向锁被占用的时候,其他线程可以通过 CAS 来获取偏向锁。

  3. 偏向锁使用了一种等到竞争出现才释放锁的机制,即只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁的获取

  1. 访问对象头中 Mark Word 里面的偏向锁标志是否为 1 ,锁标志位是否为 01 。 偏向锁的状态 — 偏向标志为 1 ,锁标志位为 01 。

  2. 如果为偏向状态,则 测试 线程 ID 是否指向当前线程。如果是,进入步骤5,否则,进入步骤3。

  3. 如果线程 ID 未指向当前线程,则通过 CAS 来竞争锁。如果竞争成功,则将Mark Word 中的偏向线程 ID 指向当前线程,进入步骤 5;否则,说明竞争失败,进入步骤 4。

  4. 如果 CAS 竞争失败,说明存在竞争。此时会执行 偏向锁撤销 动作。偏向锁会升级为 轻量级锁 ,然后被阻塞在安全点的线程继续往下执行同步代码。

  5. 执行同步代码。

偏向锁的释放

禁用偏向锁

在JDK6和JDK7中,偏向锁默认是开启的。可以使用JVM参数来关闭偏向锁:-XX:-UseBaisedLocking=false,程序就会默认进入轻量级锁。

轻量级锁

获取轻量级锁

  1. 线程访问同步块。

  2. 在当前线程的工作内存中开辟一块内存空间用以储存 锁记录(Lock Record)

  3. 将对象头中的 Mark Word (这部分的信息又称为Displaced Mark Word) 信息复制到锁记录中。

  4. 当前线程尝试CAS将对象头中的 Mark Word 内容替换成指向 锁记录(Lock Record) 的指针,并将同步对象的 Owner Thread 指向当前线程。若成功替换,执行步骤 6;否则执行步骤 5。

  5. 进行自旋获取锁。如果在一定时间内或者一定次数之后(这里只是我的猜测,但是一定会有一个条件来判断是否自旋获取锁失败),若仍然失败,锁将会膨胀为 重量级锁

  6. 线程获取对象锁,执行同步代码块。

解锁轻量级锁

使用CAS操作将Displaced Mark Word替换掉对象头中的 Mark Word。如果成功,表示没有出现线程竞争;如果失败,表示当前存在线程竞争,锁就会膨胀成重量级锁。

偏向锁和轻量级锁之间的区别

锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要 额外 的消耗,和执行 非同步 方法相比仅存在纳秒级差别 如果出现线程竞争会带来额外的 锁撤销 消耗 使用只有 一个线程 访问同步块场景
轻量级锁 竞争的线程不会被阻塞,提高了程序的响应速度 如果线程始终得不到锁,使用自旋会消耗CPU 适用于追求 响应时间,同步块执行速度非常块的场景
重量级锁 线程竞争不会自旋,不会额外消耗CPU 线程阻塞,响应速度慢 适用于追求 吞吐量,同步块执行时间较长的场景

参考

BACK