并发控制 - 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
|
synchronized(obj){ globalVariable = ... }
synchronized method(Object param){ globalVariable = ... }
static synchronized method(Object param){ globalVariable = ... }
|
Tip使用synchronized
关键字施加的同步块会在同步块内抛出异常后自动释放锁,而Lock不会,需要调用lock.unlock();
才会释放锁
Synchronized原理
查看Synchronized部分编译后的字节码文件可以发现有两个指令,mointorenter
和mointorexit
指令,可以了解到Jvm种的同步机制都是基于Mointor对象实现的,但要注意,同步方法并不是由 monitorenter
和 monitorexit
指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 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;
boolean tryLock();
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(); thread2.start(); Thread.sleep(500); thread2.interrupt(); }
|
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(); }
|
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){ }finally { readLock.unlock(); } return value; };
Runnable writeThread = () -> { writeLock.lock(); try{ value += 2; }finally { writeLock.unlock(); } }; }
|
总结
Java中的锁,Synchronized
和java.util.concurrent.locks
到这里就算是大体谈论完了,synchronized
可以渐渐的由Locks
替换掉。虽说概念性的东西和简单使用可以通过这篇文字弄明白,但是实际并发开发中会碰到的问题还是太多种多样了,尽量还是结合并发的设计模式,缓存,消息队列,锁等等工具去最大程度的解决并发问题。