作用
保证多个线程同一时刻只能有一个线程执行同步块
分类使用
- 对象锁
- 修饰在普通方法的同步锁
- 使用方法
public synchronized void method(){ }- 修饰在代码块的同步锁
public void method(){ synchronized(this){// 或者是其他的对象锁 // dosomething } } - 修饰在普通方法的同步锁
- 类锁(锁的对象是class对象)一个类可以有多个对象,但是只有一个class对象
- 修饰在static方法的同步块
public static synchronized void staticMethod(){ }- 使用类名.class修饰的同步代码块
public void staticMethod(){ synchronized(类名.class){ } }
注意事项
- synchronize同步块里面抛出异常后或者执行完毕才会释放锁
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待
- 每个对象实例都会有自己的一把锁,不同实例相互不影响
性质
- 可重入锁
- 概念:同一线程的外层函数获取锁之后,内层函数可以直接再次获取该锁(意思是:获取锁的方法里面调用了需要再次获取该锁的方法,这时候可以直接获取)
- 好处:避免死锁,提升封装性
- 可重入得粒度:线程范围内
- 不可中断
- 意思是:如果锁被其他线程获取到了,如果该线程想要获取锁只能等待获取锁的线程释放,如果获取锁的线程不释放锁,那么就只能永远等待下去,这就是不可中断性质。
原理
- 加锁释放锁的原理:现象、时机、查看字节码
- 现象:每一个Java实例对象都会有一把锁,每一个synchronize代码块都会指定一个锁对象,在执行同步代码块之前必须要获取该对象的锁方能执行,否则该线程会进入阻塞(blocking)状态,一旦线程获取到锁就会独占这把锁,不能被其他线程抢夺,直到同步代码块执行完毕或者执行中抛出异常jvm会自动释放该锁,后面阻塞的线程就会竞争这把锁,获取到进入同步代码块,否则继续阻塞。
- 时机:每次获取锁的时候会检查该锁的计数器是否为零,如果是获取该锁并给计数器加一,否则等待其他获取该锁的线程释放锁,如果一直没有释放就一直等待,因为synchronize不可中断。
- 查看字节码:进入到同步块执行monitorenter指令,退出时会执行monitorexit
- 可重入得原理:通过加锁的次数计数器,每次执行monitorenter并且加锁计数器加一,退出时计数器减一
- 保证可见性的原理:Java内存模型主要控制主内存和本地内存的间的通信来保证内存可见性,synchronize在释放锁的时候会同步本地内存到主内存中来保证内存可见性。
缺陷
- 效率低:锁的释放情况少(第一种,同步块执行完毕,第二种同步块抛出异常),试图获取锁不能设定超时(Lock锁可以设定超时),不能中断试图获取锁的线程(Lock可以中断)
- 不够灵活,加锁条件单一,只能使用对象头的锁,加锁和释放锁时机单一
- 无法知道是否成功获取到锁
面试准备
- 使用注意点:避免锁对象不能为空(锁的信息是实例对象的头部中的),作用域不宜过大(多线程的目的是提高运行效率,如果作用域过大,导致跟同步执行没有区别),避免产生死锁(注意加锁的顺序)
- 如何选择lock和synchronized关键字?
- 能避免使用锁就尽量不使用
- 如果能使用关键字synchronized解决问题尽量使用,原因可以减少编程带来的问题,如果需要使用到lock的特性,比如需要中断、需要设定超时时间,就是使用lock。
- 多线程访问同步方法的具体情况
总结
JVM会自动通过monitor来加锁和释放锁,并且保证同时只有一个线程能执行同步代码块,从而保证线程安全,同时具有了可重入性和不可中断性。
思考题
- 多个线程等待同一个synchronized锁的时候,JMV如何选择下个获取锁的线程?
- 什么是锁的升级、降级?什么事jvm里面的偏斜锁、轻量级锁、重量级锁?