synchronized锁是Java中的一种重量级锁。他有三种实现方法:
下面逐个分析下各种实现方式。
对代码块加锁
先来看一段示例:
public class Test implements Runnable{ private int index = 1 ; private final static int MAX = 100;//总票数是100张 @Override public void run(){ while (index<MAX ){ //如果已卖出的数量小于总票数,继续卖 System.out.println(Thread.currentThread() + "的电影票编号是:"+ (index));//出票 index++; //卖出的票数加1 } } public static void main(String[] args) { final Test task = new Test(); //5个线程卖票 new Thread(task,"一号窗口").start(); new Thread(task,"二号窗口").start(); new Thread(task,"三号窗口").start(); new Thread(task,"四号窗口").start(); new Thread(task,"五号窗口").start(); } }
这是一段经典的卖票示例代码。运行之后就会发现出了问题(至于为什么会出问题,后面会专门出一篇文章来分析,这篇文章只是侧重学习下synchronized锁的原理)。
这时候我们只要对会出现并发问题的代码加上synchronized锁就能正常运行了。(这里使用的是对代码块加锁的方式)
public void run(){ synchronized (MUTEX) { while (index<=MAX ) { //如果还有余票,继续卖 System.out.println(Thread.currentThread() + "的电影票编号是:" +(index));//出票 index++; //售出的票数加1 } } }
synchronized锁为什么能解决多线程问题呢,在idea中使用插件查看JVM指令可以看到加上synchronized锁的run方法的JVM指令中多了下面两个指令
点开monitorenter指令,这个插件会自动帮我们跳转到官方文档的页面。可以看到对monitorenter有如下描述:
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
翻译下就是,每个对象都关联一个monitor(管程),这个监视器只有被占有时,它才会锁住。正在执行monitorenter指令的
线程A会尝试获取monitor的所有权。具体执行规则如下:
1.如果monitor的计数器为0,线程将进入monitor并将计数器设置为,然后线程就占有这个monitor了。
2.如果线程已经占有monitor,它会重入monitor,并将计数器继续加1.
3.如果另一个线程此时占有了该monitor,线程A就会一直阻塞,直到monitor的计数器变为0时,他才会尝试重新获取monitor的所有权。
再点开monitorexit指令,可以看到下面的描述:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
说明执行monitorexit指令的线程必须是monitor的占有者。线程执行monitorexit指令后会将monitor的计数器减一,当计数器的值变成0时,线程将退出monitor。其他被阻塞的线程将尝试获取monitor的所有权。
通过上面的说明,我们可以知道这两个指令的作用了,通过对monitor分别执行加减来维护monitor的状态,当monitor的状态是0时,表示monitor可以进入,当monitor的状态大于0时,表示有线程正在占有monitor,其他线程要等待。
试一下对整个方法加锁。
@Override public synchronized void run(){ while (index<=MAX ) { //如果还有余票,继续卖 System.out.println(Thread.currentThread() + "的电影票编号是:" + (index));//出票 index++; //售出的票数加1 } }
这时候再查看JVM指令会发现monitor指令居然不见了。通过上面的分析我们知道那对monitor指令是来对线程加锁的,那么为什么对整个方法加锁的时候,这对指令没有使用呢。通过查找资料,得到下面的解释:方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之间。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要先成功持有monitor,然后才能执行方法,最后当方法完成时释放monitor(无论是否正常完成都会释放)。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获取到同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个方法所持有的monitor将在异常抛到同步方法之外时自动释放。