线程同步Lock

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/70768583
本文出自:【顾林海的博客】


前言

除了上一篇文章的synchronized,Java还提供了同步代码块的另一种机制,这种机制基于Lock接口及其实现类。相比与synchronized来说更强大也更灵活。

关于线程相关知识可以查看:

有关线程的相关知识(1)
有关线程的相关知识(2)

关于synchronized的相关知识可以查看:

线程同步synchronized

Lock的使用

使用Lock能支持更灵活的同步代码块结构,也就是控制的获取和释放不出现在同一个块结构中。Lock接口提供了很多的功能,可以通过tryLock()方法来试图获取锁,如果锁已经被其他线程获取,它将返回false并继续往下执行代码。Lock接口允许分离读和写操作,允许多个读线程和只有一个写线程。

下面编写一个使用Lock接口和它的实现类ReentrantLock类来创建一个临界区:

public class Queue {

    // 声明一个锁对象,并且用ReentrantLock类初始化
    private final Lock queueLock = new ReentrantLock();

    /**
     * 打印信息 通过调用lock()方法获取对锁对象的控制
     */
    public void printJob() {
        queueLock.lock();
        System.out.println("正在打印信息..."+new Date());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }

}
public class Event implements Runnable{

    private Queue queue;

    public Event(Queue queue){
        this.queue=queue;
    }

    @Override
    public void run() {
        queue.printJob();
    }

}
public class Client {
    public static void main(String[] args) {
        Queue queue=new Queue();

        Event event=new Event(queue);

        Thread[] thread=new Thread[10];

        for(int i=0,length=thread.length;i<length;i++){
            thread[i]=new Thread(event);
        }

        for(int i=0,length=thread.length;i<length;i++){
            thread[i].start();
        }

    }
}

上面Queue类中,定义来printJob()方法,通过lock()方法获取对锁对象的控制,最后通过unlock()方法释放对锁对象的控制。在主类中创建了10个线程,并启动这10个线程,运行程序,会在控制台上每隔1秒打印信息。

运行程序:
正在打印信息...Tue Apr 25 22:02:31 CST 2017
正在打印信息...Tue Apr 25 22:02:32 CST 2017
正在打印信息...Tue Apr 25 22:02:33 CST 2017
正在打印信息...Tue Apr 25 22:02:34 CST 2017
正在打印信息...Tue Apr 25 22:02:35 CST 2017
正在打印信息...Tue Apr 25 22:02:36 CST 2017
正在打印信息...Tue Apr 25 22:02:37 CST 2017
正在打印信息...Tue Apr 25 22:02:38 CST 2017
正在打印信息...Tue Apr 25 22:02:39 CST 2017
正在打印信息...Tue Apr 25 22:02:40 CST 2017

通过运行结果可以看出,对临界区的访问通过锁,来实现同一时间只有一个执行线程访问这个临界区,这里通过lock()方法获取对锁的控制,当某个线程访问这个方法时,如果没有其他线程获取对这个锁的控制,lock()方法将得到并且允许它立刻执行临界区。在线程离开临界区的时候,我们必须使用unlock()方法来释放它持有的锁,以让其他线程访问临界区,如果在离开临界区没有调用unlock()方法释放它持有的锁,其他线程将永远等待,从而导致死锁。


除了使用lock()方法来获取锁之外,Lock接口还提供了另一个方法来获取锁,即tryLock()方法。与lock()方法不同之处在于线程使用tryLock()不能够获取锁,tryLock()会立即返回,它不会将线程置入休眠,返回true表示线程获取了锁,false表示没有获取锁。


使用读写锁实现同步数据访问

ReadWriteLock接口和它唯一实现类ReentrantReadWriteLock,一个是读操作锁,另一个是写操作锁。使用读操作锁时可以允许多个线程同时访问,使用写操作锁时只允许一个线程进行。在一个线程执行写操作时,其他线程不能够执行读操作。

接下来通过ReadWriteLock接口来编写对某个对象的访问。

public class Product {

    private int number1;
    private int number2;

    private ReadWriteLock lock;

    public Product() {
        number1 = 10;
        number2 = 20;
        lock = new ReentrantReadWriteLock();
    }

    /**
     * 通过读锁获取对这个属性的访问
     * 
     * @return number1
     */
    public int getNumber1() {
        lock.readLock().lock();
        int number = number1;
        lock.readLock().unlock();
        return number;
    }

    /**
     * 通过读锁获取对这个属性的访问
     * 
     * @return number2
     */
    public int getNumber2() {
        lock.readLock().lock();
        int number = number2;
        lock.readLock().unlock();
        return number;
    }

    /**
     * 使用写锁来控制对这两个属性的访问
     * 
     * @param number1
     * @param number2
     */
    public void setNumber(int number1, int number2) {
        lock.writeLock().lock();
        System.out.println("写入number1:"+number1+"和number2:"+number2);
        this.number1 = number1;
        this.number2 = number2;
        lock.writeLock().unlock();
    }

}
/**
 * 读取类,读取Product的number1和number2
 * 
 * @author gulinhai
 *
 */
public class Reader implements Runnable {

    private Product product;

    public Reader(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("number1=" + product.getNumber1() + ";number2=" + product.getNumber2());
        }
    }

}
/**
 * 写入类,修改number1和number2
 * @author gulinhai
 *
 */
