Java基础-锁

1、Lock接口

  • 锁是一种工具,用于控制对共享资源的访问
  • Lock和synchronize,这两个是最常见的锁,他们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
  • Lock并不能代替synchronize,而是使用synchronize不合适或无法满足要求时,来提供高级功能

synchronize效率低,不够灵活,无法知道是否成功获取到锁。

  • lock()就是最普通的获取锁。如果锁被其他线程获取,则进行等待
  • Lock不会像synchronize一样,异常自动释放锁
  • 最佳实践是,在finally中释放锁,以保证发送异常的时候,锁一定会被释放
  • lock()方法不能被中短,这会带来隐患,一旦陷入死锁,lock()就会陷入永久等待
  • tryLock()用来尝试获取锁

2、锁的分类

分类,是从不同的角度出发,一种锁可能对应多个类型,一个类型可能对应多个锁

  • 线程要不要锁住同步资源
    • 锁住(悲观锁)
    • 不锁住(乐观锁)
  • 多线程能否共享一把锁
    • 可以(共享锁)
    • 不可以(独占锁)
  • 多线程竞争时,是否排队
    • 排队(公平锁)
    • 先尝试插队,插队失败再排队(非公平锁)
  • 同一个线程是否可以重复获取同一把锁
    • 可以(可重入)
    • 不可以(不可重入锁)
  • 是否可中断
    • 可以(可中断锁)
    • 不可以(非可中断锁)
  • 等所的过程
    • 自旋(自旋锁)
    • 阻塞(非自旋锁)

3、乐观锁和悲观锁

3.1、互斥同步锁的劣势

  • 阻塞和唤醒会有性能消耗,上下文切换
  • 可能陷入永久等待,如果持有锁的线程陷入死锁

3.2、什么是乐观锁和悲观锁

悲观锁:

  • 如果我不锁住这个资源,别人就会来争抢,就会造成结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样可以确保数据万无一失
  • java中悲观锁的实现就是synchronize和Lock相关

乐观锁:

  • 认为自己在处理操作的时候不会有其他人来干扰,所以并不会锁住被操作的对象
  • 如果数据和我一开始拿到的不一样,说明其他人在这段时间内改过数据,那我就不能继续更新刚才的数据,我会选择放弃、报错、 重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现
  • 乐观锁的典型例子就是原子类、并发容器
/**
 * @Classname PessimismOptimismLock
 * @Description 乐观锁和悲观锁
 * @Date 2021/4/19 21:56
 * @Created by WangXiong
 */
public class PessimismOptimismLock {
    
    int a ;
    
    //悲观锁
    public synchronized void testMethod(){
        a++;
    }

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        //乐观锁
        atomicInteger.getAndIncrement();
    }
}

3.3、适合使用的场景

悲观锁:

  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

乐观锁:

  • 适用于写入少,大部分是读取的场景,不加锁能让读取性能大幅度提高
  • git版本管理工具,数据库版本号

4、可重入锁和非可重入锁

以ReentrantLock举例

public class LockDemo {
    static class Outputer {
        Lock lock = new ReentrantLock();

        //字符串打印方法,一个个字符的打印
        public void output(String name){
            int len = name.length();
            // lock.lock();
            try{
                for (int i = 0; i < len; i++) {
                    System.out.print(name.charAt(i));
                }
                System.out.println("");
            }finally {
                // lock.unlock();
            }
        }
    }

    private void init(){
        final Outputer outputer = new Outputer();
        new Thread(new Runnable() {
            public void run() {
                while (true){
                    try {
                        Thread.sleep(5);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    outputer.output("悟空");
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                while (true){
                    try {
                        Thread.sleep(5);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    outputer.output("大师兄");
                }
            }
        }).start();
    }

    public static void main(String[] args) {
        new LockDemo().init();
    }

}

4.1、可重入

同一个线程可以多次获取同一把锁,这样的好处是:避免死锁,提高了封装性

public class GetHoldCount {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
}

5、公平锁和非公平锁

5.1、什么是公平和非公平

公平指的是安装线程请求的顺序,来分配锁;非公平指的是,不完全安装请求的顺序,在一定情况下,可以插队。

设置非公平,是为了提高效率,避免唤醒带来的空档期

ReentrantLock默认是非公平锁,如果要设置为公平锁,需要传递一个参数

5.2、代码演示公平和不公平

public class FairLock {
    
    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
        Thread[] thread = new Thread[10];
        for (int i = 0; i < thread.length; i++) {
            thread[i] = new Thread(new Job(printQueue));
        }
        for (int i = 0; i < thread.length; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Job implements Runnable{

    PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始打印");
        printQueue.printJob(new Object());
        System.out.println(Thread.currentThread().getName() + "打印完毕");
    }
}

class PrintQueue{
    // private Lock queueLock = new ReentrantLock();
    private Lock queueLock = new ReentrantLock(true);

    public void printJob(Object document){
        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }

        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }
}

 5.3、优缺点

公平锁:

  • 优点:线程之间公平,每个线程在等待一段时间后,总有执行的机会。
  • 缺点:更慢,吞吐量小

不公平锁:

  • 更快,吞吐量大
  • 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行

6、共享锁和排它锁

以ReentrantReadWriteLock读写锁为例

  • 排它锁,有称为独占锁,独享锁
  • 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时获取到共享锁,也可以查看但无法修改和删除数据
  • 共享锁和排它锁的典型是读写锁R嗯嗯陶然亭ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁
  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
  • 一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是二者不会同时出现(要么多读,要么一写

代码演示

public class CinemaReadWrite {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read(){
        readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "获取到了读锁,正在读取");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放了读锁");
            readLock.unlock();
        }
    }

    private static void write(){
        writeLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "获取到了写锁,正在写入");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.read();
            }
        }, "Thread1").start();
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.read();
            }
        }, "Thread2").start();
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.write();
            }
        }, "Thread3").start();
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.write();
            }
        }, "Thread4").start();
    }
}

