0
点赞
收藏
分享

微信扫一扫

JAVA多线程八:锁优化

1.锁消除

有些应用程序中用到了synchronized,但其实没有在多线程环境下,编译器+JVM 判断锁是否可消除,如果可以,就直接消除。

例如StringBuffer:

StringBuffer sb = new StringBuffer(); sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加 锁解锁操作是没有必要的, 白白浪费了一些资源开销.

2.锁粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化。实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁. 但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁。

JAVA多线程八:锁优化_System

举个栗子理解锁粗化:

滑稽老哥当了领导, 给下属交代工作任务:

方式一:

打电话, 交代任务1, 挂电话.

打电话, 交代任务2, 挂电话.

打电话, 交代任务3, 挂电话.

方式二:

打电话, 交代任务1, 任务2, 任务3, 挂电话.

显然, 方式二是更高效的方案.

3.JUC的常见类

  • ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

  • ReentrantLock 的用法:
    1. lock(): 加锁, 如果获取不到锁就死等.
    2. rylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
    3. unlock(): 解锁

    ReentrantLock lock = new ReentrantLock(); -----------------------------------------
    
    lock.lock();
    try {
       // working
    } finally { 
       lock.unlock()
    }

    • ReentrantLock 和 synchronized 的区别:
    1. synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
    2. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
    3. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就 放弃.
    4. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启 公平锁模式.
    5. 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一 个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指 定的线程.

    // ReentrantLock 的构造方法
    public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
    }

    • 如何选择使用哪个锁?
    1. 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
    2. 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
    3. 如果需要使用公平锁, 使用 ReentrantLock.

    4.死锁

    1. 当两个或多个线程互相请求对方拥有的资源时,就可能发生死锁。
    2. 一个线程,一把锁,连续加锁两次,如果锁是不可重入锁就会死锁。
    3. 多个线程多把锁

    死锁的四个必要条件:

    1. 互斥使用:

    线程1拿到了锁,线程2就得等着。

    1. 不可抢占:

    线程1拿到锁之后,必须是线程1主动释放。

    1. 请求和保持:

    线程1拿到锁A之后,再次尝试获取锁B,A这把锁还是保持的(不会因为获取锁B就把A给释放了)。

    1. 循环等待:

    线程1尝试获取锁A和锁B,线程2尝试获取锁B和锁A。线程1在获取B的时候等待线程2释放B,线程2在获取A的时候等待线程1释放A。

    案例一:

    线程1先锁定资源1,然后尝试锁定资源2。同时,线程2先锁定资源2,然后尝试锁定资源1。由于两个线程互相持有对方所需的资源,它们会相互等待对方释放资源,从而导致死锁。

    
    public class DeadlockExample {
        public static void main(String[] args) {
            final Object resource1 = new Object();
            final Object resource2 = new Object();
            
            // 线程1尝试获取resource1,然后resource2
            Thread thread1 = new Thread(() -> {
                synchronized (resource1) {
                    System.out.println("Thread 1: locked resource 1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (resource2) {
                        System.out.println("Thread 1: locked resource 2");
                    }
                }
            });
            
            // 线程2尝试获取resource2,然后resource1
            Thread thread2 = new Thread(() -> {
                synchronized (resource2) {
                    System.out.println("Thread 2: locked resource 2");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (resource1) {
                        System.out.println("Thread 2: locked resource 1");
                    }
                }
            });
            
            // 启动两个线程
            thread1.start();
            thread2.start();
        }
    }
    

    案例二:

    不可重入锁造成死锁: 一个线程没有释放锁, 然后又尝试再次加锁。

    // 第一次加锁, 加锁成功
    lock();
    // 第二次加锁, 锁已经被占用, 阻塞等待. 
    lock();

    JAVA多线程八:锁优化_加锁_02

    按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待。

    直到第一次的锁被释放, 才能获取到第二个锁。但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作,这时候就会死锁。

    methodA 和 methodB 分别尝试先获取 lock1 和 lock2,然后再尝试获取另一个锁。当两个线程同时执行这两个方法时,它们会相互等待对方释放锁,从而导致死锁。

    为了避免这种情况,应该使用可重入锁(Reentrant Lock),例如 java.util.concurrent.locks.ReentrantLock,它允许多次获得同一把锁,并且不会造成死锁。

    public class DeadLockExample {
        private final Object lock1 = new Object();
        private final Object lock2 = new Object();
    
        public void methodA() {
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + " 获取了 lock1");
    
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + " 尝试获取 lock2");
                    // 这里会发生死锁,因为当前线程已经持有 lock1,但无法再次获取 lock2
                }
            }
        }
    
        public void methodB() {
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " 获取了 lock2");
    
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + " 尝试获取 lock1");
                    // 这里也会发生死锁,因为当前线程已经持有 lock2,但无法再次获取 lock1
                }
            }
        }
    
        public static void main(String[] args) {
            DeadLockExample example = new DeadLockExample();
    
            Thread threadA = new Thread(() -> example.methodA());
            Thread threadB = new Thread(() -> example.methodB());
    
            threadA.start();
            threadB.start();
        }
    }

    案例三:

    两个线程thread1和thread2,它们都尝试以不同的顺序获取两把锁lock1和lock2。如果这两个线程几乎同时启动,它们可能会相互等待对方释放锁,从而导致死锁。

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class MultiLocksDeadlock {
    
        // 定义两把锁
        private static final Lock lock1 = new ReentrantLock();
        private static final Lock lock2 = new ReentrantLock();
    
        public static void main(String[] args) {
            Thread thread1 = new Thread(new Task("task1", lock1, lock2));
            Thread thread2 = new Thread(new Task("task2", lock2, lock1));
    
            thread1.start();
            thread2.start();
        }
    
        static class Task implements Runnable {
            private String name;
            private Lock firstLock;
            private Lock secondLock;
    
            public Task(String name, Lock firstLock, Lock secondLock) {
                this.name = name;
                this.firstLock = firstLock;
                this.secondLock = secondLock;
            }
    
            @Override
            public void run() {
                try {
                    // 线程尝试以不同的顺序获取两把锁
                    firstLock.lock();
                    System.out.println(name + " got the first lock");
    
                    Thread.sleep(1000); // 模拟一些工作
    
                    secondLock.lock();
                    System.out.println(name + " got the second lock");
    
                    // 做一些工作...
    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    secondLock.unlock();
                    firstLock.unlock();
                }
            }
        }
    }
    

    5.如何避免死锁?

    给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁,任意线程加多把锁的时候,都让线程遵守上述程序,此时循环等待自然破除。


    举报

    相关推荐

    0 条评论