Java并发系列2

Wesley13
• 阅读 520

上一节讲到Java线程和synchronized关键字的使用。下面就开始介绍JDK中的一些好用的并发控制工具。
先来看ReetrantLock类,他可用来替换synchronized关键字,而且比synchronized关键字更为强大和灵活。

一、ReetrantLock简单示例

先看代码:

public class ReeterLock implements Runnable {
    static ReentrantLock lock = new ReentrantLock();
    static int i = 0;

    @Override
    public void run() {
        for (int k = 0; k < 1000000; k++) {
            lock.lock();
            try {
                i++;
            } finally {
                lock.unlock();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ReeterLock reeterLock = new ReeterLock();
        Thread t1 = new Thread(reeterLock);
        Thread t2 = new Thread(reeterLock);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

}

可以看到重入锁有lock()和unlock()方法,使用上比synchronized关键字要灵活很多。
还记得上章节中关于区域控制权的例子么?这里lock就是获得控制权,unlock就是交出控制权,这样的话江湖有了规矩,大家都好办事儿。
这里说一下性能问题,在Java5的早期版本,ReentrantLock的性能会比synchronized强很多;在Java6之后,synchronized关键字获得了优化,性能基本和ReentrantLock相差不大。

二、ReetrantLock的中断响应

第一章中讲到线程中断机制对于同步阻塞的情况并不能做到收放自如,同步阻塞不会收到异常。
对应上一节的例子来看,如果lock.lock();这一行出现了同步等待,即使调用了线程的interrupt()的方法,lock.lock();也收不到异常。为了解决这个问题,ReentrantLock提供了lockInterruptibly()方法。
使用这个方法,就可以响应中断了。

三、锁申请等待限时

如果你排队买肯德基,时间超过10分钟,也许你就失去耐心、悻悻而归了。
有时候获得锁也一样,要获得一个锁,如果等待时间过长,那么也可以选择放弃。

public class TimeLock implements Runnable {
    static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) {
                Thread.sleep(1111);
            } else {
                System.out.println("try lock failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TimeLock reeterLock = new TimeLock();
        Thread t1 = new Thread(reeterLock);
        Thread t2 = new Thread(reeterLock);
        t1.start();
        t2.start();
    }

}

t2线程要去获得锁,他的耐心只有1秒钟,如果排队了1秒还获取不到,那么就失败,可以去先做别的事儿。
tryLock()也可以无参,意思就是试着获得锁,如果得不到就返回false。复杂情况下使用tryLock()而不是lock()可以有效避免死锁。

四、公平锁

在操作系统中,关于进程该如何竞争CPU的使用时间是个经典问题。其中有一种算法叫抢占式调度算法,他会让进程去抢占CPU,然后占用CPU一段固定时间,当占用时段结束,则该进程被挂起,让其他进程有可运行的时间。有点类似一人一口苹果的感觉。
那么在Java并发中,如果多个线程差之毫厘的去请求锁,默认的策略是什么呢?默认的策略会是系统随机挑选一个来线程来获得锁。
如果现在你想要保证公平性,也就是说规定先到先得,该怎么办?幸好ReentrantLock提供了这种机制,叫做公平锁的机制。
只需要ReentrantLock的构造函数入参是true即可:

public class FairLock implements Runnable {
    static ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " 获得锁!");
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        FairLock reeterLock = new FairLock();
        Thread t1 = new Thread(reeterLock, "t");
        Thread t2 = new Thread(reeterLock, "---t");
        t1.start();
        t2.start();
    }

}

默认的策略非常高效,但非公平;公平锁的话比较公平,效率稍有降低。

五、重入锁的好搭档:Condition

Condition,是条件的意思,可以看做是ReentrantLock的一种扩展,利用好Condition对象,我们可以让线程在合适的时间进行等待,或者在某一个特定的时刻得到通知,取消等待,继续执行。
Condition中的主要方法有:

