JUC并发编程与源码分析笔记04-说说Java“锁”事

从轻松的乐观锁和悲观锁开讲

乐观锁

认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等。
判断规则:

  1. 版本号机制
  2. CAS算法(常用),Java原子类中的递增操作就是通过CAS自旋实现的

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized关键字和Lock的实现都是悲观锁。
适合写操作多的场景,先加锁保证写操作时数据的正确。

通过8种情况演示锁运行案例,看看我们到底锁的是什么

锁相关的8种案例演示code

案例1

创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()方法,另一个线程调用sendMessage()方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
这个地方,锁的是对象,也就是Phone实例,只要实例不释放锁,其他方法调用就得等着。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(phone::sendEmail).start();
        Thread.sleep(1000);
        new Thread(phone::sendMessage).start();
    }
}

class Phone {
    public synchronized void sendEmail() {
        System.out.println("Phone.sendEmail");
    }

    public synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }
}

案例2

创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()方法(sendEmail()方法里有Thread.sleep操作),另一个线程调用sendMessage()方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
一个对象里有多个synchronized方法,某一时刻,一个线程调用其中一个synchronized方法,其他线程是拿不到这个对象的锁的,就要等待,而且sleep操作不会释放锁,所以sendMessage()方法只能等待sendEmail()释放锁之后才能执行。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(phone::sendEmail).start();
        Thread.sleep(1000);
        new Thread(phone::sendMessage).start();
    }
}

class Phone {
    public synchronized void sendEmail() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Phone.sendEmail");
    }

    public synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }
}

案例3

创建一个Phone类的实例,分别启动两个线程,一个线程调用sendEmail()方法,另一个线程调用sendHello()方法(sendHello()方法是普通方法,没有synchronized修饰),打印顺序是什么?
先输出Phone.sendHello,后输出Phone.sendEmail。
sendHello()方法没有synchronized修饰,也就不需要争抢资源,所以先输出了sendHello。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(phone::sendEmail).start();
        Thread.sleep(1000);
        new Thread(phone::sendHello).start();
    }
}

class Phone {
    public synchronized void sendEmail() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Phone.sendEmail");
    }

    public synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }

    public void sendHello() {
        System.out.println("Phone.sendHello");
    }
}

案例4

创建两个Phone类的实例,启动两个线程,一个线程调用实例1的sendEmail()方法,另一个线程调用实例2的sendMessage()方法,打印顺序是什么?
先输出Phone.sendMessage,后输出Phone.sendEmail。
因为sendEmail()sendMessage()是两个对象,synchronized锁的是对象,也就是两个锁,各锁各的,这两个对象在执行方法的时候,不存在竞争。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(phone1::sendEmail).start();
        Thread.sleep(1000);
        new Thread(phone2::sendMessage).start();
    }
}

class Phone {
    public synchronized void sendEmail() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Phone.sendEmail");
    }

    public synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }
}

案例5

创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()静态方法,另一个线程调用sendMessage()静态方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
注意,这里方法上加了static修饰,所以synchronized锁的就不是对象了,锁的是类,那么这个类的所有实例都会受到影响。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> phone.sendEmail()).start();
        Thread.sleep(1000);
        new Thread(() -> phone.sendMessage()).start();
    }
}

class Phone {
    public static synchronized void sendEmail() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Phone.sendEmail");
    }

    public static synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }
}

案例6

创建两个Phone类的实例,启动两个线程,一个线程调用实例1的sendEmail()静态方法,另一个线程调用实例2的sendMessage()静态方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
对于普通的同步方法,锁的是实例对象,通常指this,具体的某个对象,所有普通同步方法用的都是同一把锁,即实例对象本身,对于静态同步方法,所的是当前类的Class对象,当前类创建的不同实例共享这一个锁,对于tongue方法块,锁定的是synchronized括号内的对象。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> phone1.sendEmail()).start();
        Thread.sleep(1000);
        new Thread(() -> phone2.sendMessage()).start();
    }
}

class Phone {
    public static synchronized void sendEmail() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Phone.sendEmail");
    }

    public static synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }
}

案例7

