Java多线程学习(六)——Lock的使用

911 阅读6分钟

锁是用于通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读写锁。Java5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。

虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易很多。

Lock接口的特性

  • 尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁;
  • 能被中断地获取锁:获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放;
  • 超时获取锁:在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回。

ReentrantLock

ReentrantLock实现了Lock接口,并提供和synchronized相同的互斥性和内存可见性,与synchronized相比,ReentrantLock也有进入/退出同步代码块相同的内存语义,也同样的提供了可重入加锁语义。ReentrantLock还为处理锁的不可用性问题提供更高的灵活性。

public class LockTest {
    private Lock lock = new ReentrantLock();

    public void test(){
        lock.lock();
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
            }
        }finally {
            lock.unlock();
        }
    }
}
public class Main {

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();

        for (int i=0; i<5; i++){
            new Thread(lockTest::test, "Thread-"+i).start();
        }
    }
}

Condition实现等待/通知机制

synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。

而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author xiaosen
 * @date 2019/6/24 8:20
 * @description
 */
public class Myservice {
    private Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public void await() {
        lock.lock();
        try {
            System.out.println(" await时间为" + System.currentTimeMillis());
            condition.await();
            System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("signal时间为" + System.currentTimeMillis());
            condition.signal();
            Thread.sleep(3000);
            System.out.println("这是condition.signal()方法之后的语句");
        } finally {
            lock.unlock();
        }
    }

}

public class MyserviceTest {

    public static void main(String[] args) throws InterruptedException{
        Myservice myservice = new Myservice();
        new Thread(() -> myservice.await()).start();
        Thread.sleep(3000);
        myservice.signal();
    }
}

// 输出结果
 await时间为1561335861608
signal时间为1561335864608
这是condition.signal()方法之后的语句
这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行

在使用wait/notify实现等待通知机制的时候我们知道必须执行完notify()方法所在的synchronized代码块后才释放锁。在这里也差不多,必须执行完signal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。

注意: 必须在condition.await()方法调用之前调用lock.lock()代码获得同步监视器,不然会报错。

多个Condition实现等待/通知机制

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author xiaosen
 * @date 2019/6/24 8:29
 * @description
 */
public class MoreConditionNotify {

    private Lock lock = new ReentrantLock();
    public Condition conditionA = lock.newCondition();
    public Condition conditionB = lock.newCondition();

    public void awaitA() {
        lock.lock();
        try {
            System.out.println("begin awaitA时间为" + System.currentTimeMillis()
                    + " ThreadName=" + Thread.currentThread().getName());
            conditionA.await();
            System.out.println("  end awaitA时间为" + System.currentTimeMillis()
                    + " ThreadName=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void awaitB() {
        lock.lock();
        try {
            System.out.println("begin awaitB时间为" + System.currentTimeMillis()
                    + " ThreadName=" + Thread.currentThread().getName());
            conditionB.await();
            System.out.println("  end awaitB时间为" + System.currentTimeMillis()
                    + " ThreadName=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signalAll_A() {
        lock.lock();
        try {
            System.out.println("  signalAll_A时间为" + System.currentTimeMillis()
                    + " ThreadName=" + Thread.currentThread().getName());
            conditionA.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void signalAll_B() {
        lock.lock();
        try {
            System.out.println("  signalAll_B时间为" + System.currentTimeMillis()
                    + " ThreadName=" + Thread.currentThread().getName());
            conditionB.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
 
public class MoreConditionNotifyMain {

    public static void main(String[] args) throws InterruptedException{
        MoreConditionNotify conditionNotify = new MoreConditionNotify();
        new Thread(() -> conditionNotify.awaitA(), "A").start();
        new Thread(() -> conditionNotify.awaitB(), "B").start();
        Thread.sleep(3000);
        conditionNotify.signalAll_A();
    }
}

// 输出
begin awaitA时间为1561336446748 ThreadName=A
begin awaitB时间为1561336446748 ThreadName=B
  signalAll_A时间为1561336449748 ThreadName=main
  end awaitA时间为1561336449748 ThreadName=A

此时线程一直处于挂起状态,只有A线程被唤醒了。

实现生产者/消费者模式:一对一交替打印

package alternately;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author xiaosen
 * @date 2019/6/24 8:40
 * @description
 */
public class ConditionService {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean hasValue = false;

    public void set(){
        try {
            lock.lock();
            while (hasValue == true){
                condition.await();
            }
            System.out.println("打印☆");
            hasValue = true;
            condition.signal();
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void get(){
        try {
            lock.lock();
            while (hasValue == false){
                condition.await();
            }
            System.out.println("打印★");
            hasValue = false;
            condition.signal();
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

}

public static void main(String[] args){
        ConditionService service = new ConditionService();
        new Thread(() -> {
            for (int i=0; i<100; i++){
                service.set();
            }
        }).start();
        new Thread(() -> {
            for (int i=0; i<100; i++){
                service.get();
            }
        }).start();

    }

// 输出
打印☆
打印★
打印☆
打印★
打印☆
打印★
打印☆
。。。

公平锁与非公平锁

Lock锁分为:公平锁非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。

ReentrantReadWriteLock

ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock接口的实现类-ReentrantReadWriteLock读写锁就是为了解决这个问题。

读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁 也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。

多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的。)。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

代码:GitHub


欢迎关注公众号:

公众号微信