Java并发编程:马士兵并发笔记(二)可重入锁

可重入锁ReentrantLock

ReentrantLock的使用

ReentrantLock可以完全替代synchronized,提供了一种更灵活的锁.
ReenTrantLock必须手动释放锁,为防止发生异常,必须将同步代码用try包裹起来,在finally代码块中释放锁.

public class T {

    ReentrantLock lock = new ReentrantLock();

    // 使用ReentrantLock的写法
    private void m1() {
        // 尝试获得锁
        lock.lock(); 
        try {
            System.out.println(Thread.currentThread().getName());
        } finally {
            // 
            lock.unlock(); 
        }
    }

    // 使用synchronized的写法
    private synchronized void m2() {
        System.out.println(Thread.currentThread().getName());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1, "t1").start(); 
        new Thread(t::m2, "t2").start(); 
    }
}

ReentrantLock获取锁的方法

尝试锁tryLock()

使用tryLock()方法可以尝试获得锁,返回一个boolean值,指示是否获得锁.

可以给tryLock方法传入阻塞时长,当超出阻塞时长时,线程退出阻塞状态转而执行其他操作.

public class T {

    ReentrantLock lock = new ReentrantLock();

    void m() {
        boolean isLocked = false;        // 记录是否得到锁

        // 改变下面两个量的大小关系,观察输出
        int synTime = 4;   	 // 同步操作耗时
        int waitTime = 2;    // 获取锁的等待时间

        try {
            isLocked = lock.tryLock(waitTime, TimeUnit.SECONDS);    // 线程在这里阻塞5秒,尝试获取锁
            if (isLocked) {
                // 若五秒内得到锁,则执行同步操作
                for (int i = 1; i <= synTime; i++) {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + "持有锁,执行同步操作");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 使用tryLock()方法,尝试解除标记时,一定要先判断当前线程是否持有锁
            if (isLocked) {
                lock.unlock();
            }
        }
        // 执行非同步操作
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "没持有锁,执行非同步操作");
        }
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "线程1").start();
        new Thread(t::m, "线程2").start();
    }
}

若我们设置同步操作耗时4秒,获取锁的等待时间为2秒,则程序执行结果如下. 我们发现线程2在阻塞时间内没能抢到锁,直接执行非阻塞方法:

线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程2没持有锁,执行非同步操作
线程1持有锁,执行同步操作
线程2没持有锁,执行非同步操作
线程1持有锁,执行同步操作
线程2没持有锁,执行非同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
线程1没持有锁,执行非同步操作
...

若我们设置同步操作耗时4秒,获取锁的等待时间为5秒,则程序执行结果如下. 我们发现线程2在阻塞时间内成功抢到锁,先执行完同步方法才执行非同步方法:

线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
....

若我们设置同步操作耗时4秒,获取锁的等待时间为5秒,则程序执行结果如下. 我们发现线程2在阻塞时间内成功抢到锁,先执行完同步方法才执行非同步方法:

线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
....

可中断锁lockInterruptibly

使用lockInterruptibly以一种可被中断的方式获取锁.

获取不到锁时线程进入阻塞状态,但这种阻塞状态可以被中断.主线程调用被阻塞线程的interrupt()方法可以中断该线程的阻塞状态,并抛出InterruptedException异常.

