美文网首页Android开发经验谈Android技术知识Android开发
Java 并发编程—线程间的共享和协作(二)

Java 并发编程—线程间的共享和协作(二)

作者: 未见哥哥 | 来源:发表于2019-04-14 03:21 被阅读68次

    线程间的共享和协作

    线程间的共享和协作

    Lock 显示锁

    在 Java 中,一般情况下都是使用 synchronized 关键字来加锁的, synchronized 这种机制一旦开始获取锁,是不能中断的,也没有提供尝试获取锁的功能。

    JDK1.5 提供了 Lock 接口,开发人员显示地去操作锁的获取释放,因此被称为显式锁。并且提供了synchronized不提供的机制。

    Lock API

    • 阻塞式获取锁
    void lock();
    

    这种方式与 synchronized 获取锁差不多,如果需要获取的锁被其他线程持有,那么将挂起阻塞等待其他线程释放锁。

    • 尝试非阻塞式地获取锁

    非阻塞去尝试获取锁,如果获取到锁会返回 true,如果没有获取到锁,则返回 false。带有时间参数的方法表示开始获取锁时,如果此时该锁没有被其他线程持有,那么则返回 true,否则将进行超时等待,中途如果线程发生了中断信号,则 tryLock 方法抛出中断异常。

    boolean tryLock();
    
    //如果线程发出中断信息,则该方法会抛出 InterruptedException 异常。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    • 能被中断获取锁

    请求锁,如果当前锁被其他线程持有,那么将阻塞等待,除非外界调用 thread.interrupt() 发出中断信号。

    void lockInterruptibly() throws InterruptedException;
    
    • 释放锁

    释放锁,必须是在 finally 块中执行,这样能保证锁能正常的被释放,避免造成死锁。

    void unlock();
    

    下面是 Lock 锁的标准写法:

    finally 能确保锁能正常被释放,避免出现死锁的情况。

    
    //阻塞式获取锁
    lock.lock();
    
    try{
        //对共享资源的操作
    }finally{
        lock.unlock();
    }
    
    //获取一个可中断的锁
    try{
        lock.lockInterruptibly();
    }catch(InterruptedException e){
        //发生了异常,不应该往下执行,因为没有获取到锁就调用unlock 会抛出异常
        return;
    }
    try{
        //对共享资源的操作
    }finally{
        lock.unlock();
    }
    

    ReentrantLock

    ReetrantLock 从字面意思来看,它表示可重入锁,它实现了 Lock 接口,ReetrantLock 可以实现公平和非公平锁。

    什么是可重入锁?

    可重入锁就是当前线程获取到锁,并执行需要加锁的方法时,在方法内部再次的获取该锁是可以直接锁去,这种情况就是避免了自己把自己给锁死了。ReentrantLocksynchronized 都是可重入锁。

    文字看不懂,可以直接看下面这段代码就明白了

    synchronized(this){
        //do sth
        
        synchronized(this){//再次获取同一把锁,可以直接获取
            //do sth
        }
    }
    
    

    ReentrantLock实现公平锁和非公平锁

    公平锁表示线程的执行顺序按照请求锁的顺序来执行,也就是先请求锁的线程优先获取锁。非公平锁则是由 CPU 负责线程去调度获取锁,不一定是按照先请求锁的线程优先获取锁。

    • 获取一个公平锁

    非公平锁只要在构造参数参入 false 即可,ReentrantLock的 的缺省实现就是非公平锁。

    Lock lock = new ReentrantLock(true);
    

    那么这里就有一个疑问了,公平锁和非公平锁的效率哪个比较高呢?

    当然是非公平锁, ReentrantLock 和 synchronized 内部的缺省实现都是非公平锁。因为线程B在请求锁时发现当前锁被其他线程A持有,那么线程B该发生上下文切换,将处于挂起,这时如果还有另外一个线程C过来请求锁,发现锁还是被其他线程A持有,那么线程C该发生上下文切换,处于挂起状态,此时如果线程 A 释放锁了,那么此时锁只能被线程B 获取,线程 C 只能等待到线程 B 执行完毕。恢复一个被挂起的线程与该线程真正开始执行之间存在严重的延迟

    读写锁ReentrantReadWritLock

    在上面讲到的 synchronized 和 ReentrantLock 都是独占锁,也就是也就说同一时刻只能有一个线程去访问。而这里提到的读写锁是在同一时刻,允许多个读线程去访问,但是在写线程访问时,所有其他读写线程都会被阻塞。ReentrantReadWritLock 内部维护了两个锁,分别为读锁(共享锁)和写锁(排他锁),它们内部都是实现Lock 接口。

    • 读读共享
    • 读写互斥
    • 写写互斥
    • 写读互斥
    
    public class ReentrantReadWriteLock
            implements ReadWriteLock, java.io.Serializable {
        
        private final ReentrantReadWriteLock.ReadLock readerLock;
    
        private final ReentrantReadWriteLock.WriteLock writerLock;
        
        public static class ReadLock implements Lock, java.io.Serializable {}
    
        public static class WriteLock implements Lock, java.io.Serializable {}
    
    }
    

    在一般情况下,读写锁的性能要比独占锁好,因为大多数场景下读的操作是大于写的,因此使用读写锁的性能是要比独占锁好的。

    下面来演示一下读写互斥:

    开启读线程内部睡眠10s,主线程sleep1s后开始开始写线程,执行结果如下:
    读线程获取到锁:1555181819480
    写线程获取到锁:1555181829482
    写线程是在读线程执行完之后才能开始写的

    public class ReadAndWriteLockDemo {
    
    
        static class Service {
            ReadWriteLock lock = new ReentrantReadWriteLock();
    
            public void read() {
                lock.writeLock().lock();
    
                try {
                    System.out.println(Thread.currentThread().getName() + "获取到锁:" + System.currentTimeMillis());
    
                    try {
                        Thread.sleep(10000);//睡眠10s
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    lock.writeLock().unlock();
                }
            }
    
            public void write() {
                lock.writeLock().lock();
    
                try {
                    System.out.println(Thread.currentThread().getName() + "获取到锁:" + System.currentTimeMillis());
    
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    lock.writeLock().unlock();
                }
            }
        }
    
    
        static class ThreadA extends Thread {
            private final Service service;
    
            public ThreadA(String name, Service service) {
                super(name);
                this.service = service;
    
            }
    
            @Override
            public void run() {
                super.run();
                service.read();
            }
        }
    
    
        static class ThreadB extends Thread {
            private final Service service;
    
            public ThreadB(String name, Service service) {
                super(name);
                this.service = service;
    
            }
    
            @Override
            public void run() {
                super.run();
                service.write();
            }
        }
    
    
        public static void main(String[] args) {
            Service service = new Service();
    
            ThreadA readTthread = new ThreadA("读线程", service);
            ThreadB writeThread = new ThreadB("写线程", service);
    
            //先执行读线程,内部会 sleep 10s
            readTthread.start();
    
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
    
            //执行写线程
            writeThread.start();
    
        }
    }
    

    Condition接口

    位于 java.util.concurrent juc 包下

    在 synchronized 内置锁中,一般是使用对象的 wait() 和 notify() 实现等待通知机制,而在 Lock 显示锁中是使用 Condition 的 await 和 signal 实现等待通知机制。

    Condition 对象的获取

    private Lock lock = new ReentrantLock();
    Condition xiaomi9Condition = lock.newCondition();
    

    对比 Object 的 wait 和 notify

    signal()/notify()
    signalAll/notifyAll()
    await()/wait()
    await(long time, TimeUnit unit)/wait(long millis),wait(long var1, int var3)
    

    使用 Lock 配合 Condition 实现等待通知机制

    使用 Lock 配合 Condition 实现等待通知机制

    在之前的使用 synchronized 配合 wait() 和 notify()实现老王买小米9的栗子Java 并发编程-线程间的共享和协作1,现在使用 Lock 配合 Condition 来改造这个栗子。

    package com.example.waitAndnotify;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /*
    Lock 显示锁实现的 等待/通知 机制
     */
    public class Shop implements Runnable {
    
        private Lock lock = new ReentrantLock();
    
        //小米9条件
        private Condition xiaomi9Condition = lock.newCondition();
    
        private int xiaomi9Discount = 10;
    
        /*
        通知方法
         */
        public void depreciateXiaomi9(int discount) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "收到总部通知,现在进行小米9打" + discount + "折活动,通知米粉们来买吧");
                xiaomi9Discount = discount;
                //通知所有客户:小米9打折了哦,赶紧去看看价格吧。
                xiaomi9Condition.signalAll();
                //通知一个客户
                //xiaomi9Condition.signal();
            } finally {
                lock.unlock();
            }
    
    
        }
    
        /*
        等待
         */
        public void getXiaomi9Price() {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "正在查询小米9价格");
    
                while (xiaomi9Discount > 8) {
                    try {
                        System.out.println(Thread.currentThread().getName() + "发现小米9价格折扣为" + xiaomi9Discount + "太少,我要开始等待降价,老板,降价了,就通知我哦,开始等待...");
                        xiaomi9Condition.await();
                        System.out.println(Thread.currentThread().getName() + "收到通知:小米9搞活动,打折了哦,目前折扣为:" + xiaomi9Discount);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "剁手买顶配小米9:" + xiaomi9Discount + "折购入");
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            Shop shop = new Shop();
    
            //老王想要买手机
            Thread getXiaomiPriceThread = new Thread(shop);
            //老张也要买手机
            Thread getXiaomiPriceThread2 = new Thread(shop);
            getXiaomiPriceThread.start();
            getXiaomiPriceThread2.start();
            Thread.sleep(1000);
    
            //降价了
            shop.depreciateXiaomi9(9);
    
            Thread.sleep(1000);
            //又降价了
            shop.depreciateXiaomi9(8);
        }
    
    
        @Override
        public void run() {
            getXiaomi9Price();
        }
    }
    

    输出结果

    • xiaomi9Condition.signal();
    买手机栗子
    • xiaomi9Condition.signalAll();
    买手机栗子

    总结

    本文是线程间的共享和协作的第二篇博客,本文主要对比学习了 Lock 和 synchronized 的差异的知识。

    参考

    记录于2019年4月14日

    相关文章

      网友评论

        本文标题:Java 并发编程—线程间的共享和协作(二)

        本文链接:https://www.haomeiwen.com/subject/kpqhwqtx.html