Java并发编程系列(三)----Lock

前面我们讲到synchronized,既然synchronized已经能够保证线程安全,那么为什么还需要Lock呢?
我们从synchronized的缺陷讲起。

synchronized 的缺陷

  1. 只有线程执行完锁保护的代码或者发生异常或者锁的持有线程主动调用了wait才能释放锁,synchronized锁等待过程没有一种响应中断机制(注意和Thread的interrupt中断机制的区别),也没有等待超时机制。假如线程发生了阻塞,那么其他线程只能一直等待下去,影响效率。我们需要一种机制,在线程等待锁的过程可以响应中断;或者在一定时间内获取不到锁就放弃,转去做其他任务,举个例子。
package com.rancho945.concurrent;

public class LockDemo {

    public synchronized void methodA() {
        System.out.println(Thread.currentThread().getName() + "获取到了锁");
        // 这里模拟一些耗时的工作,5s
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "释放了锁");
    }

    public synchronized void methodB() {
        System.out.println(Thread.currentThread().getName()+ "获取到了锁");
        // 这里模拟一些耗时的工作,1s
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "释放了锁");
    }
}

测试类

package com.rancho945.concurrent;


public class Test {
    public static void main(String[] args) {
        final LockDemo lockDemo = new LockDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("线程"+Thread.currentThread().getName()+"启动");
                lockDemo.methodA();
                System.out.println("线程"+Thread.currentThread().getName()+"销毁");
            }
        }).start();
        //延迟30ms再启动第二个线程,保证第一个线程已经获取到锁
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("线程"+Thread.currentThread().getName()+"启动");
                lockDemo.methodB();
                System.out.println("线程"+Thread.currentThread().getName()+"销毁");
            }
        }).start();;
    }
}

执行结果

线程Thread-0启动
Thread-0获取到了锁
线程Thread-1启动

//线程1在这里等待了大概5s


Thread-0释放了锁
线程Thread-0销毁
Thread-1获取到了锁
Thread-1释放了锁
线程Thread-1销毁

像上面的例子,如果线程1执行methodB的时候,必须等到线程0释放锁才可以继续执行。如果有个需求,methodB获取不到锁则执行其他任务,或者在等待锁的时候可以被中断,显然内置锁无法做到。
2. 一个文件,在写的时候必须单个线程执行,但是在读的时候可以多个线程并发读取,如果用synchronized的同一把锁,那么在读取的时候只能单个线程去读取。
3. 当一个锁被释放后,哪个线程获取到锁是不确定的,也就是synchronized锁是非公平的。极端情况下,那么有可能个线程永远不会得到锁,我门需要一种锁,释放的锁由等待该锁最久的线程获取。

Lock

基于synchronized锁的一些限制条件,JDK提供了一些接口及其实现类。打开Lock源码,我们看一下Lock是个什么东西:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

可以看到,Lock是一个接口,从方法名称我们可以看出来,它提供了加锁lock(),可中断锁lockInterruptibly(),尝试获取锁tryLock()以及在某个时间内尝试获取锁tryLock(long time,TimeUnit unit),释放锁unlock(),newConditon是涉及到线程协作的,在这里暂时不讲。
Lock 是一个接口,我们使用的时候需要用到它的实现类ReentrantLock(可重入锁),看看这些方法都是怎么使用的,他们的使用套路如下

lock()

Lock lock = new ReentrantLock();

public void method() {
    //这里获取锁
    lock.lock();
    try {
        // 这里执行任务
    } finally {
        //这里释放锁
        lock.unlock();
    }
}

tryLock()

Lock lock = new ReentrantLock();
public void method() {
//如果获取锁成功,则返回true,否则返回false。调用无参数tryLock立刻返回结果,调用有参数的则等待一定的时间,在该时间内没有获得锁返回false
    if (lock.tryLock()) {
        try {
            //执行获取到锁的操作
        } finally {
            //释放锁
            lock.unlock();
        }
    } else {
        //执行没有获取到锁的操作
    }
}

lockInterruptibly()

Lock lock = new ReentrantLock();
//这里向上抛出异常,由调用方进行处理
public void method() throws InterruptedException{
    //这里获取锁,并且在等待锁的过程中可以响应中断
    lock.lockInterruptibly();
    try {
        // 这里执行任务
    } finally {
        //这里释放锁
        lock.unlock();
    }
}

或者

