概述
锁这个概念众所周知,在使用线程中经常会用到,但是为什么需要锁呢?
在回答这个问题之前需要知道什么是线程安全?
线程安全的核心概念是正确性,某个类的行为与其规范一致。多个线程可能通过不同的方法改变对象的状态,需要保证对象处于合法的状态。
为了保证线程安全,我们有几种方式,一种是使用不变性,一种是使用锁同步,一种是限制(比如只是单线程访问)。
从上面分析可以看出锁是在保证线程安全的时候有时使用,换句话说是锁只是保证线程安全的一种方式,锁保证线程安全是保证了从一个合法状态到另一个合法状态,也就是保证了原子性和可见性。
原子性:指令必须有不可分割的特性。
可见性:一个线程的效果对另一个线程是可见的。这里的效果是指写入成员变量的值对于另一个成员变量的读出的操作可见的。
内置锁
Java内置锁使用独占技术来实现。java中synchronized
关键字作为内置锁的申请和释放,在进入synchronized
方法或者代码块的时候获取锁,而退出时候释放锁,即使是异常退出时候也会退出锁。不会忘记释放锁。
对象与锁
- 每个Object对象都会拥有一把锁,而int和float等基本类型都不是Object,基本类型只能通过包含它们的对象锁住。
- 锁只能使用在成员变量的方法中应用。
synchronized
不属于方法签名的一部分,当子类覆盖了父类的方法是,synchronized
不会被继承。
synchronized
方法
synchronized void f(){
/**
* body
*/
}
synchronized
代码块
void f(){
synchronized(this) {
/**
* body
*/
}
}
类锁
锁住一个对象并不意味着不可访问对象或者父类的静态数据,可以通过synchronized static
方法或者代码块来对静态数据进行保护。
synchronized
静态方法
synchronized static void f(){
/**
* body
*/
}
synchronized
静态代码块
void f(){
synchronized(C.class) {
/**
* body
*/
}
}
内置锁的局限
- 无法中断一个正在等候获得锁的线程。
- 没办法定时的获取锁,一旦线程等待获取锁,就会一直等待,没办法通过时间来限制等待时间。
- 内置锁要求申请和释放锁在同一代码块中,而对于申请和释放锁不在同一代码块中内置锁无能为力。
显示锁
锁结构
从结构中可以看出存在两类型的锁接口,一个Lock
,一个ReadWriteLock
。
Lock
用于实现更广泛的锁操作,读写锁,排它锁等等
ReadWriteLock
用于维持一组相关的锁,读锁和写锁。
ReentrantLock
ReentrantLock
使用独占技术实现。ReentrantLock
有以下特点:
- 可定时的,一段时间内获取不到锁抛出异常。
- 提供了轮询的获取锁。
- 可中断的锁获取操作,使用
lockInterruptibly()
获取锁,一旦线程中断,抛出InterruptedException
。 - 非块机构的加锁,获取锁与释放可以不在同一个代码块中,可以不在同一个方法中。
- 提供了公平性选择,一旦设置了公平性,那获取锁的顺序将根据请求的顺序一致,不会出现插队现象。
常见的使用代码结构:
Lock lock = new ReentrantLock();
...
lock.lock();
try {
/**
* 更新对象状态
*/
} finally {
lock.unlock();
}
注意:一定要关注lock被释放,避免因异常导致锁无法释放。
ReentrantLock
与synchronized
之间的选择在一些内置锁无法满足需要的情况下,
ReentrantLock
可以作为一种高级工具,当需要一些高级功能时才应该使用ReentrantLock
,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则应该使用synchronized
。
读写锁
ReentrantLock
使用独占技术实现了标准的互斥锁,解决了“写-写”冲突,“读-写”冲突,同时也避免了“读-读”冲突。但很多情况下,是允许多个读并发的,为了满足这个需求,提高并发的性能,读写锁孕育而生。读写锁满足了“写-写”,”读-写”冲突,但避免了“读-读”冲突。常用的读写锁为ReentrantReadWriteLock
。
其他锁工具
闭锁(Latch)
闭锁是一种同步工具类,可以延迟线程的进度直到其到达结束状态。闭锁相当于一扇门:在闭锁到达结束之前这扇门一致是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门打开允许所有的线程通过。当闭锁到达结束状态后,将不能修改闭锁的状态。可以用于初始化等操作。
CountDownLatch是一种灵活的闭锁实现。
Latch示意图
信号量(Semaphore)
信号量更确切的说是计数信号量,用于控制直接访问的特定资源的操作数量。可以用于实现资源池或者对容器添加边界。当然也可以使用信号量来设计互斥锁,只需数量设置为1。
栅栏(Barrier)
栅栏类似于闭锁,它能阻塞一组线程知道某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
参考文档
- 《Java并发编程实战》
- 《Java并发编程-设计原则与模式》