测试

6.1、读写锁插队策略

  • 公平锁:不允许插队
  • 非公平锁:
    • 写锁可以随时插队(如果拿不到锁,就排队)
    • 读锁仅在等待头结点不是想获取写锁的时候可以插队(读锁还是可以插队的)

测试队列的头结点是写,读就不会插队

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.write();
            }
        }, "Thread1").start();
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.read();
            }
        }, "Thread2").start();
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.read();
            }
        }, "Thread3").start();
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.write();
            }
        }, "Thread4").start();
        new Thread(new Runnable() {
            public void run() {
                CinemaReadWrite.read();
            }
        }, "Thread5").start();
    }

头结点是读锁,也是可以插队的

public class NonfairBargeDemo {

    //非公平的,其实是可以插队的
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read(){
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write(){
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(40);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->write(), "Thread1").start();
        new Thread(()->read(), "Thread2").start();
        new Thread(()->read(), "Thread3").start();
        new Thread(()->write(), "Thread4").start();
        new Thread(()->read(), "Thread5").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread[] = new Thread[1000];
                for (int i = 0; i < thread.length; i++) {
                    thread[i] = new Thread(()->read(), "子线程创建的Threaad"+i);
                }
                for (int i = 0; i < thread.length; i++) {
                    thread[i].start();
                }
            }
        }).start();
    }
}

如果是公平的

public class NonfairBargeDemo {

    //设置为公平,完全按照排队的顺序来执行
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read(){
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write(){
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(40);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->write(), "Thread1").start();
        new Thread(()->read(), "Thread2").start();
        new Thread(()->read(), "Thread3").start();
        new Thread(()->write(), "Thread4").start();
        new Thread(()->read(), "Thread5").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread[] = new Thread[1000];
                for (int i = 0; i < thread.length; i++) {
                    thread[i] = new Thread(()->read(), "子线程创建的Threaad"+i);
                }
                for (int i = 0; i < thread.length; i++) {
                    thread[i].start();
                }
            }
        }).start();
    }
}

6.2锁的升级和降级

public class Upgrading {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void readUpgrading(){
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(1000);
            System.out.println("升级会带来阻塞");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取到了写锁,升级成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void writeUpgrading(){
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(1000);
            readLock.lock();
            System.out.println("在不释放写锁的情况下,直接 获取读锁,成功降级");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("先演示降级是可以的");
        Thread thread1 = new Thread(()->
            writeUpgrading(), "Thread1");
        thread1.start();
        thread1.join();
        System.out.println("-------------------------");
        System.out.println("演示升级是不行的");
        Thread thread2 = new Thread(()-> readUpgrading(), "Thread2");
        thread2.start();
    }
}

为什么不支持锁的升级?避免死锁

6.3、小结

  • ReentrantReadWriteLock实现了ReadWriteLock接口,最主要有两个方法:readLock()和writeLock()用来获取读锁和写锁
  • 要么多读,要么一写
  • 插队策略:为了防止饥饿,读锁不能插队
  • 升降级策略:只能降级,不能升级
  • ReentrantReadWriteLock适用于读多写少的情况,合理使用可以提高并发效率

7、自旋锁和阻塞锁

  • 自旋锁:不停的请求,尝试获取锁,没有获取锁的时候不阻塞
    • 如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。为了这一段时间,切换线程,是得不偿失的。
  • 阻塞锁:如果没有获取到锁,就会陷入阻塞状态

自旋其实是使用do..while循环来完成的

    //AtomicInteger原子类的方法
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

实现一个简单的自旋锁

public class SpinLock {

    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void loock(){
        Thread currentThread = Thread.currentThread();
        while (!sign.compareAndSet(null, currentThread)){
            System.out.println("自旋获取失败,再次尝试");
        }
    }

    public void unlock(){
        Thread currentThread = Thread.currentThread();
        sign.compareAndSet(currentThread, null);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "尝试获取自旋锁");
                spinLock.loock();
                System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
                }
            }
        };
        new Thread(runnable).start();
        new Thread(runnable).start();

    }
}

适用于并发度不是特别高的情况

8、可中断锁

在Java中,synchronize就是不是可中断锁,而Lock是可中断锁,因为tryLock(time)和InckInterruptibly都能响应中断

public class LockInterruptibly implements Runnable {
    private static Lock lock = new ReentrantLock();
    public void run() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        try{
            //lockInterruptibly相当于无限期等待,等锁期间,可以被打断
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                Thread.sleep(5000);
            }catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "睡眠期间被中断");
            }finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        }catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "等锁期间被中断");
        }
    }

    public static void main(String[] args) {
        LockInterruptibly r1 = new LockInterruptibly();
        Thread thread0 = new Thread(r1);
        Thread thread1 = new Thread(r1);
        thread0.start();
        thread1.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // thread0.interrupt();
        thread1.interrupt();
    }
}

9、锁优化

  • 缩小同步代码块
  • 尽量不要锁住方法
  • 减少请求锁的次数
  • 锁中尽量不要再包含锁
  • 选择合适的锁或者工具类
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值