为了保证线程安全,使用锁是其中一种方式,所以决定写篇文章巩固一下对锁的理解
锁的种类
1. 内置锁
内置锁的使用方式是使用synchoronized关键字,可以将synchoronized关键字放在方法上面,如果需要更细粒度的控制,可以使用synchoronzied代码块
//使用方式1
synchoronized(object){
//同步代码
object.wait();
object.notify();
object.notifyAll();
}
//使用方式2
public synchronized test1(){
//同步代码
wait();
notify();
notifyAll();
}
//使用方式3
public static synchronized test1(){
//同步代码
}
使用方式2和3,其实本质上也是使用方式1的调用形式,只不过锁监视的对象不同,方式2是监听当前的实例化对象,方式3是监听当前类对象。然后可以在代码块里面,可以使用wait和notify以及notifyAll方法对线程进行控制。
1.1 wait,notify,notifyAll的作用
wait:让当前线程进入阻塞状态,进入锁的等待队列,并且释放当前的锁
notify:随机让一个等待队列中的线程从阻塞状态变为就绪,但是此时并没有释放锁,需要运行到synchronized代码块结束,或者碰到下一个wait方法释放锁。然后和其他线程竞争这个锁(可能有多个线程变为就绪,或者新起线程)。
notifyAll:激活阻塞队列中的所有线程,但是这些线程还是需要竞争锁,因为全都变成就绪态了,如果当前线程退出同步代码块,释放锁了,其他被激活的线程会继续竞争锁。这与notify有所不同,notify只激活了一个线程,即使锁可用了,那些阻塞的线程没有被notify或者notifyAll还是会一直阻塞下去。
1.2 wait是否一定要notify或者notifyAll才能唤醒
首先,明确一点,唤醒和继续执行下去是两回事,因为需要继续执行需要重新获得锁
wait除了被notify和notifyAll唤醒,还有以下几种方式
- 其他线程调用该线程的interrupt方法
- 使用超时的wait方法
- 特殊情况 当使用线程对象作为锁对象时
第三种情况比较有意思,如果以运行中的线程对象当作锁对象,在同步代码块中的wait方法,会在这个线程结束后,自动将当前线程唤醒
贴上我自己发现这个问题的代码
public class WaitTest {
static class ThreadA extends Thread {
public ThreadA(String name){
super(name);
}
@Override
public void run() {
synchronized (this){
System.out.println(Thread.currentThread().getName()+" call notify()");
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadA t1 =new ThreadA("t1");
synchronized (t1){
System.out.println(Thread.currentThread().getName()+" start t1");
t1.start();
System.out.println(Thread.currentThread().getName()+" wait");
t1.wait();
System.out.println(Thread.currentThread().getName()+" continue");
}
}
}
这边t1.wait()理论上来讲 应该阻塞住,但是没有,如果将 t1.start()注释掉,就能被阻塞住
经过查询资料,真相就是。。。在Thread的join源码上发现一端解释
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. ==As a thread terminates the
* {@code this.notifyAll} method is invoked==. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
意思就是线程结束,会自动调用这个线程对象的notifyall,所以在线程结束之前把它作为锁对象,会自动把wait唤醒
2. 显式锁
显式锁主要用到JUC包中的ReentrantLock,与内置锁不同,这边的锁的获取和释放需要显式使用代码
//使用方式
public void test(){
ReentrantLock lock = new ReentrantLock();
Condition condition =reentrantLock.newCondition();
lock.lock();
try{
//同步代码
condition.await();
condition.signal();
condition.signalAll();
}finally{
lock.unlock();
}
}
ReentrantLock提供了Condition对象,里面的await,signal和signalAll对应隐式锁中的wait,notify和notifyAll
2.1 condition这个设计的好处
在内置锁中,使用wait方法之后,会把当前线程加入到对应锁的等待队列中,但是只有一个提交队列,如果2个线程通过这种形式
while(not conditionA){
wait();
}
等待不同的2个条件,而第三个线程执行了notify,它的目的是唤醒conditionA下的等待,由于norify是随机选择一个,就有几率把这个唤醒丢失,导致程序出错,当然这时可以使用notifyAll来避免。但是肯定会带来一定多余的上下文切换。
而condition的设计,可以让我们在一个锁上创建多个等待队列,激活这个等待队列中的线程,不会影响其他等待队列中的线程。并且condition会继承lock的一些特性,比如公平/非公平。
2.2 公平/非公平模式
ReentrantLock支持2种模式,公平和非公平,默认实现为非公平。具体不同之处是,在公平模式下,锁的获得,是按照lock先后顺序的。而非公平模式,被激活的下一个线程会与新加入的线程产生竞争。
3.两种锁的区别
1.是否可中断
2.尝试性获取锁
3.锁的释放方式
4.公平/非公平模式
5.condition精确唤醒
我的公众号
希望大家关注下我的公众号,我保证自己每天都能更新,为了我的粉丝哈
image
网友评论