• 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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    案例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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    案例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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    案例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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    案例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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    案例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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    案例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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    案例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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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) {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    通过对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) {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过对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) {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过对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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    何为公平锁/非公平锁

    公平锁:多个线程按照申请锁的顺序来获取锁,类似排队买票,先来的人先买,后来的人在队尾排着。
    非公平锁:多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,可能造成优先级翻转或者饥饿状态。
    为什么会有公平锁/非公平锁?为什么默认非公平锁?
    恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    显式锁

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    要保证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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    如何排查死锁

    在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与锁升级

  • 相关阅读:
    【我的日志】关于我可爱的新同事
    使用C++在控制台输出一辆ASCII car
    internet download manager2024中文绿色版(IDM下载器)
    【Git从青铜到王者】第二篇:Git的初始
    Linux之history命令详解
    第1篇 目标检测概述 —(1)目标检测基础知识
    人工智能考证初步探索
    纵横网络靶场 部分wp
    《C和指针》(5)操作符和表达式
    3.4.3 终结操作
  • 原文地址:https://blog.csdn.net/qq_36059561/article/details/128070762