synchronized 可以用来修饰以下 3 个层面:
修饰实例方法;
修饰静态类方法;
修饰代码块。
这种情况锁对象是当前实例对象,因此只有同一个实例对象调用此方法才会产生互斥效果,不同实例对象之间不会有互斥效果。
示例如下:
int num = 1; public synchronized void add() { for (int i=0;i< 5;i++) { num++; System.out.println(Thread.currentThread().getName()+"_"+num); } } public static void main(String[] args) { ZtengSyncSample ztengSyncSample11 = new ZtengSyncSample(); ZtengSyncSample ztengSyncSample22 = new ZtengSyncSample(); Thread mThread1 = new Thread("ztengSyncSample11_mThread1") { public void run() { ztengSyncSample11.add(); }; }; Thread mThread2 = new Thread("ztengSyncSample22_mThread2") { public void run() { ztengSyncSample22.add(); }; }; mThread1.start(); mThread2.start(); }在不同的线程中调用的是不同对象的 add方法,因此彼此之间不会有排斥效果。示例执行结果为
可以看出,两个线程是交互执行的。
如果将代码进行修改,两个线程调用同一个对象的 add方法
public static void main(String[] args) { ZtengSyncSample ztengSyncSample11 = new ZtengSyncSample(); //ZtengSyncSample ztengSyncSample22 = new ZtengSyncSample(); Thread mThread1 = new Thread("mThread1") { public void run() { ztengSyncSample11.add(); }; }; Thread mThread2 = new Thread("mThread2") { public void run() { ztengSyncSample11.add(); }; }; mThread1.start(); mThread2.start(); }执行结果如下:
可以看出:只有一个线程中的代码执行完之后,才会调用另一个线程中的代码。也就是说此时两个线程间是互斥的
如果 synchronized 修饰的是静态方法,那么锁对象是当前类的 Class 对象。因此即使在不同线程中调用不同实例对象,也会有互斥效果。
可以看出,两个线程有互斥效果 并依次执行的
synchronized 还可以作用于代码块
修饰代码块时如果用的是同一对象,则有互斥效果,反之则没有,以上打印如下
synchronized 既可以作用于方法,也可以作用于某一代码块。但在实现上是有区别的
使用 synchronized 作用于代码块
使用 javap -v xxx.class 查看字节码,可以看出,编译而成的字节码中会包含 monitorenter 和 monitorexit 这两个字节码指令
下面字节码中有 1 个 monitorenter 和 2 个 monitorexit,因为虚拟机需要保证当异常发生时也能释放锁。 2 个 monitorexit 一个是代码正常执行结束后释放锁,一个是在代码执行异常时释放锁。
synchronized 修饰方法
被 synchronized 修饰的方法在被编译为字节码后,在方法的 flags 属性中会被标记为 ACC_SYNCHRONIZED 标志。当虚拟机访问一个被标记为 ACC_SYNCHRONIZED 的方法时,会自动在方法的开始和结束或异常位置添加 monitorenter 和 monitorexit 指令。
关于 monitorenter 和 monitorexit,可以理解为一把锁。在这个锁中保存着两个比较重要的属性:计数器和指针。
计数器代表当前线程一共访问了几次这把锁;
指针指向持有这把锁的线程。
计数器默认为0,当执行monitorenter指令时,如果计数器值为0 说明这把锁并没有被其它线程持有。那么这个线程会将计数器加1,并将锁中的指针指向自己。当执行monitorexit指令时,会将计数器减1。
从 Java 6 开始 对 synchronized 关键字做了多方面的优化,减少“重量级锁”的使用次数,并最终减少线程上下文切换的频率,主要做了以下几个优化: 锁自旋、轻量级锁、偏向锁。
所谓自旋,就是让该线程等待一段时间,不会被立即挂起,看当前持有锁的线程是否会很快释放锁。而所谓的等待就是执行一段无意义的循环即可(自旋)。
线程的阻塞和唤醒需要 CPU 从用户态转为核心态,频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力,所以 Java 引入了自旋锁的操作,自旋锁要占用 CPU,如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的 CPU 时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。
对于一块同步代码,虽然有多个不同线程会去执行,但是这些线程是在不同的时间段交替请求这把锁对象,也就是不存在锁竞争的情况。在这种情况下,锁会保持在轻量级锁的状态,从而避免重量级锁的阻塞和唤醒操作。
轻量级锁是在没有锁竞争情况下的锁状态,但是在有些时候锁不仅存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作