Lock lock = new ReentrantLock();
public void methodC() {
    //这里在函数内部捕获异常并处理
    try {
        //获取锁
        lock.lockInterruptibly();
        try {
            // 这里执行任务
        } finally {
            //这里释放锁
            lock.unlock();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
        //处理中断
    }
}

上面的几种锁获取流程都差不多,所有的操作必须放在try中执行,并且在finally块中释放,保证发生异常也可以释放锁。我们把前面synchronized的例子用lock进行同步:

package com.rancho945.concurrent;

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

public class LockDemo {

    private Lock lock = new ReentrantLock();

    public void methodA() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "获取到了锁");
        try {
            // 这里模拟一些耗时的工作,5s
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放了锁");
        }
    }

    public void methodB() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        //如果获取锁成功,则返回true,否则返回false。调用无参数tryLock立刻返回结果,调用有参数的则等待一定的时间,在该时间内没有获得锁返回false
        if (lock.tryLock()) {
            System.out.println(Thread.currentThread().getName() + "获取到了锁");
            try {
                // 这里模拟一些耗时的工作,1s
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "获取锁失败");
        }
    }

    public void methodC() {
        try {
            System.out.println(Thread.currentThread().getName() + "尝试获取锁");
            //获取锁,等待锁的过程中可以被中断
            lock.lockInterruptibly();
            System.out.println(Thread.currentThread().getName() + "获取到了锁");
            try {
                // 这里模拟一些耗时的工作,1s
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                //这里释放锁
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println(Thread.currentThread().getName() + "获取锁被中断");
        }
    }

}

测试类

package com.rancho945.concurrent;


public class Test {
    public static void main(String[] args) {
        final LockDemo lockDemo = new LockDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("线程"+Thread.currentThread().getName()+"启动");
                lockDemo.methodA();
                System.out.println("线程"+Thread.currentThread().getName()+"销毁");
            }
        }).start();
        //延迟30ms再启动第二个线程,保证第一个线程已经获取到锁
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("线程"+Thread.currentThread().getName()+"启动");
                lockDemo.methodB();
                System.out.println("线程"+Thread.currentThread().getName()+"销毁");
            }
        }).start();
    }
}

运行结果

线程Thread-0启动
Thread-0尝试获取锁
Thread-0获取到了锁
线程Thread-1启动
Thread-1尝试获取锁

//注意这里和前面synchronized对比,没有获取到锁,则去执行其他任务,没有一直等待

Thread-1获取锁失败
线程Thread-1销毁
Thread-0释放了锁
线程Thread-0销毁

可以看到,当获取锁失败的时候可以转去做其他的任务,如果是内置的Synchronized锁,则必须等待到获取锁为止。

测试可中断锁

package com.rancho945.concurrent;

public class Test {
    public static void main(String[] args) {
        final LockDemo lockDemo = new LockDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("线程"+Thread.currentThread().getName()+"启动");
                lockDemo.methodA();
                System.out.println("线程"+Thread.currentThread().getName()+"销毁");
            }
        }).start();
        //延迟30ms再启动第二个线程,保证第一个线程已经获取到锁
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("线程"+Thread.currentThread().getName()+"启动");
                //注意methodC是等待锁过程中是可中断的
                lockDemo.methodC();
                System.out.println("线程"+Thread.currentThread().getName()+"销毁");
            }
        });
        thread.start();
        //执行中断,此时锁的等待可以相应中断
        thread.interrupt();
    }
}

执行结果

线程Thread-0启动
Thread-0尝试获取锁
Thread-0获取到了锁
线程Thread-1启动
Thread-1尝试获取锁

//这里就是执行thread.interrupt()后的中断响应


java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at com.rancho945.concurrent.LockDemo.methodC(LockDemo.java:51)
    at com.rancho945.concurrent.Test$2.run(Test.java:29)
    at java.lang.Thread.run(Thread.java:745)
Thread-1获取锁被中断
线程Thread-1销毁
Thread-0释放了锁
线程Thread-0销毁

可以看到,在等待锁的过程中,等待锁的过程中可以相应线程的中断。

ReadWriteLock

ReadWriteLock同样是一个接口

public interface ReadWriteLock {
    //获取读锁
    Lock readLock();
    //获取写锁
    Lock writeLock();
}

该接口的实现类为ReentrantReadWriteLock。所谓的读写锁,就是读的时候可以并发读,写的时候只能一个线程写,并且写的时候不能读。看例子:

package com.rancho945.concurrent;

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

public class ReadWriteLockDemo {
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private int count = 0;
    public int readCount() {
        readWriteLock.readLock().lock();
        System.out.println(Thread.currentThread().getName()+"获得读锁");
        try {
            System.out.println(Thread.currentThread().getName()+"正在读取count---"+count);
            return count;
        } finally{
            //这里输出放在锁释放之前,因为如果放在释放锁之后的话,有可能释放完锁之后还没有进行打印,就执行到第二个线程的获取锁,会造成一种没有释放锁就被其他线程获取的错觉
            System.out.println(Thread.currentThread().getName()+"释放读锁");
            readWriteLock.readLock().unlock();
        }
    }

