JUC高级二: Java锁(上篇)
1. 乐观锁和悲观锁
synchronized关键字和Lock的实现类都是悲观锁
悲观锁
: 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。- 适合写操作多的场景,先加锁可以保证写操作时数据正确
- 显式的锁定之后在操作同步资源
乐观锁
:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是
CAS算法
,Java原子类中的递增操作就通过CAS自旋实现的 - 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
- 乐观锁一般有两种实现方式:
- 采用版本号机制
CAS(Compare-and-Swap,即比较并替换)
算法实现
- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是
2. 8锁案例
2.1 标准访问有ab两个线程,请问先打印邮件还是短信
package site.zhourui.juc.locks;
import java.util.concurrent.TimeUnit;
class Phone //资源类
{
public synchronized void sendEmail()
{
System.out.println("-------sendEmail");
}
public synchronized void sendSMS()
{
System.out.println("-------sendSMS");
}
}
/**
* 标准访问有ab两个线程,请问先打印邮件还是短信
* 结果:
* -------sendEmail
* -------sendSMS
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
phone.sendSMS();
},"b").start();
}
}
执行结果:
2.2 sendEmail方法暂停3秒钟,请问先打印邮件还是短信
package site.zhourui.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");
}
}
/**
* sendEmail方法暂停3秒钟,请问先打印邮件还是短信
* 结果:
* -------sendEmail
* -------sendSMS
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
phone.sendSMS();
},"b").start();
}
}
执行结果:
2.3 新增一个普通的hello方法,请问先打印邮件还是hello
package site.zhourui.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");
}
}
/**
* 新增一个普通的hello方法,请问先打印邮件还是hello
* 结果:
* -------hello
* 三秒后
* -------sendEmail
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
// phone.sendSMS();
phone.hello();
},"b").start();
}
}
执行结果:
2.4 有两部手机,请问先打印邮件还是短信
package site.zhourui.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");
}
}
/**
* 有两部手机,请问先打印邮件还是短信
* 结果:
* -------sendSMS
* 三秒后
* -------sendEmail
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
// phone.sendSMS();
// phone.hello();
phone2.sendSMS();
},"b").start();
}
}
执行结果:
2.5 两个静态同步方法,同1部手机,请问先打印邮件还是短信
package site.zhourui.juc.locks;
import java.util.concurrent.TimeUnit;
class Phone //资源类
{
public static synchronized void sendEmail()
{
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-------sendEmail");
}
public static synchronized void sendSMS()
{
System.out.println("-------sendSMS");
}
public void hello()
{
System.out.println("-------hello");
}
}
/**
* 两个静态同步方法,同1部手机,请问先打印邮件还是短信
* 结果:
* 三秒后
* -------sendEmail
* -------sendSMS
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
// Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
phone.sendSMS();
// phone.hello();
// phone2.sendSMS();
},"b").start();
}
}
执行结果:
2.6 两个静态同步方法, 2部手机,请问先打印邮件还是短信
package site.zhourui.juc.locks;
import java.util.concurrent.TimeUnit;
class Phone //资源类
{
public static synchronized void sendEmail()
{
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-------sendEmail");
}
public static synchronized void sendSMS()
{
System.out.println("-------sendSMS");
}
public void hello()
{
System.out.println("-------hello");
}
}
/**
* 两个静态同步方法, 2部手机,请问先打印邮件还是短信
* 结果:
* 三秒后
* -------sendEmail
* -------sendSMS
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
// phone.sendSMS();
// phone.hello();
phone2.sendSMS();
},"b").start();
}
}
执行结果:
2.7 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
package site.zhourui.juc.locks;
import java.util.concurrent.TimeUnit;
class Phone //资源类
{
public static 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");
}
}
/**
* 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
* 结果:
* -------sendEmail
* 2.7秒后
* -------sendSMS
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
phone.sendSMS();
// phone.hello();
// phone2.sendSMS();
},"b").start();
}
}
执行结果:
2.8 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
package site.zhourui.juc.locks;
import java.util.concurrent.TimeUnit;
class Phone //资源类
{
public static 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");
}
}
/**
* 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
* 结果:
* -------sendEmail
* 2.7秒后
* -------sendSMS
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
// phone.sendSMS();
// phone.hello();
phone2.sendSMS();
},"b").start();
}
}
执行结果:
2.9 8锁执行总结
-
锁一锁二
- 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
- 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
-
锁三锁四
- 加个普通方法后发现和同步锁无关
- 换成两个对象后,不是同一把锁了,情况立刻变化,即两把锁不会锁住对方
-
锁五锁六
三种 synchronized 锁的内容有一些差别:
- 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
- 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
- 对于同步方法块,锁的是 synchronized 括号内的对象
-
锁七锁八
当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
- 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
- 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class,具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
2.10 个人总结(8锁)
主要的思路就是判断锁对象是否一致,判断我们的锁是
对象锁
还是类锁
,只要锁对象不相同就不会出现加锁阻塞的情况
8锁问题讲的是synchronized,而synchronized 有三种加锁方式
-
同步方法:锁的是当前实例对象,通常指this
- 如果两个调用对象是相同的那么就是同一把锁,会出现锁的情况
- 如果如果两个调用对象不是相同的那么则无影响,就算是相同类的两个对象也是一样.
-
静态同步方法:静态同步方法,锁的是当前类的Class对象
- 众所周知一个类在jvm中只有一个对应的Class对象,所以调用同一个类的两个静态同步方法一定是会被锁住的.
-
同步代码块:时我们手动指定加锁对象比较容易判断,即判断两个线程的锁对象是否一致
3. 从字节码角度分析synchronized实现
3.0 前置知识javap反编译
javap -c ***.class文件反编译
- -c 对代码进行反汇编
假如你需要更多信息
- javap -v ***.class文件反编译
- -v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
3.1 synchronized同步代码块
package site.zhourui.juc.locks;
/**
* synchronized同步代码块字节码实现
*/
public class LockByteCodeDemo {
Object object = new Object();
public void m1()
{
synchronized (object){
System.out.println("----------hello sync code");
}
}
public static void main(String[] args) {
}
}
执行一下后用在控制台找到字节码文件进行反编译
小总结:
synchronized同步代码块实现使用的是monitorenter和monitorexit指令
3.1.2 一定是一个enter两个exit吗?
m1方法里面自己添加一个异常试试
package site.zhourui.juc.locks;
/**
* synchronized同步代码块字节码实现
*/
public class LockByteCodeDemo {
Object object = new Object();
public void m1()
{
synchronized (object){
System.out.println("----------hello sync code");
throw new RuntimeException("------exp");//添加异常
}
}
public static void main(String[] args) {
}
}
执行一下后用在控制台找到字节码文件进行反编译
回答不是:在程序出异常的情况下只有一个一个enter一个exit
3.2 synchronized同步代码方法
package site.zhourui.juc.locks;
/**
* synchronized同步代码方法字节码实现
*/
public class LockByteCodeDemo {
public synchronized void m2()
{
System.out.println("----------hello static synchronized m2");
}
public static void main(String[] args) {
}
}
我们使用-v查看更多信息
javap -v .\LockByteCodeDemo.class
同步方法字节码的实现是通过加标志位
ACC_SYNCHRONIZED
实现的调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor
3.3 synchronized静态同步方法
package site.zhourui.juc.locks;
/**
* synchronized静态同步代码方法字节码实现
*/
public class LockByteCodeDemo {
public static synchronized void m3()
{
System.out.println("----------hello static synchronized m3");
}
public static void main(String[] args) {
}
}
我们使用-v查看更多信息
javap -v .\LockByteCodeDemo.class
静态同步方法字节码的实现是通过加标志位
ACC_STATIC
和ACC_SYNCHRONIZED
实现的
4. 反编译synchronized锁的是什么(为什么任何一个对象都可以成为一个锁?)
回答:因为在HotSpot虚拟机中,monitor采用ObjectMonitor实现,而我们每个java类的父类都是Object类,所以每个对象天生都带着一个对象监视器,所以为什么任何一个对象都可以成为一个锁
4.1 C++源码解读
-
c++ 源码解读 : ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp
-
objectMonitor.hpp中定义了几项重要的属性
5. 公平锁和非公平锁
5.1 公平锁
是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
ReentrantLock reentrantLock = new ReentrantLock(true);
5.2 非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
ReentrantLock reentrantLock = new ReentrantLock(false);//默认false
ReentrantLock reentrantLock = new ReentrantLock();//默认非公平锁
5.3面试题
5.3.1 为什么会有公平锁/非公平锁的设计为什么默认非公平?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
5.2.2 使⽤公平锁会有什么问题
公平锁保证了排队的公平性,不会造成锁饥饿,但是会增加线程的开销
非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”
5.2.3 什么时候用公平?什么时候用非公平?
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
- 否则那就用公平锁,大家公平使用。
5.4 从ReentrantLock卖票编码演示公平和非公平现象
package site.zhourui.juc.locks;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Ticket
{
private int number = 50;
private Lock lock = new ReentrantLock(); //默认用的是非公平锁,分配的平均一点,=--》公平一点
public void sale()
{
lock.lock();
try
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number);
}
}finally {
lock.unlock();
}
}
}
public class SaleTicketDemo {
public static void main(String[] args)
{
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"c").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"d").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"e").start();
}
}
默认非公平锁时执行结果:
线程a直接将票卖完了
公平锁执行结果:
所有线程都参与进来了,没有出现线程饥饿的问题
5.5 预埋伏AQS(抽象队列同步器)
6. 可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
6.1 为什么会出现可重入锁
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
6.2 可重入锁种类
6.2.1 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
package site.zhourui.juc.locks;
public class ReEntryLockDemo {
public static void main(String[] args)
{
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println("-----外层调用");
synchronized (objectLockA)
{
System.out.println("-----中层调用");
synchronized (objectLockA)
{
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}
验证结果:该锁可以递归调用
6.2.2 显式锁(即Lock)也有ReentrantLock这样的可重入锁。
package site.zhourui.juc.locks;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReEntryLockDemo {
public static void main(String[] args)
{
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t"+"-----外层");
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t"+"-----内层");
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println("------22222");
}finally {
lock.unlock();
}
},"t2").start();
}
}
执行结果: 该锁可以递归调用
6.3 Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
- 当执行monitorenter时,如果目标锁对象的计数器
_count
为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程_owner
设置为当前线程,并且将其计数器_count
加1。 - 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器
_count
加1并且锁的重入次数_recursions
加一,否则需要等待,直至持有线程释放该锁。 - 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。如果
_recursions
不为0_recursions
同时减一,计数器为零代表锁已被释放。
7. 死锁及排查
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
7.1 产生死锁的主要原因:
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
7.2 一个死锁代码示例
package site.zhourui.juc.locks;
import java.util.concurrent.TimeUnit;
public class DeadLockDemo {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args)
{
Thread a = new Thread(() -> {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
}
}
}, "a");
a.start();
new Thread(() -> {
synchronized (lockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+" 自己持有B锁,期待获得A锁");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lockA)
{
System.out.println(Thread.currentThread().getName()+"\t 获得A锁成功");
}
}
},"b").start();
}
}
执行结果:
7.3 如何排查死锁
7.3.1 纯命令
-
jps -l
查询我们程序执行的进程id -
我们推测DeadLockDemo出现了死锁,
jstack 进程编号
,如果出现死效果如下图:
7.3.2 图形化
-
控制台输入jconsole,选中猜测死锁的线程点击连接
-
选择线程选项点击检测死锁
-
查看是否有死锁信息,有死锁的话会自动显示出来
8. 总结
指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)