public class Writer implements Runnable {

    private Product product;

    public Writer(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {

            product.setNumber(i, i * 2);
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
public class Client {
    public static void main(String[] args) {
        Product product = new Product();

        Reader[] reader = new Reader[5];
        Thread[] readerThread = new Thread[5];

        for (int i = 0, length = readerThread.length; i < length; i++) {
            reader[i] = new Reader(product);
            readerThread[i] = new Thread(reader[i]);
        }

        Writer writer = new Writer(product);
        Thread writerThread = new Thread(writer);

        for (int i = 0, length = readerThread.length; i < length; i++) {
            readerThread[i].start();
        }
        writerThread.start();

    }
}
运行结果:
写入number1:0number2:0
写入number1:1number2:2
number1=1;number2=2
number1=1;number2=2
number1=1;number2=2
number1=1;number2=2
number1=1;number2=2
写入number1:2number2:4
number1=2;number2=4
number1=2;number2=4
number1=2;number2=4
number1=2;number2=4
number1=2;number2=4
写入number1:3number2:6
number1=3;number2=6
number1=3;number2=6
number1=3;number2=6
写入number1:4number2:8
number1=4;number2=8
number1=3;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8
number1=4;number2=8

通过输出结果可以看出合理的利用ReentrantReadWriteLock类的两种锁,可以有效的避免数据不一致的问题。

ReentrantLock和ReentrantReadWriteLock类的构造器都含有一个布尔参数,它允许你控制这个两个类的行为,默认值是false,称为非公平模式,在非公平模式下,当有很多线程在等待锁时,锁将选择他们中的一个来访问临界区,这个选择是没有任何约束的。当布尔值为true时,称为公平模式,在公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,而且选择的是等待时机最长的。


锁中使用多条件

一个锁可能关联一个或者多个条件,这些条件通过Condition接口声明,目的允许线程获取锁并且查看等待的某一个条件是否满足,如果不满足就挂起直到某个线程唤醒它们,Condition接口提供了挂起线程和唤起线程的机制。

编程程序,利用Condition来解决生产者-消费者问题。

/**
 * 创建数据存储类EventStorage,并保存一个最大值maxSize和数据集合 LinkedList<Date>来保存存入的日期。
 * 
 * @author gulinhai
 *
 */
public class EventStorage {

    private int maxSize;
    private LinkedList<Date> storage;

    private ReentrantLock lock;

    private Condition condition1;
    private Condition condition2;

    public EventStorage() {
        lock = new ReentrantLock();
        condition1 = lock.newCondition();
        condition2 = lock.newCondition();
        this.maxSize = 10;
        this.storage = new LinkedList<>();
    }

    public int size() {
        return storage.size();
    }

    /**
     * <pre>
     * 获取锁,检查这个缓冲区是否满了,如果缓冲区满了,
     * 就调用条件condition1的await()方法
     * 等待空位出现,当其他线程调用条件condition1的
     * signal()或者signalAll()方法时,这个线程
     * 将被唤醒,在有空位后,线程会将数据保存到缓冲区中,
     * 并调用条件condition2的signalAll()方法。
     * </pre>
     */
    public void set() {
        lock.lock();
        try {
            while (storage.size() == maxSize) {
                condition1.await();
            }
            storage.add(new Date());
            condition2.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    /**
     * <pre>
     * 获取锁,检查这个缓冲区是否为空,如果缓冲区为空,
     * 就调用条件condition2的await()方法
     * 等待数据出现,当其他线程调用条件condition2的
     * signal()或者signalAll()方法时,这个线程
     * 将被唤醒,在有数据后,线程会从缓冲区中取出数据,
     * 并调用条件condition1的signalAll()方法。
     * </pre>
     */
    public void get() {
        lock.lock();
        try {
            while (storage.size() == 0) {
                condition2.await();
            }
            storage.poll();
            condition1.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

}
/**
 * 生产者
 * @author gulinhai
 *
 */
public class Producer implements Runnable{

    private EventStorage storage;

    public Producer(EventStorage storage){
        this.storage=storage;
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            storage.set();
        }
    }

}
/**
 * 消费者
 * @author gulinhai
 *
 */
public class Consumber implements Runnable{

    private EventStorage storage;

    public Consumber(EventStorage storage){
        this.storage=storage;
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            storage.get();
        }
    }

}
public class Client {

    public static void main(String[] args) {
        EventStorage storage=new EventStorage();

        Producer producer=new Producer(storage);
        Thread producerThread=new Thread(producer);

        Consumber consumber=new Consumber(storage);
        Thread consumberThread=new Thread(consumber);

        producerThread.start();
        consumberThread.start();

        try {
            producerThread.join();
            consumberThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("缓冲区数据个数:"+storage.size());

    }

}
输出结果:
缓冲区数据个数:0

与锁绑定的所有条件对象都是通过Lock接口声明的newCondition()方法创建的,在使用条件的时候,必须获取这个条件绑定的锁,所以带条件的代码必须在调用Lock对象的lock()方法和unlock()方法之间,当线程调用条件的await()方法时,它将自动释放这个条件绑定的锁,其他某个线程才可以获取这个锁并且执行相同的操作,或者执行这个锁保护的另一个临界区代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值