    public void writeCount(int value){
        readWriteLock.writeLock().lock();
        System.out.println(Thread.currentThread().getName()+"获得写锁");
        try {
            System.out.println(Thread.currentThread().getName()+"正在设置count----"+value);
            count = value;
            //模拟一个1s写的延迟过程
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } finally{
            //这里输出放在锁释放之前,因为如果放在释放锁之后的话,有可能释放完锁之后还没有进行打印,就执行到第二个线程的获取锁,会造成一种没有释放锁就被其他线程获取的错觉
            System.out.println(Thread.currentThread().getName()+"释放写锁");
            readWriteLock.writeLock().unlock();
        }
    }
}

测试写锁

package com.rancho945.concurrent;

public class Test {
    public static void main(String[] args) {
        final ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
        //开启10个线程并发写
        for (int i = 0; i <10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //进行写操作
                    readWriteLockDemo.writeCount(10);
                }
            }).start();
        }
    }
}

执行结果

Thread-0获得写锁
Thread-0正在设置count----10
Thread-0释放写锁
Thread-2获得写锁
Thread-2正在设置count----10
Thread-2释放写锁
Thread-1获得写锁
Thread-1正在设置count----10
Thread-1释放写锁
Thread-3获得写锁
Thread-3正在设置count----10
Thread-3释放写锁
Thread-4获得写锁
Thread-4正在设置count----10
Thread-4释放写锁
Thread-5获得写锁
Thread-5正在设置count----10
Thread-5释放写锁
Thread-6获得写锁
Thread-6正在设置count----10
Thread-6释放写锁
Thread-7获得写锁
Thread-7正在设置count----10
Thread-7释放写锁
Thread-8获得写锁
Thread-8正在设置count----10
Thread-8释放写锁
Thread-9获得写锁
Thread-9正在设置count----10
Thread-9释放写锁

可以看到,写锁必须只能单个线程持有。

测试读锁

package com.rancho945.concurrent;

public class Test {
    public static void main(String[] args) {
        final ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
        //开启10个线程并发读
        for (int i = 0; i <10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //执行读操作
                    readWriteLockDemo.readCount();
                }
            }).start();
        }
    }
}

执行结果

Thread-2获得读锁
Thread-4获得读锁
Thread-4正在读取count---0
Thread-0获得读锁
Thread-3获得读锁
Thread-1获得读锁
Thread-1正在读取count---0
Thread-1释放读锁
Thread-7获得读锁
Thread-7正在读取count---0
Thread-7释放读锁
Thread-8获得读锁
Thread-8正在读取count---0
Thread-8释放读锁
Thread-3正在读取count---0
Thread-3释放读锁
Thread-4释放读锁
Thread-0正在读取count---0
Thread-0释放读锁
Thread-5获得读锁
Thread-5正在读取count---0
Thread-2正在读取count---0
Thread-5释放读锁
Thread-9获得读锁
Thread-9正在读取count---0
Thread-6获得读锁
Thread-6正在读取count---0
Thread-6释放读锁
Thread-9释放读锁
Thread-2释放读锁

可以看到,读锁可以同时被多个线程获取。

读写锁测试

package com.rancho945.concurrent;

public class Test {
    public static void main(String[] args) {
        final ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //执行写操作
                readWriteLockDemo.writeCount(10);
            }
        }).start();
        //等待20ms再开始启动读操作,保证前面一个线程获取到了写锁
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                //执行读操作
                readWriteLockDemo.readCount();
            }
        }).start();
    }
}

执行结果

Thread-0获得写锁
Thread-0正在设置count----10
//这里停顿了一秒钟,证明读的线程一直在等待着写锁。
Thread-0释放写锁
Thread-1获得读锁
Thread-1正在读取count---10
Thread-1释放读锁

读写锁总结

  1. 读锁可以被多个线程占用,但只要只有线程占用着读锁,其他线程如果要申请写锁,就必须等待所有持有读锁的线程释放读锁
  2. 写锁只能被一个线程占用,并且写锁在占用期间,任何线程都无法获取读锁。

公平锁

前面我们讲到,内置锁无法做到公平性,也就是无法保证哪个线程会先获得锁,有可能等待了很久的线程没有获得锁,而等待时间很短的线程获得了锁。而ReentrantLock和ReadWriteReentrantLock可以实现公平锁,也就是等待时间久的可以先获得锁,等待时间短的后获得锁,他们默认情况下是非公平锁,如果要实现其公平性,则需要在构造函数中传入true参数实现公平锁。

参考资料

《深入理解Java虚拟机》 周志明 著
《Java并发编程的艺术》 方腾飞 著
《Java并发编程实战》 Brian Goetz等著 童云兰等译

你可能感兴趣的:(Java多线程)