创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()静态方法,另一个线程调用sendMessage()方法,打印顺序是什么?
先输出Phone.sendMessage,后输出Phone.sendEmail。
这里的锁,一个加在了类上,一个加在了对象上,锁的是两个东西,两者不冲突,所以先输出的Phone.sendMessage。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> phone.sendEmail()).start();
        Thread.sleep(1000);
        new Thread(() -> phone.sendMessage()).start();
    }
}

class Phone {
    public static synchronized void sendEmail() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Phone.sendEmail");
    }

    public synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }
}

案例8

创建两个Phone类的实例,启动两个线程,一个线程调用实例1的sendEmail()静态方法,另一个线程调用实例2的sendMessage()方法,打印顺序是什么?
先输出Phone.sendMessage,后输出Phone.sendEmail。
同理,一个是对象锁,一个是类锁,他俩并没有资源的竞争。
当一个线程试图访问同步代码的时候,它必须先获取到锁,正常退出、抛出异常的时候都会释放锁。

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> phone1.sendEmail()).start();
        Thread.sleep(1000);
        new Thread(() -> phone2.sendMessage()).start();
    }
}

class Phone {
    public static synchronized void sendEmail() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Phone.sendEmail");
    }

    public synchronized void sendMessage() {
        System.out.println("Phone.sendMessage");
    }
}

synchronized有三种应用方式

synchronized同步代码块、synchronized普通同步方法、synchronized静态同步方法。

从字节码角度分析synchronized实现

javap -c ***.class:文件反编译
javap -v ***.class:文件反编译(更详细,v:verbose)

synchronized同步代码块:

public class LockDemo {
    final Object object = new Object();

    public void fun() {
        synchronized (object) {
            System.out.println("LockDemo.fun");
            // throw new RuntimeException("xxx");// 如果手动抛出异常,那么monitorenter和monitorexit就是一对一了
        }
    }

    public static void main(String[] args) {
    }
}

通过对class文件反编译javap -c ***.class,可以看出synchronized同步代码块的原理是:使用monitorentermonitorexit指令来完成,但是这里会发现两个monitorexit,分别对应正常退出和异常退出两种情况的释放锁。

synchronized普通同步方法:

public class LockDemo {
    public synchronized void fun() {
        System.out.println("LockDemo.fun");
    }

    public static void main(String[] args) {
    }
}

通过对class文件反编译javap -v ***.class,可以看到fun上会有一个ACC_SYNCHRONIZED标识。Java虚拟机会检查方法上有没有ACC_SYNCHRONIZED标识,如果有,执行线程会先持有monitor锁,然后再执行方法,最后在方法完成(正常完成或异常退出)时候释放锁。

synchronized静态同步方法:

public class LockDemo {
    public static synchronized void fun() {
        System.out.println("LockDemo.fun");
    }

    public static void main(String[] args) {
    }
}

通过对class文件反编译javap -v ***.class,可以看到fun上会有两个标识:ACC_STATIC ACC_SYNCHRONIZED,由此就可以确定方法是普通同步方法还是静态同步方法了。

反编译synchronized锁的是什么

Java里的每一个对象都可以持有锁,我们知道所有类的父类是Object,根据Java底层源码是C++来看,Java里的Object对应的C++里的ObjectMonitor,然而ObjectMonitor就带着一个对象监视器,也就是monitor,所以每个Java对象都可以持有锁。

公平锁与非公平锁

ReentrantLock卖票demo演示公平和非公平

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        }, "thread1").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        }, "thread2").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        }, "thread3").start();
    }
}

class Ticket {
    private int number = 50;
    ReentrantLock reentrantLock = new ReentrantLock(true);// 不传参时候,默认非公平锁

    public void sale() {
        reentrantLock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第" + number-- + "张,还剩" + number + "张");
            }
        } finally {
            reentrantLock.unlock();
        }
    }
}

何为公平锁/非公平锁

公平锁:多个线程按照申请锁的顺序来获取锁,类似排队买票,先来的人先买,后来的人在队尾排着。
非公平锁:多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,可能造成优先级翻转或者饥饿状态。
为什么会有公平锁/非公平锁?为什么默认非公平锁?
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平锁?什么时候用非公平锁?
如果为了更高的吞吐量,采用非公平锁更合适,因为节省了多线程切换的时间,吞吐量自然就高了,否则就采用公平锁。

