JUC - 为了线程安全 - 互斥同步 - ReentrantLock

1. 为什么需要Lock

  • 锁是一种工具,用于控制对共享资源的访问
  • Locksynchronized,这两个是最常见的锁,他们都可以达到线程安全的目的,但是在使用上和功能上又有较大的区别
  • Lock并不是用来代替synchronized的,而是当使用synchronized不合适或者不足以满足要求的时候,来提供高级功能的
  • Lock接口中最常见的实现类是ReentrantLock
  • 通常情况下,Lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现也允许并发访问,比如ReadWriteLoc中的ReadLock

1.1 为什么Synchronized不够用

① 效率低: 锁的释放情况少(执行完成或者出现异常),试图获得锁时不能设置超时,不能中断一个正在获得锁的线程
② 不够灵活: 加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的(读写锁更灵活)
③ 无法知道是否成功获得锁

1.2 而对于lock

lock作为一个接口,最典型的实现类是就是ReentrantLock,lock中声明了四种获得锁的方法相对synchronized来说具备可中断可以设置超时时间可以设置为公平锁支持多个条件变量等特点
在这里插入图片描述

1.lock():
	lock()就是最普通的获取锁,如果锁已经被其他线程获取,则进行等待
	lock()不会像synchronized一样在异常的时候自动释放锁
	因此最佳实践是在finally中释放锁,以保证发生异常的时候锁一定被释放
	lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock()就会陷入永久等待

2.tryLock():
	tryLock()用来尝试获取锁,如果当前的锁没有被其他线程占用,则获取成功,返回true,否则返回false,代表获取锁失败
	相比于lock(),这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为
	该方法会立即返回,即使拿不到锁也不会死等

3.tryLock(long, TimeUnit)

2. ReentrantLock

lock接口最典型的实现类是就是ReentrantLock,相对于 synchronized 它具备如下特点

① 可中断 (可以在取消等待锁,终止等待)被动的避免死等

对比synchronizedsynchronized获得的锁不可中断,也就是说线程A一直在等待锁,可以让求他线程终止他的等待

② 可以设置超时时间,被动的避免死等

这个的意思是说,线程A等待锁,我们可以让大他再等待了一定时间后,如果还没等待的锁就放弃等待,可以避免死锁问题

③ 可以设置为公平锁 (先到先得,防止饥饿)

④ 支持多个条件变量(不满足的条件不同可以去不同的waitset中等待,synchronized只有一个waitset

⑤ 与 synchronized 一样,都支持可重入(同一个线程对同一个对象可以反复加锁)

2.1 正确使用姿势

lock就是最普通的获得锁的方法,如果锁已经被其他锁获取了,则进行等待
上面说过相对与synchronized不同,lock并不会在异常的时候自动释放锁,所以为了保证不管是不是出现了异常都可以释放锁,把临界区的方法放入try,释放锁的代码放入finally

//先创建ReentrantLock对象,在线程外
reentrantLock.lock(); 
try {    
	// 临界区 
} finally {//为了保证不管是不是出现了异常都可以释放锁
// 释放锁
	reentrantLock.unlock();
}

2.2 特性详解

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

    static ReentrantLock lock = new ReentrantLock();

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

    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }

    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }
② 可打断 lock.lockInterruptibly()

被动的避免死等
可打断的ReentrantLock要使用lockInterruptibly方法获得,被打断或获得InterruptedException

	public static ReentrantLock lock = new ReentrantLock();
	public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("启动...");
            try {
            	//可打断的ReentrantLock要使用lockInterruptibly方法获得
                lock.lockInterruptibly();
                //执行逻辑
                try {
                	log.debug("获得了锁");
            	} finally {
            		//释放锁
                	lock.unlock();
            	}
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断");
            }
        }, "t1");
		//以下是主线程的代码
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            sleep(1);
            t1.interrupt();
            log.debug("执行打断");
        } finally {
            lock.unlock();
        }
    }

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

③ 锁超时 lock.tryLock(1, TimeUnit.SECONDS)

主动的避免死等,使用的是tryLock,不带参数就是尝试获得锁,获得不到直接放弃,tryLock也支持可打断,源码的注释中给我们注明了它的正确使用姿势

