Java多线程之Lock接口

本文深入探讨了Java中的Lock接口及其具体实现ReentrantLock与ReentrantReadWriteLock,对比了它们与synchronized关键字的区别,并通过示例代码展示了如何使用这些锁机制来解决实际问题。

Lock接口通过底层框架的形式为设计更面向对象、可更加细粒度控制线程代码、更灵活控制线程通信提供了基础。实现Lock接口且使用得比较多的是可重入锁ReentrantLock以及读写锁ReentrantReadWriteLock(成员内部类:WriteLock、ReadLock)。

1. ReentrantLock

synchronized使用及实现原理里面,已经总结过通过使用Synchronized关键字实现线程内的方法锁定。但使用Synchronized关键字有一些局限性,上锁和释放锁是由JVM决定的,用户没法上锁和释放进行控制。那么问题就来了:假如有一个线程业务类管理某一全局变量的读和写。对于每条线程,在读的时候数据是共享的可以让多个线程同时去读。但有某个线程在对该全局变量进行写的时候,其他的线程都不能够对变量进行读或者写(对应数据库内的读共享写互斥)。

ReentrantLock提供了一个可中断、拥有并发竞争机制[指线程对锁的竞争方式:公平竞争或不公平竞争]的方式。

正如ReentrantLock跟Synchronized关键字所使用的功能基本一样,而且Synchronized还能自己释放锁,那什么时候使用ReentrantLock?

  1. 在中断线程的时候,可以使用ReentrantLock进行控制:如线程1有一个耗时很大的任务在执行,执行时线程2必须进行等待。当线程1执行的任务时间实在太长了,线程2放弃等待进行线程后续的操作。该情况下如果使用Synchronized,只能通过抛出异常的形式进行异常操作。
  2. 多条件变量通讯:如有3条线程,线程1完成任务后通知线程2执行,线程2执行完业务逻辑以后通知线程3执行,线程3执行完通知线程1继续执行。用Synchronized关键字很难处理这种问题。用Lock却可以很好的处理这些内容。当然,线程1 、2、3 同样地可以换由一个线程组去执行这些任务。
1.1 ReentrantLock对线程中断的控制

首先,单纯地使用synchronized关键字不能进行锁中断控制. 在synchronized关键字控制的代码块内,不会因为线程中断而做出相关处理。

先查看使用synchronized关键字在处理线程中断时的结果。

业务逻辑主要为:开辟两条线程,一条线程对文件进行读操作,另一条线程对文件进行写操作。写操作内容需要时间较长,且先执行。读操作后执行,若读线程等待超过4秒。让读线程中断,进行格式化文件。

使用接口,区分使用synchronized关键字及Lock方式控制线程中断的业务逻辑。

public interface IFileHandler {

    boolean isGetReadLock = false;

    void read();

    void write();

    void formatFile();
}

在synchronized关键字控制代码块的前提下,对线程进行中断的业务逻辑代码。synchronized关键字不会去响应线程中断。

public class SyncFileHandler implements IFileHandler {

    private volatile boolean isGetReadLock = false;

    public boolean isGetReadLock() {
        return isGetReadLock;
    }

    public void read() {
        synchronized (FileHandlerByThreads.class.getClass()) {
            System.out.println(Thread.currentThread().getName() + " start");
            // 能进来则设置变量标志位
            isGetReadLock = true;
        }
    }

    // 模拟运行时间比较久的写操作
    public void write() {
        try {
            synchronized (FileHandlerByThreads.class.getClass()) {
                System.out.println(Thread.currentThread().getName() + " start");
                long startTime = System.currentTimeMillis();
                // 模拟一个耗时较长的操作
                for (; ; ) {
                    if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
                        break;
                    }
                }
            }

            System.out.println("Writer has writered down everything! bravo");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public void formatFile() {
        System.out.println("begin to format the file");
        // format the file
    }
}

客户端测试代码

public class TestLock {

