0
点赞
收藏
分享

微信扫一扫

并发编程之Lock


Lock

我们先看一张Lock锁的继承结构图:

并发编程之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();
}

这是一个接口,与synchronized 关键字相比,使用Lock更加灵活,可以自由的控制获取锁和释放锁,同时也提供了几种获取锁不同的方式。

lock: 用于获取锁,如果没有获取到,则会一直等待,处于阻塞状态。不可以被中断的。

lockInterruptibly:用于获取锁,如果没有获取到锁,则会一直等待,处于阻塞状态,但是这个方法是可以响应中断的。

tryLock(): 从名字就知道,尝试获取锁,所以,这个方法是尝试获取锁,如果获取到锁则会返回true,如果没有获取到锁就返回false,这个方法不会进入阻塞状态。

tryLock(time):这个方法其实与tryLock()方法类型,也是尝试获取锁,但是在获取锁的时候,不会像tryLock()那样立即返回,而是如果没有获取到锁,会进行time时间的等待,如果在等待时间内获取锁,也会正常返回true,并持有锁,如果超时后还是未获取到锁,就返回false。

unLock(): 就是释放锁。获取锁就需要释放,否则下一个线程就一直获取不到锁。

newCondition(): 这个方法就相当于获取synchronized的monitor对象那样,可以手动阻塞线程,也可以手动唤醒线程。

上面Lock是一个接口,我们可以看到它的一个基本的实现类,ReentrantLock,我们首先先不关注这个类,只是简单作为一个实现类来使用。

public class LockTest {
private static int num = 0;
private static final Lock lock = new ReentrantLock();
public static void addNum() {
lock.lock();
try {
num++;
} finally {
lock.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
for (int k = 0; k < 10; k++) {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
Thread thread = new Thread(LockTest::addNum);
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList){
thread.join();
}
System.out.println(LockTest.num);
}
}
}

上面可以实现在多线程情况下,准确的相加,但是如果把锁去掉,则会出现最终num值小于10000的情况,这就是线程安全问题。

我们可以通过锁来实现与前面Synchronized同样的效果。

RenntrantLock

下面我们看一下Lock的实现类,重入锁ReentrantLock。

public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
public Condition newCondition() {
return sync.newCondition();
}
}

可以看到,ReentrantLock内部其实是使用了AQS(AbstractQueuedSynchronizer) 和CAS (CompareAndSet) 来实现的。

重入锁是可以反复进入的.

lock.lock();
lock.lock();
try{
i++;
}finally{
lock.unlock();
lock.unlock();
}

在这种情况下,一个线程连续两次获得同一把锁时允许的。如果不允许这样操作,那么同一个线程在第2次获得锁时,将会和自己产生死锁。程序就会“卡死”在第2次申请锁的过程中。但需要注意的是,如果同一个线程多次获取锁,那么在释放锁的时候,也必须释放相同次数。如果释放锁的次数多了,那么就会得到一个java.lang.IllegalMonitorStateException 异常。反之,如果释放少了,那么相当于线程还持有这个锁,因此,其他线程也无法进入到临界区。

我们看看ReentrantLock 的构造函数:

public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

在大多数情况下,锁的申请都是非公平的。也就是说,线程1首先请求了锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1可以获取到锁还是线程2可以获取到锁呢?这是不一定的,系统只是会从这个锁的等待队列中随机挑选一个。因此不能保证其公平性。而公平的锁,则不是这样的,它会按照时间的先后顺序,保证先到这先得,后到者后得。公平锁的一大特定是:它不会产生饥饿现象。主要你排队,最终还是可以等到资源的。如果我们使用Synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。如上面的构造函数2,当我们传入的是true,则是公平锁,默认是非公平锁。

下面我们演示一下公平锁和非公平锁的区别:

public class ReentrantLockTest {
private static ReentrantLock reentrantLock = new ReentrantLock(false)
public static class IRunnable implements Runnable
@Override
public void run() {
while (true){
reentrantLock.lock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
System.out.println(Thread.currentThread().getName() +" get lock and handle work.");
}finally {
reentrantLock.unlock();
}
}
}
}
public static void main(String[] args) {
IRunnable iRunnable = new IRunnable();
new Thread(iRunnable,"Thread-A").start();
new Thread(iRunnable,"Thread-B").start();
}
}

可以看到,上面我们使用的是非公平模式,我们看看最后的输出结果:

Thread-A get lock and handle work.
Thread-A get lock and handle work.
Thread-A get lock and handle work.
Thread-A get lock and handle work.
Thread-A get lock and handle work.
.... 一长串A
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.
Thread-B get lock and handle work.

通过结果可以看出,根据系统的调度,回更加倾向于分配给已经持有锁的线程,这种分配时更加高效的,但是无公平性可言。下面我们设置为公平模式:

private static ReentrantLock reentrantLock = new ReentrantLock(true);

输出结果:

Thread-A get lock and handle work.
Thread-B get lock and handle work.
Thread-A get lock and handle work.
Thread-B get lock and handle work.
Thread-A get lock and handle work.
Thread-B get lock and handle work.
Thread-A get lock and handle work.
Thread-B get lock and handle work.

可以看到,线程A和线程B是交替获取到锁的,是一种公平模式。

就重入锁的实现来看,它主要集中在java层面。在重入锁的实现中,主要包含三个要素。

第一,原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有了。

第二,等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。

第三,阻塞原语park() 和 unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。

Condition

我们前面已经知道了在使用synchronized情况下,如何阻塞线程和恢复线程,是基于monitor对象的wait方法和notify的方法。如果理解了wait和notify方法,那么就可以把monitor的概念搬到Condition了。它与wait() 和 notify() 方法的作用大致相同。但是wait() 方法 和 notify() 方法是与 synchronized 关键字合作使用的,而Condition对象是与重入锁相关联的。通过Lock接口(重入锁就实现了这一接口)的 Condition newCondition() 方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition对象,我们就可以让线程在合适的时候等待,或者在某一个特定的时刻得到通知,继续执行。

public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}

以上方法的含义:

  • await() 方法会使得当前线程等待,同时释放当前锁,当其他线程中使用signal() 方法或者 signalAll() 方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和 Object.wait() 方法相似。
  • awaitUninterruptibly() 方法和 await() 方法基本相同,但是它并不会在等待的过程中响应中断。
  • singal() 方法用于唤醒一个在等待中的线程,singalAll() 方法会唤醒所有在等待中的线程,和Object.notify()方法类似。

下面我们先用synchronized关键字演示一个例子:

