并发控制 - Java的锁
ShawJie

并发控制 - Java的锁

之前谈论了关于两种并发控制思路,乐观锁与悲观锁。在实际开发过程中,在多线程开发环境,还需要保证多线程的原子性、可见性、有序性,这时候通常就会引入锁机制,由于本人的主要开发语言是Java,所以这回就来主要谈论一下Java内对于锁的实现和使用。

Java种有两种锁实现,分别是Java内置的Synchronized关键字和由Java1.5引入的java.util.concurrent.locks

Synchronized

Synchronized是Java内置的关键字,一般用于关键部分的同步处理控制,以将并发的任务逻辑转换成串行的处理逻辑。同理,Synchronized在保证了数据的一致性的同时,也大大的降低了任务的处理效率。因此,在高性能、高并发、高流量的WEB服务器上,对于Synchronized关键字,还是能不用尽量不用。

Synchronize使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 同步方法块
* 给obj对象加锁
*/
synchronized(obj){
// do something
globalVariable = ...
}
/*
* 同步方法
* 相当于给当前对象加锁
*/
synchronized method(Object param){
// do something
globalVariable = ...
}
/*
* 同步静态方法
* 给类加锁
*/
static synchronized method(Object param){
// do something
globalVariable = ...
}
Tip使用synchronized关键字施加的同步块会在同步块内抛出异常后自动释放锁,而Lock不会,需要调用lock.unlock();才会释放锁

Synchronized原理

查看Synchronized部分编译后的字节码文件可以发现有两个指令,mointorentermointorexit指令,可以了解到Jvm种的同步机制都是基于Mointor对象实现的,但要注意,同步方法并不是由 monitorentermonitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

java.util.concurrent.locks

Concurrent并发包是在java1.5时引入的,旨在更优雅的解决并发所带来的问题,本文要谈论其中重点的两个锁,ReentrantLock重入锁,和ReadWriteLock读写锁。

在具体了解重入锁和读写锁之前,我们得先了解他们的公共实现的Lock接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Lock {
// 显示加锁
void lock();

// 获得锁,但优先响应中断
void lockInterruptibly() throws InterruptedException;

// 尝试获取锁,若锁被占用,则返回false
boolean tryLock();

// 尝试在x时间内获取锁,若时间内无法获取锁,则放弃
boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;

// 显示释放锁
void unlock();

Condition newCondition();
}

简单实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static ReentrantLock lock = new ReentrantLock();
static int count; // 全局变量

public static void main(String[] args){
Runnable runnable = () -> {
lock.lock();
try{
count ++;
} finally{
// 判断锁持有者是否为当前线程
if(locak.isHeldByCurrentThread){
lock.unlock();
}
}
};

Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);

thread1.start();
thread2.start();
}

可以注意到,相对于synchronized关键字加锁来说,lock的加锁和释放锁都是显示的,需要注意的事当获取锁线程出现异常后并不会自动释放锁,需要在finally块中显示释放锁,不然该锁可能永远无法释放变成死锁。

Interrupted(中断)
中断机制是Java并发工具包里提供以应付无限等待的情况的机制,在synchronized关键字加锁情况中,当锁被占有时,线程会进入锁池,等待锁持有线程释放锁,这时候调用thread.interrupt(),线程是不会立即响应的。这时若锁持有线程发生了死锁,则会在锁池积压大量的线程无法释放。

实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) throws Exception{
Runnable runnable = () -> {
try{
lock.lockInterruptibly();
Thread.sleep(30000); // 模拟死锁
}catch(InterruptedException e){
System.out.printf("thread interrupted %s \n", Thread.currentThread().getName());
}finally{
if (lock.isHeldByCurrentThread()){
lock.unlock();
}
}
};

Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);

thread1.start(); // thread1将会获得锁
thread2.start(); // thread2将会进入等待
Thread.sleep(500);
thread2.interrupt(); // thread2响应中断
}

Timeout(超时)
超时机制也是解决锁无限阻塞线程一种方式,该功能的使用比较简单,可以通过Lock的tryLock()方法和tryLock(long timeout, TimeUtil timeUtil)方法实现,看方法签名就可以了解传入时长,设置时间单位,tryLock()方法在规定时间过后会放弃锁竞争。