interrupt()方法只能中断线程的阻塞状态.若某线程已经得到锁或根本没去尝试获得锁,则该线程当前没有处于阻塞状态,因此不能被interrupt()方法中断.

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();

    // 线程1一直占用着lock锁
    new Thread(() -> {
        lock.lock();
        try {
            System.out.println("线程1启动");
            TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);  // 线程一直占用锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }, "线程1").start();

    // 线程2抢不到lock锁,若不被中断则一直被阻塞
    Thread t2 = new Thread(() -> {
        try {
            lock.lockInterruptibly();       // 尝试获取锁,若获取不到锁则一直阻塞
            System.out.println("线程2启动");
        } catch (InterruptedException e) {
            System.out.println("线程2阻塞过程中被中断");
        } finally {
            if (lock.isLocked()) {
                try {
                    lock.unlock(); // 没有锁定进行unlock就会抛出IllegalMonitorStateException异常
                } catch (Exception e) {
                }
            }
        }
    }, "线程2");
    t2.start();

    // 4秒后中断线程2
    try {
        TimeUnit.SECONDS.sleep(4);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t2.interrupt();//告诉t2别傻等了,抛出异常
}

程序输出如下:

线程1启动
线程2阻塞过程中被中断

并不是所有处于阻塞状态的线程都可以被interrupt()方法中断,要看该线程处于具体的哪种阻塞状态.

阻塞状态包括普通阻塞,等待队列,锁池队列.

  • 普通阻塞: 调用sleep()方法的线程处于普通阻塞,调用其interrupt()方法可以中断其阻塞状态并抛出InterruptedException异常
  • 等待队列: 调用锁的wait()方法将持有当前锁的线程转入等待队列,这种阻塞状态只能由锁对象的notify方法唤醒,而不能被线程的interrupt()方法中断.
  • 锁池队列: 尝试获取锁但没能成功抢到锁的线程会进入锁池队列
    • 争抢synchronized锁的线程的阻塞状态不能被中断.
    • 使用ReentrantLocklock()方法争抢锁的线程的阻塞状态不能被中断.
    • 使用ReentrantLocktryLock()lockInterruptibly()方法争抢锁的线程的阻塞状态不能被中断. ???

公平锁

在初始化ReentrantLock时给其fair参数传入true,可以指定该锁为公平锁.

CPU默认的进程调度是不公平的,也就是说,CPU不能保证等待时间较长的线程先被执行.但公平锁可以保证等待时间较长的线程先被执行.

public class T implements Runnable {

    private static ReentrantLock lock = new ReentrantLock(true);// 指定锁为公平锁

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "持有锁");
            } finally {
                lock.unlock(); 
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t, "线程1").start();
        new Thread(t, "线程2").start();
    }
}

程序输出如下,发现两个线程严格交替执行

线程1持有锁
线程2持有锁
线程1持有锁
线程2持有锁
线程1持有锁
线程2持有锁
...

等待/通知(await/signal)机制

之前的wait notify 生产者消费者:在这里插入图片描述在这里插入图片描述
在这里插入图片描述
使用while原因:

  • 在进程被唤醒期间,原本 不满/不空 的同步栈有可能被其它先被唤醒的 生产者/消费者 操作过后 变满/变空 ,如果再进行操作,就会发生下标越界。使用while,wait执行完后需要再检查一遍while(lists.size() == MAX),就不会出现这个问题。
  • 一旦在wait()方法中出现异常,若使用if语句,则就会直接进入catch语句,打印异常并退出if语句,执行后面对数组的操作

notifyAll原因:

  • 叫醒的线程又可能是一个同类型的角色

await()signal()方法

synchronized关键字类似,ReentrantLock锁也支持等待/通知机制.与synchronized不同的是,不是将线程阻塞在锁上,而是将其阻塞在条件Condition对象上,要通过Condition对象调用这些方法.

  • public void await(): 将当前线程阻塞在调用该方法的Condition对象上
  • public void signal(): 唤醒一个阻塞在调用该方法的Condition对象上的线程
  • public void signalAll(): 唤醒所有阻塞在调用该方法的Condition对象上的线程

理解Condition对象

Condition对象将Object的监视器方法(wait(),notify()notifyAll())分解成截然不同的条件对象,使等待/通知机制支持多路等待.

多个Condition对象被绑定到一个ReentrantLock对象上,一个锁上可以绑定多个Condition对象,用来控制多个执行路线的等待通知,可以通过锁对象的newCondition()方法得到一个绑定到当前对象上的Condition对象.

要注意的是,Condition对象是绑定到锁对象上的(可以理解为一种细粒度更高的锁),而不是绑定在线程上的.因此分析其wait(),notify()notifyAll()还是要针对锁来进行分析,而不是直接分析Condition对应哪个线程.

Condition实现生产者消费者

在这里插入图片描述
在这里插入图片描述

ThreadLocal线程局部变量

在这里插入图片描述
在这里插入图片描述

自己线程变量自己用

控制变量只有自己线程可见,ThreadLocal是空间换时间,synchronized是时间换空间。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值