【Java面试并发多线程】Synchronized锁的讲解

在我们和面试官吹多线程的时候,我们务必会涉及到锁的问题,这就需要我们死磕并发并深入剖析synchronized底层原理了,希望可以帮到你,哈哈~~

1.面试经验:

面试官问你,你有没有接触过多线程的问题
  • 复习的不错的朋友可以说:有接触到,在项目中因为一些业务,哪哪有使过多线程,怎么使用的,然后再去深入讲解多线程知识。
  • 如果你之前只是粗略的学习过多线程,面试初级开发的话,你可以这样回答:因为项目的限制,很少使用多线程,但是多线程怎么使用还是会的,也做过一些窗口卖票的多线程小案例。

2.线程的并发问题:

在我们执行代码,开启多个线程的时候,如果不加锁会出现线程安全问题。导致数据出错。

3.什么是线程不安全?

我的回答:

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

总结:
Java中的多线程,当多线程对一个数据进行操作时,可能会产生“竞争条件”的现象,这个时候需要对线程的操作进行加锁,来解决多线程操作一个数据时可能产生的问题。加锁方式有两种,一个是申明lock对象来对语句进行加锁,另一种是通过synchronized关键字来对方法进行加锁,以上两种方法都可以有效解决Java多线程中存在的竞争条件的问题。

4.(想聊的话可以说下)深入说明ArrayList线程不安全的原因:

List接口下面有两个实现,一个是ArrayList,另外一个是vector。 从源码的角度来看,因为Vector的方法前加了,synchronized 关键字,也就是同步的意思,sun公司希望Vector是线程安全的,而希望arraylist是高效的,缺点就是另外的优点。 说下原理: 一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:

1. 在 Items[Size] 的位置存放此元素;
2. 增大 Size 的值。

在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

5.如何解决线程不安全的问题:

加锁:jvm内置锁 synchronized lock

6.synchronized的使用

  1. 修饰实例方法,对当前实例对象加锁
  2. 修饰静态方法,多当前类的Class对象加锁
  3. 修饰代码块,对synchronized括号内的对象加锁
public synchronized void f(){ //这个是同步方法
System.out.println("Hello world");
}
public void g(){ 
synchronized (this){ //这个是同步代码块 
System.out.println("Hello world"); 
}
}

同步方法

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info
Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的
ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),
然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

同步代码块

代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

在这里插入图片描述
这里要注意:
synchronized是可重入的,所以不会自己把,自己锁死
synchronized锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。

7.JVM对synchronized的锁优化

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。
1、偏向锁
偏向锁是JDK1.6中引用的优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。

2、轻量级锁
轻量级锁也是在JDK1.6中引入的新型锁机制。它不是用来替换重量级锁的,它的本意是在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

3、重量级锁
Synchronized的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

4、自旋锁
互斥同步对性能影响最大的是阻塞的实现,挂起线程和恢复线程的操作都需要转入到内核态中完成,这些操作给系统的并发性能带来很大的压力。
于是在阻塞之前,我们让线程执行一个忙循环(自旋),看看持有锁的线程是否释放锁,如果很快释放锁,则没有必要进行阻塞。

5、锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是检测到不可能发生数据竞争的锁进行消除。

6、锁粗化
如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

8.synchronized和lock性能比较:

lock使用
lock.lock();
执行业务代码
lock.unlock();

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。

到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

猜你喜欢

转载自blog.csdn.net/csdn_667/article/details/106277024