ReentrantLock构造函数

1
2
3
4
5
6
7
public ReentrantLock() {
this.sync = new ReentrantLock.NonfairSync();
}

public ReentrantLock(boolean var1) {
this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}

根据方法逻辑可以看出,构造函数的boolean参数传入,若为true则是使用公平锁,默认为非公平锁。

  • 公平锁
    • 根据等待时长分配锁权限
      • 公平锁会平均分配时间片,会降低系统吞吐量,但能保证线程被顺序执行
  • 非公平锁
    • 随机分配锁权限

ReetrantLock(重入锁)

重入锁是Java并发工具包锁接口的一个具体实现,之所以叫重入锁,是指该锁可以被一个线程重复多次获取。当然,获取了多少次,就要释放多少次,释放次数少了,将无法离开锁边界,其它线程也无法得到该锁。释放次数多了,则会抛出IllegalMonitorStateException

简单实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ReentrantLockExample{
private static Lock lock = new ReentrantLock();
static int count;

public static void main(String[] args){
ReentrantLockExample example = new ReentrantLockExample();

Runnable runnable = () -> {
example.executeThings();
};

Thread testThread = new Thread(runnable);
testThread.start();
}

void executeThings(){
lock.lock();
count++;
if(count == 10){
lock.unlock();
}else{
executeThings();
lock.unlock();
}
}
}

按照该实践逻辑,该实例线程递归地执行了executeThings()方法,因此多次的获取了了锁,在达到了执行边界后,又递归的多次释放了锁。若重入锁不能重入,则会在第一次递归时和自己产生死锁,这在执行逻辑上是不允许的,这也是重入锁的意义之一。

Condition

Condition是锁的监视方法,用于替换Object对象的wait(), notify(), notifyAll()方法。

先来看一下Condition接口的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Condition {
void await() throws InterruptedException;

void awaitUninterruptibly();

long awaitNanos(long var1) throws InterruptedException;

boolean await(long var1, TimeUnit var3) throws InterruptedException;

boolean awaitUntil(Date var1) throws InterruptedException;

void signal();

void signalAll();
}

可以看出来,Condition接口的方法都能和Object的线程控制方法一一对应。

简单实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();

public static void main(String[] args){
Runnable workThread = () -> {
lock.lock();
try {
System.out.println("ready to await");
condition.await();
System.out.println("keep running");
} catch (InterruptedException e) {
// 不做任何操作
} finally {
if (lock.isHeldByCurrentThread()){
lock.unlock();
}
}
};

Runnable singleThread = () -> {
// 获取锁 后执行唤醒方法
lock.lock();
condition.signal();
lock.unlock();
};

Thread threadTest0 = new Thread(workThread);
Thread threadTest1 = new Thread(singleThread);

threadTest0.start();
Thread.sleep(3000);
threadTest1.start();
}

/*
* 输出:
* ready to await
* 三秒后
* keep running
*/

ReadWriteLock(读写锁)

一般来说,对于数据的读需求回要比写需求大得多,在操作数据时可能并不一定要针对数据加锁,甚至说读内容根本无需加锁,所以在Java并发工具包中,还提供了读写锁,读写分离,以提高效率。

简单实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock readLock = lock.readLock();
private static Lock writeLock = lock.writeLock();

private static volatile int value;

public static void main(String[] args) throws Exception{
Callable<Integer> readThread = () -> {
readLock.lock();
try{
// 模拟文件读取
Thread.sleep(100);
}catch(InterruptedException e){
// do nothing
}finally {
readLock.unlock();
}
return value;
};

Runnable writeThread = () -> {
writeLock.lock();
try{
value += 2;
}finally {
writeLock.unlock();
}
};
}

总结

Java中的锁,Synchronizedjava.util.concurrent.locks到这里就算是大体谈论完了,synchronized可以渐渐的由Locks替换掉。虽说概念性的东西和简单使用可以通过这篇文字弄明白,但是实际并发开发中会碰到的问题还是太多种多样了,尽量还是结合并发的设计模式,缓存,消息队列,锁等等工具去最大程度的解决并发问题。