    public static void main(String[] args) throws Exception {
        // 1. 根据lock控制中断
        // FileHandlerByThreads fileControl = new FileHandlerByThreads();
        // Thread readthr = new Thread(new ReadThread(fileControl), "reader");
        // Thread writethr = new Thread(new WriteThread(fileControl), "writer");

        // 2. 使用synchronized关键字控制中断线程
        SyncFileHandler sync = new SyncFileHandler();

        Thread readthr = new Thread(new ReadThread(sync), "reader");
        Thread writethr = new Thread(new WriteThread(sync), "writer");
        writethr.start();
        readthr.start();

        long startTime = System.currentTimeMillis();
        // 循环判是否有线程获取到了读锁断
        while (!sync.isGetReadLock()) {
            long endTime = System.currentTimeMillis();
            // 如果4秒后读线程仍然没有等到读锁,离开等待
            if (endTime - startTime > 4000) {
                readthr.interrupt();
                System.out.println("4 seconds have passed,try to interrupt reader Thread");
                break;
            }
        }
    }
}

class ReadThread implements Runnable {
    private IFileHandler fileControl;

    public ReadThread(IFileHandler fileControl) {
        this.fileControl = fileControl;
    }

    @Override
    public void run() {
        fileControl.read();
        // 测试单纯使用synchronized关键字控制线程中断
        System.out.println("reader thread end");
        fileControl.formatFile();
    }
}

class WriteThread implements Runnable {
    private IFileHandler fileControl;

    public WriteThread(IFileHandler fileControl) {
        this.fileControl = fileControl;
    }

    @Override
    public void run() {
        fileControl.write();
    }
}

代码运行结果:线程未中断:

f1539b975584432ad4611abdd77256e3ad7.jpg

下面使用ReentrantLock实现可中断线程控制

public class FileHandlerByThreads implements IFileHandler {

    private volatile boolean isGetReadLock = false;
    private ReentrantLock lock = new ReentrantLock();

    public boolean isGetReadLock() {
        return isGetReadLock;
    }

    public void read() {
        try {
            // 等待20毫秒再进行后续操作,防止主线程操作过快
            Thread.sleep(50);
            // 使用reentrantlock
            lock.lockInterruptibly();
            System.out.println(Thread.currentThread().getName() + " start");
            isGetReadLock = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("reader Thread leave the file and going to format the file");
        }
    }

    // 模拟运行时间比较久的写操作
    public void write() {
        try {

            // 1.使用lock实现写锁定
            // 等待20毫秒再进行后续操作,防止主线程操作过快
            Thread.sleep(20);
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " start");
            long startTime = System.currentTimeMillis();
            // 模拟一个耗时较长的操作
            for (; ; ) {
                if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
                    break;
                }
            }

            System.out.println("Writer has writered down everything! bravo");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void formatFile() {
        System.out.println("begin to format the file");
        // format the file
    }
}

客户端测试代码

public class TestLock {

    public static void main(String[] args) throws Exception {
        // 1. 根据lock控制中断
        FileHandlerByThreads fileControl = new FileHandlerByThreads();
        Thread readthr = new Thread(new ReadThread(fileControl), "reader");
        Thread writethr = new Thread(new WriteThread(fileControl), "writer");

        // 2. 使用synchronized关键字控制中断线程
        // SyncFileHandler sync = new SyncFileHandler();

        //Thread readthr = new Thread(new ReadThread(sync), "reader");
        //Thread writethr = new Thread(new WriteThread(sync), "writer");
        writethr.start();
        readthr.start();

        long startTime = System.currentTimeMillis();
        // 循环判是否有线程获取到了读锁断
        while (!fileControl.isGetReadLock()) {
            long endTime = System.currentTimeMillis();
            // 如果4秒后读线程仍然没有等到读锁,离开等待
            if (endTime - startTime > 4000) {
                readthr.interrupt();
                System.out.println("4 seconds have passed,try to interrupt reader Thread");
                break;
            }
        }

    }
}

class ReadThread implements Runnable {
    private IFileHandler fileControl;

    public ReadThread(IFileHandler fileControl) {
        this.fileControl = fileControl;
    }

    @Override
    public void run() {
        fileControl.read();
        // 测试单纯使用synchronized关键字控制线程中断
        System.out.println("reader thread end");
        fileControl.formatFile();
    }
}

class WriteThread implements Runnable {
    private IFileHandler fileControl;

    public WriteThread(IFileHandler fileControl) {
        this.fileControl = fileControl;
    }

    @Override
    public void run() {
        fileControl.write();
    }
}

f8d94c1d9ff19417082927f272e248a7c24.jpg

1.2 ReentrantLock实现条件变量的控制
package lock;

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

/**
 * ReentrantLock Condition使用
 * <p>
 * Created by Jiacheng on 2018/7/3.
 */
public class ConditionLock {