public class ConditionTest {
private static final Object o = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (o){
try {
System.out.println("wait...");
o.wait();
System.out.println("running...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();

// 保证上述线程进入阻塞
TimeUnit.SECONDS.sleep(1);

// o.notify();
synchronized (o){
o.notify();
}
}
}

结果就不展示了,可以自行运行查看。其实就是在我们使用synchronized关键字进行线程同步的时候,需要使用monitor对象(这里的o)的wait方法进行阻塞。同时唤醒的使用也需要使用monitor对象进行notify,注意,唤醒也需要在synchronized关键字语句块中,否则会抛出 java.lang.IllegalMonitorStateException 异常。

下面我们使用ReentrantLock 实现上面的功能:

public class ConditionTest {
public static void main(String[] args) throws InterruptedException {
final ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("await...");
condition.await();
System.out.println("running...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}).start();

TimeUnit.SECONDS.sleep(1);
reentrantLock.lock();
condition.signal();
reentrantLock.unlock();
}
}

上面就是使用ReentrantLock 和 Condition 进行实现的与synchronized同样的功能的代码。

reentrantLock.lock();
condition.signal();
reentrantLock.unlock();

同样,singal或singalAll方法也是需要放置到 lock和unlock之间的,否则也会抛 java.lang.IllegalMonitorStateException。

ReadWriteLock 读写锁

同样,我们也先来看一张读写锁的继承结构图:

并发编程之Lock_并发编程_02

在看一下ReadWriteLock 的代码:

public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

这个接口就定义了两个方法,一个是获取读锁的,一个是获取写锁的。

ReadWriteLock 是JDK5中提供的读写分离的锁。读写分离锁可以有效地帮助减少锁的竞争,提升系统性能。用锁分离的机制来提升性能非常容易理解,比如 线程A1,A2,A3进行写操作,B1,B2,B3进行读操作,如果使用重入锁或者内部锁,从理论上说所有读之间、读与写之间、写与写之间都是串行操作。当B1进行读取时,B2,B3则需要等待锁。由于读操作并不对数据的完整性造成破坏,这种等待显然是不合理的。因此,读写锁就有发挥功能的余地了。

在这种情况下,读写锁允许多个线程同时读,使得B1,B2,B3之间真正并行。但是,考虑到数据完整性。写写操作和读写操作间依然是需要相互等待和持有锁的。总的来说,读写锁的访问约束如下:




非阻塞

阻塞


阻塞

阻塞

  • 读-读不互斥:读读之间不阻塞。
  • 读-写互斥:读阻塞写,写也阻塞读。
  • 写-写互斥:写写阻塞。

如果在系统中,读操作的次数远远大于写操作的次数,则读写锁就可以发挥最大的功效,提升系统的性能。

ReentrantReadWriteLock

ReadWriteLock 的 实现类就是 ReentrantReadWriteLock。通过名字就知道,这是一个可重入的,并且是一个读写分离的锁。我们来看看源码:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;

public ReentrantReadWriteLock() {
this(false);
}

public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }

abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync
static final class FairSync extends Sync {}
public static class ReadLock implements Lock, java.io.Serializable
public static class WriteLock implements Lock, java.io.Serializable {}
}

将ReentrantReadWriteLock的具体实现删除了,我们通过内部的几个重要的类可以知道,内部是通过CAS和AQS来实现加锁机制的,虽然ReadWriteLock没有继承自Lock。但是内部实现了Lock,来实现锁的功能。

下面我们使用一个例子来测试一下:

下面是使用ReadWriteLock 来测试的代码和结果:

public class ReadWriteLockTest {
private static long value;

public static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

public static long readMethod() {
readLock.lock();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
return value;
}

private static void writeMethod(int val) {
writeLock.lock();
try {
TimeUnit.MILLISECONDS.sleep(2);
value = val;
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
Thread t1 = new Thread(ReadWriteLockTest::readMethod);
t1.start();
threadList.add(t1);
}

for (int i = 0; i < 10; i++) {
final int index = i;
Thread t2 = new Thread(() -> {
ReadWriteLockTest.writeMethod(index);
});
t2.start();
threadList.add(t2);
}

for (Thread thread : threadList) {
thread.join();
}
long end = System.currentTimeMillis();
System.out.println("spend time : " + (end - start));
}
}

结果:

spend time : 93

下面我们使用ReentrantLock来测试同样的代码:

public class ReentrantLockTest {

private static long value;

public static final ReentrantLock lock = new ReentrantLock();
public static long readMethod() {
lock.lock();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return value;
}

private static void writeMethod(int val) {
lock.lock();
try {
TimeUnit.MILLISECONDS.sleep(2);
value = val;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
Thread t1 = new Thread(ReentrantLockTest::readMethod);
t1.start();
threadList.add(t1);
}

for (int i = 0; i < 10; i++) {
final int index = i;
Thread t2 = new Thread(() -> {
ReentrantLockTest.writeMethod(index);
});
t2.start();
threadList.add(t2);
}

for (Thread thread : threadList) {
thread.join();
}
long end = System.currentTimeMillis();
System.out.println("spend time : " + (end - start));
}
}

测试结果:

spend time : 269

可以看出,当程序中读操作占比比较大时,读写锁的优势就显示得更明显了。


举报

相关推荐

0 条评论