天天看点

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

作者:做好一个程序猿

乐观锁/悲观锁

乐观锁

乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用“数据版本机制”或“CAS操作”来实现。eg: 文档在线编辑,git等都是用的乐观锁

悲观锁

悲观锁认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁并发操作一定会出问题。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。

synchronized八种案例

package com.atguigu.juc.locks;


import java.util.concurrent.TimeUnit;

class Phone {
    public synchronized void sendEmail() {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }

    public void hello() {
        System.out.println("-------hello");
    }
}

public class Lock8Demo {
    public static void main(String[] args){
        Phone phone = new Phone();//资源类1
        Phone phone2 = new Phone();//资源类2

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            //phone.sendSMS();
            //phone.hello();
            phone.sendSMS();
        },"b").start();
    }
}
           

案例

  1. 一部手机标准访问有ab两个线程,请问先打印邮件还是短信 (普通方法)
  2. sendEmail方法暂停3秒钟,请问先打印邮件还是短信 (普通方法)
  3. 新增一个普通的hello方法,请问先打印邮件还是hello (普通方法)
  4. 有两部手机,请问先打印邮件还是短信 (普通方法)
  5. 两个静态同步方法,同1部手机,请问先打印邮件还是短信
  6. 两个静态同步方法, 2部手机,请问先打印邮件还是短信
  7. 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
  8. 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信

说明

1-2

  • 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

3-4

  • 加个普通方法后发现和同步锁无关,hello
  • 换成两个对象后,不是同一把锁了,情况立刻变化。

5-6 都换成静态同步方法后

三种 synchronized 锁的内容有一些差别:

  • 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁,也就是说所得是实例对象本身。
  • 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
  • 对于同步方法块,锁的是 synchronized 括号内的对象

7-8

  • 当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
  • 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
  • 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
  • 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

从字节码角度分析synchronized

javap -c ***.class文件反编译,假如你需要更多信息 javap -v ***.class文件反编译。

synchronized 同步代码块

首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?我们先看一段简单的代码:

package com.paddx.test.concurrent;
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}           

查看反编译后结果:_ _

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

反编译结果

monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
    2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
    3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异常退出释放锁;防止出现锁无法释放,并发问题。

通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

一定是一个enter两个exit吗?

手动throw 一个异常的情况,在程序正常执行时也抛出一个异常,那么无论在抛出异常的代码之前的代码正常还是异常,程序最后都会走异常处理的流程,就不需要正常的monitorexit处理,这样字节码中就会只有一个monitorexit

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

synchronized 普通同步方法

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}           

查看反编译后结果:

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

synchronized 静态同步方法

ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

反编译synchronized锁的是什么

什么是管程monitor

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

为什么任何一个对象都可以成为一个锁

因为每个对象都内置了一个ObjectMonitor监视器对象,这个监视器是用c++写的,底层依赖操作系统的互斥量mutex实现的加锁解锁。

Object.java → ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

为什么会有公平锁/非公平锁的设计为什么默认非公平?

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized
  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

可重入锁

可重入锁”的概念:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时锁还没释放,当再次获取这个对象锁的时候还可以获取。对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Reentrant Lock 重新进入锁。

对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

Java 锁(一)乐观锁/悲观锁/公平锁/非公平锁/synchronized

继续阅读