常见的锁类型

乐观锁和悲观锁

悲观锁:

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,synchronized和Lock的实现类都是悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源-----狼性锁

乐观锁:

认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:版本号机制Version最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命---佛系锁

synchronized

  • 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁;
  • 作用于代码块,对括号里配置的对象加锁
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
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");
    }
}

/**
 * 现象描述:
 * 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信  共用一个对象锁
 * 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信  共用一个对象锁
 * 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件
 * 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件  资源没有争抢,不是同一个对象锁
 * 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信  共用一个类锁
 * 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁
 * 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件   一个用类锁一个用对象锁
 * 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁
 */

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.sendSMS();
        }, "b").start();
    }

}

结论:

  • 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁--->实例对象本身
  • 对于静态同步方法,锁的时当前类的Class对象
  • 对于同步方法块,锁的时synchronized括号内的对象

公平锁和非公平锁

公平锁:

  • 是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的
  • ----- Lock lock = new ReentrantLock(true)---表示公平锁,先来先得。

非公平锁:

  • 是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)
  • ---- Lock lock = new ReentrantLock(false)---表示非公平锁,后来的也可能先获得锁,默认为非公平锁。

可重入锁

概念:

  • 是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。

可重入锁种类:

  • 隐式锁(即synchronized关键字使用的锁),默认是可重入锁
    • 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁。
  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁
public class ReEntryLockDemo {

    public static void main(String[] args) {
        final Object o = new Object();
        /**
         * ---------------外层调用
         * ---------------中层调用
         * ---------------内层调用
         */
        new Thread(() -> {
            synchronized (o) {
                System.out.println("---------------外层调用");
                synchronized (o) {
                    System.out.println("---------------中层调用");
                    synchronized (o) {
                        System.out.println("---------------内层调用");
                    }
                }
            }
        }, "t1").start();

        /**
         * 注意:加锁几次就需要解锁几次
         * ---------------外层调用
         * ---------------中层调用
         * ---------------内层调用
         */
        Lock lock = new ReentrantLock();
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("---------------外层调用");
                lock.lock();
                try {
                    System.out.println("---------------中层调用");
                    lock.lock();
                    try {
                        System.out.println("---------------内层调用");
                    } finally {
                        lock.unlock();
                    }
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

死锁及排查

概念:

死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。

产生原因:

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

case:

/**
 * @author Guanghao Wei
 * @create 2023-04-10 16:20
 */
public class DeadLockDemo {
    static  Object a=new Object();
    static  Object b=new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (a){
                System.out.println("t1线程持有a锁,试图获取b锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    System.out.println("t1线程获取到b锁");
                }
            }
         },"t1").start();

        new Thread(() -> {
            synchronized (b){
                System.out.println("t2线程持有a锁,试图获取a锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    System.out.println("t2线程获取到a锁");
                }
            }
        },"t2").start();
    }
}

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

读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。读写锁不需要特殊支持就可以直接用之前提到的几个东西实现,比如可以直接用两个 spinLock 或者两个 mutex 实现:

    void 以读者身份加锁(rwlock) {
        加锁(rwlock.保护当前读者数量的锁);
        rwlock.当前读者数量 += 1;
        if (rwlock.当前读者数量 == 1) {
            加锁(rwlock.保护写操作的锁);
        }
        解锁(rwlock.保护当前读者数量的锁);
    }

    void 以读者身份解锁(rwlock) {
        加锁(rwlock.保护当前读者数量的锁);
        rwlock.当前读者数量 -= 1;
        if (rwlock.当前读者数量 == 0) {
            解锁(rwlock.保护写操作的锁);
        }
        解锁(rwlock.保护当前读者数量的锁);
    }

    void 以写者身份加锁(rwlock) {
        加锁(rwlock.保护写操作的锁);
    }

    void 以写者身份解锁(rwlock) {
        解锁(rwlock.保护写操作的锁);
    }

如果整个场景中只有一个读者、一个写者,那么其实可以等价于直接使用互斥器。不过由于读写锁需要额外记录读者数量,花销要大一点。

自旋锁spinLock

概念:

自旋锁(spinlock)很好理解。对自旋锁加锁的操作,你可以认为是类似这样的:

while (抢锁(lock) == 没抢到) {}

只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。

仔细想想,其实没有必要一直去尝试加锁,因为只要锁的持有状态没有改变,加锁操作就肯定是失败的。所以,抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行好了。这就是互斥器(mutex)也就是题目里的互斥锁(不过个人觉得既然英语里本来就不带 lock,就不要称作锁了吧)。对​​​​​​​互斥锁加锁的操作你可以认为是类似这样的:

while (抢锁(lock) == 没抢到) { 本线程先去睡了请在这把锁的状态发生改变时再唤醒(lock);}

  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值