面试官看了都说好的Synchronized详解

一、什么是互斥同步

互斥是一种制约关系,当一个进程或者多个进程进入临界区后会进行加锁操作,此时其他进程(线程)无法进入临界区,只有当该进程(线程)使用后进行解锁其他人才可以使用这种技术往往是通过阻塞完成。
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock

二、Synchronized各修饰位置效果

1. 同步一个代码块

public void func() {
    synchronized (this) {
        // ...
    }
}

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}


public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}


0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

2. 同步一个方法

public synchronized void func () {
    // ...
}

它和同步代码块一样,作用于同一个对象

3. 同步一个类

public void func() {
   synchronized (SynchronizedExample.class) {
       // ...
   }
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上会进行互斥同步。

public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}


public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

4. 同步一个静态方法

public synchronized static void fun() {
    // ...
}

其效果与作用于类是一样的。

三、Synchronized八锁问题

实战讲解:八锁问题
1、单一普通方法情况

import  java.util.concurrent.TimeUnit;

/**
*1、标准情况下,两个线程先打印  发短信还是  打电话?  答:1/发短信	2/打电话
*2、sendSms延迟4秒,两个线程先打印  发短信还是  打电话? 答: 1/发短信	2/打电话
*/
public  class  Test1  {
	public  static  void  main(String[]  args)  { 
		Phone  phone  =  new  Phone();

		//锁的存在
		new Thread(()->{ 
			phone.sendSms();
		},"A").start();
	
		// 捕 获
		try {
			TimeUnit.SECONDS.sleep(1);
		}  catch  (InterruptedException  e)  {
			e.printStackTrace();
		}
	
		new Thread(()->{
			phone.call();
		},"B").start();
	}
}

class  Phone{

	//  synchronized  锁的对象是方法的调用者!、
	// 两个方法用的是同一个锁,谁先拿到谁执行!
	public  synchronized  void  sendSms(){ 
		try {
			TimeUnit.SECONDS.sleep(4);
		}  catch  (InterruptedException  e)  {
		 e.printStackTrace();
	}
	System.out.println("发短信");
}

public  synchronized  void  call(){ 
	System.out.println("打电话");
	}
}

2、多个普通方法情况

import  java.util.concurrent.TimeUnit;

/**
*3、  增加了一个普通方法后!先执行发短信还是Hello?答://普通方法
*4、 两个对象,两个同步方法, 发短信还是 打电话? 答:// 打电话
*/
public  class  Test2	{
	public  static  void  main(String[]  args)  {
// 两个对象,两个调用者,两把锁! 
	Phone2  phone1  =  new  Phone2(); 
	Phone2  phone2  =  new  Phone2();

	//锁的存在
	new Thread(()->{ 
		phone1.sendSms();
	},"A").start();

	// 捕 获
	try {
		TimeUnit.SECONDS.sleep(1);
	}  catch  (InterruptedException  e)  {
		 e.printStackTrace();
	}

	new Thread(()->{ 
		phone2.call();
	},"B").start();
}

class  Phone2{

	//  synchronized  锁的对象是方法的调用者!
	public  synchronized  void  sendSms(){ 
		try {
			TimeUnit.SECONDS.sleep(4);
	}  catch  (InterruptedException  e)  {
		 e.printStackTrace();
	}
	System.out.println("发短信");
	}

	public  synchronized  void  call(){ 
		System.out.println("打电话");
	}

	// 这里没有锁!不是同步方法,不受锁的影响
	public  void  hello(){ 
		System.out.println("hello");
	}

}

3、多个静态方法情况

import  java.util.concurrent.TimeUnit;

/**
*5、增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话? 答:发短信
*6、两个对象!增加两个静态的同步方法, 先打印 发短信?打电话? 答:打电话
*/
public  class  Test3	{
	public  static  void  main(String[]  args)  {
		//  两个对象的Class类模板只有一个,static,锁的是Class 
		Phone3  phone1  =  new  Phone3();
		Phone3  phone2  =  new  Phone3();

		//锁的存在
		new Thread(()->{
			 phone1.sendSms();
		},"A").start();

		// 捕 获
		try {
			TimeUnit.SECONDS.sleep(1);
		}  catch  (InterruptedException  e)  { 
			e.printStackTrace();
		}

		new Thread(()->{
			phone2.call();
		},"B").start();
	}
}

//  Phone3唯一的一个  Class  对象
class  Phone3{

	//  synchronized  锁的对象是方法的调用者!
	//  static  静态方法
	//  类一加载就有了!锁的是Class
	public  static  synchronized  void  sendSms(){ 
	try {
		TimeUnit.SECONDS.sleep(4);
	}  catch  (InterruptedException  e)  {
		 e.printStackTrace();
	}
	System.out.println("发短信");
	}

	public  static  synchronized  void  call(){ 
		System.out.println("打电话");
	}

4、单个静态方法与单个普通方法情况

import  java.util.concurrent.TimeUnit;

/**
*7、1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话? 答:发短信
*8、1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话? 答:打电话
*/
public  class  Test4	{
	public  static  void  main(String[]  args)  {
		//  两个对象的Class类模板只有一个,static,锁的是Class 
		Phone4  phone1  =  new  Phone4();
		Phone4  phone2  =  new  Phone4();
		//锁的存在
		new Thread(()->{ 
			phone1.sendSms();
		},"A").start();

		// 捕 获
		try {
			TimeUnit.SECONDS.sleep(1);
		}  catch  (InterruptedException  e)  { 
			e.printStackTrace();
		}
		
		new Thread(()->{ 
			phone2.call();
		},"B").start();
	}
}

//  Phone3唯一的一个  Class  对象
class  Phone4{

//  静态的同步方法  锁的是  Class  类模板
	public  static  synchronized  void  sendSms(){ 
		try {
			TimeUnit.SECONDS.sleep(4);
		}  catch  (InterruptedException  e)  { 
			e.printStackTrace();
		}
		System.out.println("发短信");
	}

	// 普通的同步方法	锁的调用者
	public  synchronized  void  call(){ 
		System.out.println("打电话");
	}
}

四、Synchronized中的线程通信

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时。其它线程会调用 notify() 唤醒随机一个挂起线程,或者调用 notifyAll() 来唤醒所有挂起的线程。
它们都属于 Object 的一部分,而不属于 Thread。只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。
使用 wait() 挂起期间,线程会释放锁与CPU。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。而sleep()等待期间,只会释放CPU,而不释放锁

五、JDK 1.6对Synchronized优化

1、锁升级
锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)

(1)偏向锁

为什么要引入偏向锁?

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

偏向锁的升级

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

偏向锁的取消:

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;

如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;

(2)轻量级锁

为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

(3)重量级锁
轻量级锁什么时候升级为重量级锁?
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

*注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。

(4)这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)
在这里插入图片描述
2、锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

3、锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

六、比较Synchronized与ReentrantLock

1、 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2、性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock效率大致相同。
3、 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4、公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5、 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
6、使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

参考:
[1] 【狂神说Java】JUC并发编程最新版通俗易懂
[2] Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值