Java 并发编程(五)同步锁Lock的使用

除了上篇讲的synchronized关键字来实现同步,java5中也提供了Lock对象来实现同步的效果,我们重点学习以下两个知识点。

  • ReentrantLock类
  • ReentrantReadWriteLock类

1. ReentrantLock 类的使用

java多线程中,synchronized关键字实现多线程之间的同步,但是ReentrantLock类也能达到同样的效果,并且在扩展功能上也更强大,比如嗅探锁定、多路分支通知等,也比synchronized更加灵活。

1.1 简单实例

创建类Service.java,如下:

public class Service {
	// 创建ReentrantLock对象
    private Lock lock = new ReentrantLock();

    public void testMethod() {
	    // 获取锁
        lock.lock();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + (i + 1));
        }
        // 释放锁
        lock.unlock();
    }
}

创建线程ThreadA和ThreadB类,如下

public class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethod();
    }
}

public class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethod();
    }
}

调试代码:

public class Main {
    public static void main(String[] args) {
	    // 创建执行对象
        Service service = new Service();
        ThreadA[] threadAS = new ThreadA[2];
        ThreadB[] threadBS = new ThreadB[2];
        for (int i = 0; i < threadAS.length; i++) {
            threadAS[i] = new ThreadA(service);
            threadBS[i] = new ThreadB(service);
        }
        // 启动多线程
        for (int i = 0; i < threadAS.length; i++) {
            threadAS[i].start();
            threadBS[i].start();
        }
    }
}

执行结果:

Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-1 5
Thread-3 1
Thread-3 2
Thread-3 3
Thread-3 4
Thread-3 5
Thread-2 1
Thread-2 2
Thread-2 3
Thread-2 4
Thread-2 5

从结果上分析,当前线程执行结束后才释放锁,其他线程才可获取锁才可以继续打印,但是线程之间打印是随机的。

1.2 与synchronized同步一致

将1.1中Service.java,ThreadA.java和ThreadB.java修改如下:

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

    public void testMethodA() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " start...");
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName() + " end...");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void testMethodB() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " start...");
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName() + " end...");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethodA();
    }
}

public class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethodB();
    }
}

调试代码:

public class Main {
    public static void main(String[] args) {
        Service service = new Service();
        ThreadA threadA = new ThreadA(service);
        threadA.setName("A");
        threadA.start();
        ThreadA threadAA = new ThreadA(service);
        threadAA.setName("AA");
        threadAA.start();
        ThreadB threadB = new ThreadB(service);
        threadB.setName("B");
        threadB.start();
        ThreadB threadBB = new ThreadB(service);
        threadBB.setName("BB");
        threadBB.start();
    }
}

调试结果:

A start...
A end...
AA start...
AA end...
B start...
B end...
BB start...
BB end...

从结果上看,lock.lock()方法的线程持有的是对象锁,效果和synchronized一样。

1.3 使用Condition实现等待/通知

synchronized使用的wait()和notify()/notifyAll()方法的结合可以实现线程之间的等待/通知,ReentrantLock类也有同样的功能实现,但是要使用Condition对象,Condition类是java5提供的,其灵活性更高,可以实现多路通知功能,也就是一个Lock对象可以创建多个Condition对象实例,线程对象可以注册在指定的Condition中从而可以选择性地给线程通知,调度上更加灵活。