//A typical usage idiom for this method would be:
Lock lock = ...;
//if判断尝试获得锁是否成功,成功才会进行if语句块进行临界区的操作,并且在finally释放锁,没拿到锁是不会执行unlock的
if (lock.tryLock()) {
	try {
		// manipulate protected state
	} finally {
		lock.unlock();
	}
} else {
	// perform alternative actions
}
对哲学家问题的改进
	class Chopstick extends ReentrantLock {
        String name;

        public Chopstick(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "筷子{" + name + '}';
        }
    }

    class Philosopher extends Thread {
        Chopstick left;
        Chopstick right;

        public Philosopher(String name, Chopstick left, Chopstick right) {
            super(name);
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
            while (true) {            
                // 尝试获得左手筷子       
                if (left.tryLock()) {
                    try {
                        // 尝试获得右手筷子 
                        if (right.tryLock()) {//如果我没有获得右手筷子,那么会继续往下执行,释放了左手筷子,就避免了死锁
                            try {
                                eat();
                            } finally {
                                right.unlock();
                            }
                        }
                    } finally {
                        left.unlock();
                    }
                }
            }
        }

        private void eat() {
            log.debug("eating...");
            Sleeper.sleep(1);
        }
    }

公平锁(按照进入阻塞队列的顺序获得锁)(可以解决饥饿)

ReentrantLock 默认是不公平的,不公平指的是,不会按进入阻塞队列的顺序去获得锁
可以通过构造方法改变为公平锁

ReentrantLock lock = new ReentrantLock(true);
	public static ReentrantLock lock = new ReentrantLock(true);
	public static void main(String[] args) {
        lock.lock();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "t" + i).start();
        }

		// 1s 之后去争抢锁
        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start...");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " running...");
            } finally {
                lock.unlock();
            }
        }, "强行插入").start();
        lock.unlock();
	}

公平锁一般没有必要,会降低并发度

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒

使用要点:

  • 通过lock.newCondition()获得新的条件变量
  • await 前需要获得锁 ,之后进入相应的休息室
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

    static ReentrantLock lock = new ReentrantLock();
    //获得新的条件
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitbreakfastQueue = lock.newCondition();
    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasCigrette) {
                    try {
                    	//进入相应的条件变量休息室等待
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的烟");
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            try {
                lock.lock();
                while (!hasBreakfast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的早餐");
            } finally {
                lock.unlock();
            }
        }).start();

        sleep(1);
        sendBreakfast();
        sleep(1);
        sendCigarette();
    }

    private static void sendCigarette() {
        lock.lock();
        try {
            log.debug("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }
    }

    private static void sendBreakfast() {
        lock.lock();
        try {
            log.debug("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }

3. 可见性保证

Lock的加解锁和synchronized有同样的内存语义,也就是说下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,您想了解关于Java并发编程中的CountDownLatch吗? CountDownLatch是Java并发编程中非常重要的一个工具类,它可以让某个线程等待其他线程完成后再继续执行。它的作用类似于倒计时门闩,当count值减为0时,所有等待者都会被唤醒。 CountDownLatch是一次性的,也就是说,它的计数器只能使用一次。如果需要重复使用,可以考虑使用CyclicBarrier。 在使用CountDownLatch时,我们需要先创建一个CountDownLatch对象,并指定计数器的初始值。然后在需要等待的线程中调用await()方法进行等待,同时在其他线程中调用countDown()方法进行计数器的减1操作。 举个例子,假设我们有一个需求:主线程需要等待两个子线程完成后再执行。那么可以这样编写代码: ```java import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "执行开始"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行完毕"); countDownLatch.countDown(); }, "线程1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "执行开始"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行完毕"); countDownLatch.countDown(); }, "线程2").start(); System.out.println(Thread.currentThread().getName() + "等待子线程执行完毕"); countDownLatch.await(); System.out.println(Thread.currentThread().getName() + "所有子线程执行完毕,继续执行主线程"); } } ``` 在上面的例子中,我们首先创建了一个计数器初始值为2的CountDownLatch对象,然后创建了两个线程分别进行一些操作,并在操作结束后调用countDown()方法进行计数器减1操作。在主线程中,我们调用await()方法进行等待,直到计数器减为0时,主线程才会继续执行。 希望能够对您有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值