  • await() 使当前线程等待,同时释放当前锁。当其他线程中使用signal()signalAll()方法时,线程会重新获得锁并继续执行
  • awaitUninterruptibly()await()方法相同,但不会在等待过程中响应中断
  • singal() 用于唤醒一个在等待中的线程

例如警匪片中,警察前往歹徒毒品交易的地点,卧底发来消息:交易时间变更,请等待我的通知(调用了await()方法)。警察原地待命,30分钟后卧底发来通知:出发(调用了singal()方法),警察继续行动。
Condition和ReentrantLock是配合使用的:

public class ReentrantLockCondition implements Runnable {
    static ReentrantLock lock = new ReentrantLock(true);
    static Condition condition = lock.newCondition();

    @Override
    public void run() {
        try {
            lock.lock();
            condition.await();
            System.out.println("going on");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockCondition reeterLock = new ReentrantLockCondition();
        Thread t1 = new Thread(reeterLock);
        t1.start();
        Thread.sleep(2000);
        lock.lock();
        condition.signal();
        lock.unlock();
    }

}

这里要注意一点,condition.signal()调用的时候一定要保证当前线程重新获得锁(也就是最好先调用lock.lock()方法),否则会报IllegalMonitorStateException异常。


JDK中的并发容器ArrayBlockingQueue中就使用到ReentrantLock和Condition的配方,ArrayBlockingQueue在后面我们会讲到。如果你现在就非常感兴趣的话,可以看下ArrayBlockingQueue的源码:

    /** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

先在构造函数中初始化了lock和两个condition,这里我们先拿notEmpty condition来看一下用法。
ArrayBlockingQueue是一个阻塞队列,他的工作方式是:取出元素的时候,如果队列中没有元素,则线程进行等待,直到队列中有元素,才从队列中取值。
我们看到take()方法中,如果队列元素数为0,则notEmpty condition进行等待。直到put进来一个元素,调用enqueue()方法后,会调用notEmpty.signal()方法,这时take()方法才能解除阻塞、继续执行。
这可真是ReentrantLock和Condition的经典配合。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
2年前
Java中的ThreadLocal功能演示
除了使用synchronized同步符号外,Java中的ThreadLocal是另一种实现线程安全的方法。在进行性能测试用例的编写过程中,比较简单的办法就是直接使用synchronized关键字,修饰对象、方法以及类。但是使用synchronized同步,这可能会影响应用程序的可伸缩性以及运行效率。但是如果要在多个
Wesley13 Wesley13
2年前
Java并发(5)
引言在synchronized未优化之前,我们在编码中使用最多的同步工具类应该是ReentrantLock类,ReentrantLock拥有优化后synchronized关键字的性能,又提供了更多的灵活性。相比synchronized,他在功能上更加强大,具有等待可中断,公平锁以及绑定多个条件等synchronized不具备
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
Java 多线程与并发(七):ReentrantLock 与 ReentrantReadWriteLock
ReentrantLock我们已经通过前几章学会了synchronized和AQS等相关只是。下面我们继续来学习ReentrantLock这个并发工具类,如果你已经了解了AQS的机制,那么你学习ReentrantLock将会非常轻松。背景Synchronized关键字虽然在JDK1.6
Wesley13 Wesley13
2年前
Java多线程synchronized关键字引出的多种锁
前言Java中的 synchronized关键字可以在多线程环境下用来作为线程安全的同步锁。本文不讨论 synchronized 的具体使用,而是研究下synchronized底层的锁机制,以及这些锁分别的优缺点。一、synchronized机制synchro
Wesley13 Wesley13
2年前
Java并发编程(六)
  上一节已经讲到,使用Synchronzied代码块可以解决共享对象的竞争问题,其实还有其他的方法也可以避免资源竞争问题,我统称他们为Java同步块。Java同步块(synchronizedblock)用来标记方法或者代码块是同步的,可以避免资源竞争,避免死锁。。Java同步关键字(synchronized)J
Wesley13 Wesley13
2年前
Java并发编程:Lock
一.synchronized的缺陷synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?  在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
2年前
JAVA 并发编程之二:Object对象中的wait,notify,notifyAll 概念+作用(线程状态控制Type1)
<divclass"htmledit\_views"id"content\_views"<pwait,notify,notifyAll是定义在Object类的实例方法,用于控制线程状态。</p<p三个方法都必须在synchronized同步关键字所限定的作用域中调用,否则会报错java.lang.IllegalMonitorStat