可重入锁,不可重入锁; 乐观锁,悲观锁;公平锁,非公平锁;显示锁,隐式锁
https://blog.youkuaiyun.com/xiaoleizhanghahaha/article/details/79238152 乐观悲观锁
0716,2018
一 synchronized , reentrantlock ,CountDownLatch
--------synchronized
1.
把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。
一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,如下面的代码所示,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized
块所进行的更新,当进入由同一监控器(lock)保护的另一个 synchronized
块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于 volatile
变量上。
synchronized (lockObject) {
// update object state
}
实现同步操作需要考虑安全更新多个共享变量所需的一切,不能有争用条件,不能破坏数据(假设同步的边界位置正确),而且要保证正确同步的其他线程可以看到这些变量的最新值。 ---ACID 原子,持久,一致,隔离;
2.
对比锁的多种实现,除了不能违背ACID原则,衔接底层以及JVM,遵从"java一处编译,处处运行"的基本原则以外;剩下就是多种锁的性能对比了: 锁的性能主要体现在: 切换时间片的效率以及线程本身的伸缩性,即功能性上;还有就是线程生命周期中的相关操作和分配回收内存;
--------reentrantlock
1. java.util.concurrent.lock
中的 Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。
这就为 Lock
的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义(伸缩性更强)。 ReentrantLock
类实现了 Lock
,它拥有与 synchronized
相同的并发性和内存语义,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
2.
reentrantlock 有一个与锁相关的 获取计数器,拥有锁的某个线程再次获得锁,计数器就会 ++ , 这就要求这个线程被释放两次锁之后才能获得真正释放;
Lock lock = new ReentrantLock();
lock.lock();
try {
// update object state
}
finally {
lock.unlock();
}
Lock
和 synchronized 有一点明显的区别 —— lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放! ----如果你不能发现,你的程序就有可能出现死锁,并且难以发现;(当你拥有更好的操作空间的时候,就要思考更多的局限性)
除此之外,与目前的 synchronized 实现相比,争用下的 ReentrantLock
实现更具可伸缩性。(在未来的 JVM 版本中,synchronized 的争用性能很有可能会获得提高。)这意味着当许多线程都在争用同一个锁时,使用 ReentrantLock
的总体开支通常要比 synchronized
少得多。
3. 通过synchronized 和 reentrantlock 在多个线程的压力测试下的吞吐率对比可知:
synchronized 版本在处理任何类型的争用时,表现都相当差,而 Lock
版本在调度的开支上花的时间相当少,从而为更高的吞吐率留下空间,实现了更有效的 CPU 利用。
4. Reentrantlock 的条件变量 ---- 不理解存在的意义
根类 Object
包含某些特殊的方法,用来在线程的 wait()
、 notify()
和 notifyAll()
之间进行通信。这些是高级的并发性特性,许多开发人员从来没有用过它们 —— 这可能是件好事,因为它们相当微妙,很容易使用不当。幸运的是,随着 JDK 5.0 中引入 java.util.concurrent
,开发人员几乎更加没有什么地方需要使用这些方法了。
通知与锁定之间有一个交互 —— 为了在对象上 wait
或 notify
,您必须持有该对象的锁。就像 Lock
是同步的概括一样, Lock
框架包含了对 wait
和 notify
的概括,这个概括叫作 条件(Condition)
。 Lock
对象则充当绑定到这个锁的条件变量的工厂对象,与标准的 wait
和 notify
方法不同,对于指定的 Lock
,可以有不止一个条件变量与它关联。这样就简化了许多并发算法的开发。例如, 条件(Condition)
的 Javadoc 显示了一个有界缓冲区实现的示例,该示例使用了两个条件变量,“not full”和“not empty”,它比每个 lock 只用一个 wait 设置的实现方式可读性要好一些(而且更有效)。 Condition
的方法与 wait
、 notify
和 notifyAll
方法类似,分别命名为 await
、 signal
和 signalAll
,因为它们不能覆盖Object
上的对应方法。
5.
下面是个人完成的一段代码,旨在顺序输出ABC。 eg:ABCABCABC...
即使使用wait() notify() notifyAll() 三函数,也不能够实现顺序输出;需一次锁两个对象并且使用Thread.sleep()来确保线程装填;
虽然我们按顺序 创建线程对象,但是线程变换到可执行状态的顺序并不能控制,除此之外notify() 与 notifyAll() , 两个函数并不能保证释放下一个线程的锁, 所以想要实现按顺序的公平性,需使用 Reentrantlock ,它提供参数设置是否公平;
public void run(){
for(int i = 0 ; i < PRINT_MAX_NUMBER ; i ++ ){
synchronized (selfLocked){
synchronized (nextLocked){
logger.info(strPrinted);
nextLocked.notify();
}
try {
selfLocked.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String args[]) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
new JavaWork02("A",a,b).start();
Thread.sleep(100);
new JavaWork02("B",b,c).start();
Thread.sleep(100);
new JavaWork02("C",c,a).start();
}
ReentrantLock
构造器的一个参数是 boolean 值,它允许您选择想要一个 公平(fair)锁,还是一个 不公平(unfair)锁。
公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许直接获取锁,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。
不公平锁并不一直是高效,有益的。它只用于实现一些特定情况,例如上文的顺序输出; 在现实中,公平保证了锁是非常健壮的锁,有很大的性能成本。要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,应当把公平设置为 false
,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。 ---- 确保公平,浪费性能
通过压力下的吞吐率测试发现公平锁效率远小于不公平锁,基本等于synchronized的吞吐率;
------------------ 对比 两者
1. Synchronized
能做的,它都能做,它拥有与 synchronized
相同的内存和并发性语义,还拥有 synchronized
所没有的特性,在负荷下还拥有更好的性能。
但即使这样,不也不应该完全抛弃Synchronied,毕竟 它在使用的简洁性上是无可厚非的;比如,在使用 synchronized 的时候,不可能忘记释放锁;在退出 synchronized
块时,JVM 会为您做这件事。您很容易忘记用 finally
块释放锁,这对程序非常有害。您的程序能够通过测试,但会在实际工作中出现死锁,那时会很难指出原因(这也是为什么根本不让初级开发人员使用 Lock
的一个好理由。)
java.util.concurrent.lock
中的锁定类是用于高级用户和高级情况的工具 。一般来说,除非您对 Lock
的某个高级特性有明确的需要,或者有明确的证据(而不是仅仅是怀疑)表明在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用 synchronized。
2. 什么时候选择用 ReentrantLock 代替 synchronized
既然如此,我们什么时候才应该使用 ReentrantLock
呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。 ReentrantLock
还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock
“性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
synchronized 很熟悉了,应在下文提供一些Reentrantlock在实际中的示例代码
------- Reentrantlock 示例代码
public class ThreadDomain38
{
private Lock lock = new ReentrantLock();
public void testMethod()
{
try
{
lock.lock();
for (int i = 0; i < 2; i++)
{
System.out.println("ThreadName = " + Thread.currentThread().getName() +
", i = " + i);
}
}
finally
{
lock.unlock();
}
}
}
-------CountDownLatch
1 CountDownLatch 函数介绍 ; 共享锁
/*
给CountDownLathc 共享锁;初始化CountDownLatch
@param count 计数器,用于初始化CountDownLatch
*/
CountDownLatch( int count )
/*
使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
*/
void await()
/*
使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
*/
boolean await(long timeout, TimeUnit unit)
/*
递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
*/
void countDown()
/*
返回当前计数。
*/
long getCount()
/*
返回标识此锁存器及其状态的字符串。
*/
String toString()
在@Override public void run() { } 中 , 使用countDown() 函数对执行完的任务 减一 , 然后最后在主线程上使用await() 函数,旨在衔接多线程和主线程,即在多线程任务执行结束以后,主线程才继续执行; 但这样在多线程中并没有同步互斥机制; 只用来保证多线程计算和主线程召回结果的顺序性;