    /**
     * BoundedBuffer 是一个定长100的集合,当集合中没有元素时,take方法需要等待,直到有元素时才返回元素
     * 当其中的元素数达到最大值时,要等待直到元素被take之后才执行put的操作
     */
    static class BoundedBuffer {
        final Lock lock = new ReentrantLock();
        final Condition notFull = lock.newCondition();
        final Condition notEmpty = lock.newCondition();

        final Object[] items = new Object[100];
        int putptr, takeptr, count;

        public void put(Object x) throws InterruptedException {
            System.out.println("put wait lock");
            lock.lock();
            System.out.println("put get lock");
            try {
                while (count == items.length) {
                    System.out.println("buffer full, please wait");
                    notFull.await();
                }

                items[putptr] = x;
                if (++putptr == items.length)
                    putptr = 0;
                ++count;
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
        }

        public Object take() throws InterruptedException {
            System.out.println("take wait lock");
            lock.lock();
            System.out.println("take get lock");
            try {
                while (count == 0) {
                    System.out.println("no elements, please wait");
                    notEmpty.await();
                }
                Object x = items[takeptr];
                if (++takeptr == items.length)
                    takeptr = 0;
                --count;
                notFull.signal();
                return x;
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {

        final BoundedBuffer boundedBuffer = new BoundedBuffer();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1 run");
                for (int i = 0; i < 1000; i++) {
                    try {
                        System.out.println("putting..");
                        boundedBuffer.put(Integer.valueOf(i));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    try {
                        Object val = boundedBuffer.take();
                        System.out.println(val);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        });

        t1.start();
        t2.start();
    }
}

2. ReentrantReadWriteLock (读写锁)

ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁
线程进入读锁的前提条件:

  1. 没有其他线程的写锁
  2. 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个

线程进入写锁的前提条件:

  1. 没有其他线程的读锁
  2. 没有其他线程的写锁

到ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系。然后就是总结这个锁机制的特性了: 

  1. 重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。 
  2. WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能,为什么?参看(a),呵呵. 
  3. ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。 
  4. 不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。 
  5. WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。 
package lock;

import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写锁
 *
 * Created by Jiacheng on 2018/7/3.
 */
public class ReadWriteLockTest {

    public static void main(String[] args) {
        final Queue3 q3 = new Queue3();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                while (true) {
                    q3.get();
                }
            }).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                while (true) {
                    q3.put(new Random().nextInt(10000));
                }
            }).start();
        }
    }
}

class Queue3 {
    private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public void get() {
        rwl.readLock().lock();//上读锁,其他线程只能读不能写
        System.out.println(Thread.currentThread().getName() + " be ready to read data!");
        try {
            Thread.sleep((long) (Math.random() * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "have read data :" + data);
        rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面
    }

    public void put(Object data) {

        rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写
        System.out.println(Thread.currentThread().getName() + " be ready to write data!");
        try {
            Thread.sleep((long) (Math.random() * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.data = data;
        System.out.println(Thread.currentThread().getName() + " have write data: " + data);

        rwl.writeLock().unlock();//释放写锁
    }
}

 下面使用读写锁模拟一个缓存器:

package lock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写锁模拟的缓存器
 *
 * Created by Jiacheng on 2018/7/3.
 */
public class CacheByReadWriteLock {

    private Map<String, Object> map = new HashMap<String, Object>();//缓存器
    private ReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {

    }

    public Object get(String id) {
        Object value = null;
        rwl.readLock().lock();//首先开启读锁,从缓存中去取
        try {
            value = map.get(id);
            if (value == null) {  //如果缓存中没有释放读锁,上写锁
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try {
                    if (value == null) {
                        value = "aaa";  //此时可以去数据库中查找
                    }
                } finally {
                    rwl.writeLock().unlock(); //释放写锁
                }
                rwl.readLock().lock(); //然后再上读锁
            }
        } finally {
            rwl.readLock().unlock(); //最后释放读锁
        }
        return value;
    }
}

3. synchronizedlock的区别

  1. (用法)synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
  2. (用法)lock(显示锁):需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类作为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。 如果没有主动释放锁,就有可能导致死锁现象。
  3. (机制)synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁,等待的线程会一直等待下去,不能够响应中断。Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就 是CAS操作(Compare and Swap)。
  4. (性能)synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。

 

参考资料

Lock接口,ReentranctLock,ReentrantReadWriteLock

java中ReentrantReadWriteLock读写锁的使用

java并发控制:ReentrantLock Condition使用详解

转载于:https://my.oschina.net/ljc94/blog/1839439

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值