可重入锁(又名递归锁)

说明

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是一个有synchronized修饰的递归调用方法,程序第二次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

“可重入锁”这四个字分开来解释

可:可以
重:再次
入:进入
锁:同步锁
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入,自己可以获取自己的内部锁。

可重入锁种类

隐式锁

隐式锁默认是可重入锁,在一个synchronized修饰的方法或者代码块内部,调用本类的其他synchronized修饰的方法或者代码块时候,是永远可以得到锁的。
synchronized同步代码块

public class LockDemo {
    public static void main(String[] args) {
        final Object object = new Object();
        new Thread(() -> {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + ":外层调用");
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + ":中层调用");
                    synchronized (object) {
                        System.out.println(Thread.currentThread().getName() + ":内层调用");
                    }
                }
            }
        }).start();
    }
}

synchronized同步方法

public class LockDemo {
    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(lockDemo::fun1, "threadName").start();
    }

    public synchronized void fun1() {
        System.out.println("LockDemo.fun1:" + Thread.currentThread().getName());
        fun2();
    }

    public synchronized void fun2() {
        System.out.println("LockDemo.fun2:" + Thread.currentThread().getName());
        fun3();
    }

    public synchronized void fun3() {
        System.out.println("LockDemo.fun3:" + Thread.currentThread().getName());
    }
}

显式锁

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        new Thread(() -> {
            reentrantLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + ":外层");
                reentrantLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + ":内层");
                } finally {
                    reentrantLock.unlock();
                }
            } finally {
                reentrantLock.unlock();
            }
        }, "threadName").start();
    }
}

要保证lock()unlock()是配对出现的,如果不配对,就有可能出现死锁的情况,第一个线程使用完之后,不释放,第二个线程迟迟拿不到锁,程序就卡住了。

Synchronized的重入的实现机理

在ObjectMonitor.cpp里,有几个关键属性,可以记录锁相关的数据。

属性作用
_owner指向持有ObjectMonitor对象的线程
_WaitSet存放处于wait状态的线程队列
_EntryList存放处于等待锁block状态的线程队列
_recursions锁的重入次数
_count用来记录该线程获取锁的次数

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenterl时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

死锁及排查

是什么

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生的原因:

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

一个死锁代码

public class LockDemo {
    public static void main(String[] args) {
        Object object1 = new Object();
        Object object2 = new Object();
        new Thread(() -> {
            synchronized (object1) {
                System.out.println(Thread.currentThread().getName() + "持有object1的锁,尝试获取object2的锁");
                try {
                    Thread.sleep(1);// 让thread2可以start起来
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                    System.out.println(Thread.currentThread().getName() + "成功获取object2的锁");
                }
            }
        }, "thraed1").start();
        new Thread(() -> {
            synchronized (object2) {
                System.out.println(Thread.currentThread().getName() + "持有object2的锁,尝试获取object1的锁");
                try {
                    Thread.sleep(1);// 让thread1可以start起来
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1) {
                    System.out.println(Thread.currentThread().getName() + "成功获取object1的锁");
                }
            }
        }, "thraed2").start();
    }
}

如何排查死锁

在Terminal里输入jps -l查看当前运行的Java线程,输入jstack 进程编号查看具体信息,可以看到最后一行有一个Found 1 deadlock,说明发生了死锁。
还有一种方式:jconsole,在控制台输入jconsole,回车,选择自己的进程,点击连接,选择线程标签,点击左下角“检测死锁”,会出现一个新的“死锁”标签,里面就有具体的信息了。

写锁(独占锁)、读锁(共享锁)

详见JUC并发编程与源码分析笔记14-ReentrantLock、ReentrantReadWriteLock、StampedLock讲解

自旋锁SpinLock

详见JUC并发编程与源码分析笔记08-CAS

无锁→独占锁→读写锁→邮戳锁

详见JUC并发编程与源码分析笔记14-ReentrantLock、ReentrantReadWriteLock、StampedLock讲解

无锁→偏向锁→轻量锁→重量锁

详见JUC并发编程与源码分析笔记12-Synchronized与锁升级

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值