示例:

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

    public void awaitA() {
    	 lock.lock();
        try {
            System.out.println("waitA time:" + System.nanoTime());
            conditionA.await();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println("lockA released");
        }
    }

    public void awaitB() {
        try {
            lock.lock();
            System.out.println("waitB time:" + System.nanoTime());
            conditionB.await();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println("lockB released");
        }
    }

    public void signalA() {
        try {
            lock.lock();
            System.out.println("signalA time :" + System.nanoTime());
            conditionA.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signalB() {
        try {
            lock.lock();
            System.out.println("signalB time :" + System.nanoTime());
            conditionB.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

创建线程类:

public class ThreadA extends Thread {
    private MyService myService;

    @Override
    public void run() {
        myService.awaitA();
    }

    public ThreadA(MyService myService) {
        this.myService = myService;
    }
}

public class ThreadB extends Thread {
    private MyService myService;

    @Override
    public void run() {
        myService.awaitB();
    }

    public ThreadB(MyService myService) {
        this.myService = myService;
    }
}

调试程序:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        ThreadA a = new ThreadA(myService);
        a.start();
        ThreadB b = new ThreadB(myService);
        b.start();
        TimeUnit.SECONDS.sleep(3);
        myService.signalA();
        myService.signalB();
    }
}

调试结果:

waitA time:1506428352433847000
waitB time:1506428352435218000
signalA time :1506428355439920000
signalB time :1506428355440144000
lockA released
lockB released

从结果上看,已经实现了等待/通知模式,对此说明,Object类中的wait()方法相当于Condition中await()方法,Object类中的notify()方法相当于Condition类中的signal()方法。Object类中notifyAll()方法相当于Condition类中的signalAll()方法。

1.4 使用Lock对象实现生产者/消费者模式,交替打印数据

创建Service.java类,代码如下:

public class Service {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean hasValue = false;

     public void set() {
        try {
            lock.lock();
            while (hasValue == true) {
                System.out.println("生产了数据: ## 连续");
                condition.await();
            }
            System.out.println("生产了数据:#");
            hasValue = true;
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void get() {
        try {
            lock.lock();
            while (hasValue == false) {
                System.out.println("消费了数据: $$ 连续");
                condition.await();
            }
            System.out.println("消费了数据:$");
            hasValue = false;
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

创建生产者/消费者线程类,代码如下:

public class ProducerThread extends Thread {
    private Service service;

    public ProducerThread(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            service.set();
        }
    }
}

public class ConsumerThread extends Thread {
    private Service service;

    public ConsumerThread(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            service.get();
        }
    }
}

调试代码:

public class Run {
    public static void main(String[] args) {
        Service service = new Service();
        ProducerThread[] producerThreads = new ProducerThread[10];
        ConsumerThread[] consumerThreads = new ConsumerThread[10];
        for (int i = 0; i < 10; i++) {
            producerThreads[i] = new ProducerThread(service);
            consumerThreads[i] = new ConsumerThread(service);

            producerThreads[i].start();
            consumerThreads[i].start();
        }
    }
}

调试结果:

生产了数据: ## 连续
生产了数据: ## 连续
生产了数据: ## 连续
生产了数据: ## 连续
消费了数据:$
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
生产了数据:#
生产了数据: ## 连续
生产了数据: ## 连续
生产了数据: ## 连续
消费了数据:$
消费了数据: $$ 连续
生产了数据:#
生产了数据: ## 连续
消费了数据:$
消费了数据: $$ 连续
生产了数据:#
生产了数据: ## 连续
生产了数据: ## 连续

从输出结果可以看出是交替输出的,但是出现了"生产了数据: ## 连续"和"消费了数据: $$ 连续"有时候是连续打印的,原因是程序中使用的Condition对象中signalAll()方法来通知所有await()的线程,那么有可能通知的是同类,所以就出现以上连续的情况了。

2. ReentrantReadWriteLock 类的使用

锁ReentrantLock是互斥的,同一时间只有一个线程可执行lock.lock()方法,这样保证了实例变量的线程安全,但是效率比较低,故JDK提供了另一种读写锁ReentrantReadWriteLock类。使其加速运行速度。
从字面上可以看出,读写锁包含了两个锁,一个是读锁,一个是写锁,读锁是共享锁,写锁则是排他锁。意思就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。也就是在没有现成进行写操作时,进行读操作的多个线程都可以获取读锁,而进行写入操作的线程只有在获取了写锁后才能进行写操作。多个线程可以同时进行读操作,但是同一时间只允许一个线程进行写操作。这就是ReentrantReadWriteLock类的全面语义理解。
还是通过代码示例来理解会更加容易

2.1 读读共享锁

创建Service.java类,代码如下:

public class Service {
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    public void read() {
        try {
            reentrantReadWriteLock.readLock().lock();
            System.out.println("获得读锁 " + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.readLock().unlock();
        }
    }
}

创建读线程对象,代码如下:

public class ReadThread extends Thread {
    private Service service;

    public ReadThread(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.read();
    }
}

调试代码:

public class Main {
    public static void main(String[] args) {
        Service service = new Service();
        ReadThread readThreadA = new ReadThread(service);
        ReadThread readThreadB = new ReadThread(service);
        readThreadA.start();
        readThreadB.start();
    }
}

调试结果:

获得读锁 Thread-0:1506430190938
获得读锁 Thread-1:1506430190938

通过结果可看出,两个线程几乎同时进入lock()方法后面的代码,说明lock.readLock()读锁可以提高运行效率,允许多个线程同时执行lock()后面的代码。

2.2 写写互斥

修改上例中的Service.java类,修改如下:

public class Service {
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    public void write() {
        try {
            reentrantReadWriteLock.writeLock().lock();
            System.out.println("获得写锁 " + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(5);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.writeLock().unlock();
        }
    }

创建写线程类,代码如下:

public class WriteThread extends Thread {
    private Service service;

    public WriteThread(Service service) {
        this.service = service;
    }

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

调试代码:

public class Main {
    public static void main(String[] args) {
        Service service = new Service();
        WriteThread writeThreadA = new WriteThread(service);
        WriteThread writeThreadB = new WriteThread(service);
        writeThreadA.start();
        writeThreadB.start();
    }
}

调试结果:

获得写锁 Thread-0:1506430557896
获得写锁 Thread-1:1506430562898

从结果可看到,两个线程获得锁时差为5s,是同步执行的,所以写写锁是互斥的。

2.3 读写锁互斥

按照上例,只需要修改调试程序即可,如下:

public class Main {
    public static void main(String[] args) {
        Service service = new Service();
        ReadThread readThread = new ReadThread(service);
        WriteThread writeThread = new WriteThread(service);
        
        writeThread.start();
        readThread.start();
    }
}

调试结果:

获得写锁 Thread-1:1506430757186
获得读锁 Thread-0:1506430762191

可见,运行时间有差别,获取写锁的时候,是获取不到读锁的,所以读写锁互斥。

3 小结

通过几个简单的示例,我们在后续多线程同步代码时可使用Lock对象替换synchronized关键字,而且Lock对象比synchronized功能更强大。Lock对象是synchronized的进阶,掌握Lock有助于学习java并发包源代码的实现原理,在并发包中大量的类使用Lock接口作为同步的处理方式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值