参考文章
线程状态
了解线程锁之前先了解一下线程状态。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
新建状态(NEW):
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。
就绪状态(RUNNABLE):
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
运行状态(RUNNING):
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞状态(BLOCKED):
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu ,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu 转到运行(running)状态。阻塞的情况分三种:
- 等待阻塞(o.wait->等待对列):运行(running)的线程执行 o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞(sleep/join):运行(running)的线程执行 Thread.sleep(long ms)或t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
备注:wait 释放锁,释放CUP,sleep 不释放锁,释放CUP
线程死亡(DEAD):
线程会以下面三种方式结束,结束后就是死亡状态。
- 正常结束:run()或 call()方法执行完成,线程正常结束。
- 异常结束:线程抛出一个未捕获的 Exception 或 Error。
- 调用 stop: 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
锁的分类
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,sql中是排它锁。
AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
while(true){
if("获取锁"){
break;
}
}
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
自旋锁时间阈值(1.6 引入了适应性自旋锁)
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞。
自旋锁的开启
JDK1.6 中-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;
公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁(Nonfair)
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。
不可重入锁
判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待
可重入锁
不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。
public class Test{
Lock lock = new Lock();
public void methodA(){
lock.lock();
...........;
methodB();
...........;
lock.unlock();
}
public void methodB(){
lock.lock();
...........;
lock.unlock();
}
}
如果是不可重入锁,上面的方法死锁,如果是可重入锁这没有问题。
锁的实现
Synchronized
synchronized 它可以把任意一个非 NULL 的对象当作锁。他是悲观锁,同步锁,非公平锁,可重入锁。
Synchronized 作用范围
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,会锁所有调用该方法的线程;
- 当作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,
当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。 - 当作用于类时,会锁所有调用该方法的线程;
作用于方法
public synchronized void method()
{
// todo
}
作用于静态方法
public synchronized static void method() {
// todo
}
作用于一个对象实例
private Object obj;
public void method1()
{
synchronized(this) {
// todo
}
}
public void method2()
{
synchronized(obj) {
// todo
}
}
作用于类
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
在子类方法中加上synchronized关键字
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { }
}
在子类方法中调用父类的同步方法
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
synchronized锁在发生异常的时候会自动释放锁
ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,是可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
class Printer {
private Lock lock = new ReentrantLock();//默认是非公平锁
public void print(String name) {
lock.lock(); // 获取锁 , 获取不到会阻塞
try {
System.out.println("测试一下ReentrantLock");
} finally {
lock.unlock(); // 释放锁
}
}
}
Lock 构造方法:
- ReentrantLock():非公平锁
- ReentrantLock(boolean fair):fair=true 公平锁,false:非公平锁
Lock 接口的主要方法:
- void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
- boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
- void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
- Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
- getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
- getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
- getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
- hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
- hasQueuedThreads():是否有线程等待此锁
- isFair():该锁是否公平锁
- isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
- isLock():此锁是否有任意线程占用
- lockInterruptibly():如果当前线程未被中断,获取锁
- tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
- tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。
Condition样例: 多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockCondition {
private final Lock lock = new ReentrantLock();//锁对象
private final Condition notFull = lock.newCondition();//写线程条件
private final Condition notEmpty = lock.newCondition();//读线程条件
private final Object[] items = new Object[100];//缓存队列
private int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
if (count == items.length)//如果队列满了
notFull.await();//阻塞写线程
items[putptr++] = x;//赋值
if (putptr == items.length)
putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0
++count;//个数++
notEmpty.signal();//唤醒读线程
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
if (count == 0)//如果队列为空
notEmpty.await();//阻塞读线程
Object x = items[takeptr++];//取值
if (takeptr == items.length)
takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0
--count;//个数--
notFull.signal();//唤醒写线程
return x;
} finally {
lock.unlock();
}
}
}
Condition 类和 Object 类锁方法区别区别
- Condition 类的 awiat 方法和 Object 类的 wait 方法等效
- Condition 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件(如上的读/写线程)的线程,而 object 的唤醒是随机的
ReentrantLock 与 synchronized
两者的共同点
- 都是用来协调多线程对共享对象、变量的访问,都保证了可见性和互斥性
- 都是可重入锁,同一线程可以多次获得同一个锁
两者的不同点
- ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作, synchronized 会 被 JVM 自动加锁,解锁机制
- synchronized 异常自动解锁, ReentrantLock 必须在 finally 控制块中进行解锁操作。
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
- ReentrantLock 相比 synchronized 的优势是可公平锁、多个锁。这种情况下需要使用 ReentrantLock。
加锁流程
核心组件
- Wait Set :等待队列 。哪些调用 wait 方法被阻塞的线程被放置在这里;
- Contention List:竞争队列。所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:参赛队列。 Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
- OnDeck:准备就绪的。任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
- Owner:当前已经获取到所资源的线程被称为 Owner;
- !Owner:当前释放锁的线程。
加锁流程
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList
会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。 - Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
- OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
- 处于 ContentionList、EntryList、WaitSet, OnDeck 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的
- Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