Various locks

1, listen to the story of the knowledge mastered

In a village there, there is a well of water, water quality is very good, the villagers want to dig a well in the water. This is only a well, the village of so many people, so come to a draw with the rules of the job. Mayor brains, and ultimately came up with a reasonable plan, let's take a closer look at the wisdom and understanding of adult village.

The well to arrange a person to see well, fetch water to maintain order.

When fetching water, the family unit, the family to which anyone first to the well, you can hit the water, but if a family accounted kick right to come kick his family at this time do not line up. Those who did not seize the right to draw water, one by one, lined up next to the well, the top surface of the first arrival. Kick diagram below:

Schematic kick

Is not feeling very harmonious, if the kick had finished, he would look well with people reporting, people will see the well to fetch water and then a second person. So we are always able to hit the water. It is not seem quite fair, first man hit the water, of course, not absolutely fair, and see for yourself the following scenario:

Kick with his family

Watching a baby's father was fetching water, he also went to the well of the baby, so the women with the father directly into your front kick, the envy of others. 
The story above model is known as a fair lock model, when a person think of the well to fetch water, and now people who are not their own side kick, this time you have to obediently line up in the back of the queue.

Things are not always so easy, there is always some people want to take shortcuts, people saying well look older, sometimes, eyesight is not very good, this time, people began playing a new idea. New to kick people when they see people queuing to fetch water, they would not be so well-behaved pushed to the bottom to line up, on the contrary, they will now have to see that no one is to draw water, if someone fetch water , crash others, had routed queue in the back, but if this time fetching water in front of people just kick down the water, is the transfer, the team ranked in the human head is not yet complete handover, this time, new people can try to grab the right to draw water, if to grab, Oh, other people can only open one eye closed, because we are the default rule. This is called unfair lock model. The new man is not necessarily gotta obediently line up, which also resulted in the original queue, the queue of people may have to wait a long, long time. 

reentrant lock java implementation details -ReentrantLock

ReentrantLock supports two ways to acquire the lock, one is a fair model of a non-equity model. Before continuing, we first convert story elements into program elements.

Element conversion 

First is that we lock fair model:

初始化时, state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,如下所示:

A thread acquires the lock

线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后来了村民B也想打水(线程B请求锁),线程B无法获取锁,生成节点进行排队,如下图所示:

Thread B waits

初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁,就相当于是打水期间,同一家人也来打水了,是有特权的,这时候的状态如下图所示:

Reentrant lock acquisition

 

此处可能有人会问 在代码里边怎么理解这种可重入锁的形态呢?

Copy the code
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;
    public void run() 
  {
        for (int j = 0;j<100000;j++) 
     {
            lock.lock();
            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
                lock.unlock();
            }
        }
    }
Copy the code

 

为什么需要使用可重入锁 在故事描述完后进行具体说明;

 

到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁,就成这样了:

A thread releases a lock

仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。 

非公平锁模型

如果你已经明白了前面讲的公平锁模型,那么非公平锁模型也就非常容易理解了。当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续乖乖休眠了。这里就不再画图说明了。 

其它知识点

java5中添加了一个并发包, java.util.concurrent,里面提供了各种并发的工具类,通过此工具包,可以在java当中实现功能非常强大的多线程并发操作。对于每个java攻城狮,我觉得非常有必要了解这个包的功能。虽然做不到一步到位,但慢慢虚心学习,沉下心来,总能慢慢领悟到java多线程编程的精华。 

本问故事情节转载自其他博客,原文地址:https://blog.csdn.net/yanyan19880509/article/details/52345422/

 

2、为什么使用可重入锁?

  ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”。

ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现锁的关键)来实现锁的获取与释放。

其可以完全替代 synchronized 关键字。JDK 5.0 早期版本,其性能远好于 synchronized,但 JDK 6.0 开始,JDK 对 synchronized 做了大量的优化,使得两者差距并不大。

“独占”,就是在同一时刻只能有一个线程获取到锁,而其它获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

“可重入”,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

该锁还支持获取锁时的公平和非公平性选择。“公平”是指“不同的线程获取锁的机制是公平的”,而“不公平”是指“不同的线程获取锁的机制是非公平的”。

2、1 中断响应(lockInterruptibly)

  对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,获得这把锁继续执行,或者线程就保持等待。

而使用重入锁,提供了另一种可能,这就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的需求。

下面的例子中,产生了死锁,但得益于锁中断,最终解决了这个死锁:

Copy the code
public class IntLock implements Runnable{
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    /**
     * 控制加锁顺序,产生死锁
     */
    public IntLock(int lock) {
        this.lock = lock;
    }
    public void run() {
        try {
            if (lock == 1) {
                lock1.lockInterruptibly(); // 如果当前线程未被 中断,则获取锁。
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");
            } else {
                lock2.lockInterruptibly();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 查询当前线程是否保持此锁。
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getName() + ",退出。");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        IntLock intLock1 = new IntLock(1);
        IntLock intLock2 = new IntLock(2);
        Thread thread1 = new Thread(intLock1, "线程1");
        Thread thread2 = new Thread(intLock2, "线程2");
        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        thread2.interrupt(); // 中断线程2
    }
}
Copy the code

上述例子中,线程 thread1 和 thread2 启动后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。这便形成 thread1 和 thread2 之间的相互等待。

代码 56 行,main 线程处于休眠(sleep)状态,两线程此时处于死锁的状态,代码 57 行 thread2 被中断(interrupt),故 thread2 会放弃对 lock1 的申请,同时释放已获得的 lock2。这个操作导致 thread1 顺利获得 lock2,从而继续执行下去。

执行代码,输出如下:

 

2、2锁申请等待限时(tryLock)

  除了等待外部通知(中断操作 interrupt )之外,限时等待也可以做到避免死锁。

  通常,无法判断为什么一个线程迟迟拿不到锁。也许是因为产生了死锁,也许是产生了饥饿。但如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。可以使用 tryLock() 方法进行一次限时的等待。

 

Copy the code
public class TimeLock implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public void run() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6 * 1000);
            }else {
                System.out.println(Thread.currentThread().getName()+" get Lock Failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            // 查询当前线程是否保持此锁。
            if (lock.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName()+" release lock");
                lock.unlock();
            }
        }
    }
    /**
     * 在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法再5秒的等待时间内获得锁,因此请求锁会失败。
     */
    public static void main(String[] args) {
        TimeLock timeLock = new TimeLock();
        Thread t1 = new Thread(timeLock, "线程1");
        Thread t2 = new Thread(timeLock, "线程2");
        t1.start();
        t2.start();
    }
}
Copy the code

  上述例子中,由于占用锁的线程会持有锁长达 6 秒,故另一个线程无法在 5 秒的等待时间内获得锁,因此,请求锁失败。

  ReentrantLock.tryLock()方法也可以不带参数直接运行。这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁成功,立即返回 true。否则,申请失败,立即返回 false,当前线程不会进行等待。这种模式不会引起线程等待,因此也不会产生死锁。

2、3 公平锁

  ·默认情况下,锁的申请都是非公平的。也就是说,如果线程 1 与线程 2,都申请获得锁 A,那么谁获得锁不是一定的,是由系统在等待队列中随机挑选的。这就好比,买票的人不排队,售票姐姐只能随机挑一个人卖给他,这显然是不公平的。而公平锁,它会按照时间的先后顺序,保证先到先得。公平锁的特点是:不会产生饥饿现象。

  重入锁允许对其公平性进行设置。构造函数如下:

public ReentrantLock(boolean fair)
Copy the code
public class FairLock implements Runnable{
    public static ReentrantLock fairLock = new ReentrantLock(true);

    public void run() {
        while (true) {
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName()+",获得锁!");
            }finally {
                fairLock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        FairLock fairLock = new FairLock();
        Thread t1 = new Thread(fairLock, "线程1");
        Thread t2 = new Thread(fairLock, "线程2");
        t1.start();t2.start();
    }
}
Copy the code

测试结果:

  1.当参数设置为 true 时:线程1 和 线程2 交替进行 公平竞争 交替打印

Copy the code
线程1,获得锁!
线程2,获得锁!
线程1,获得锁!
线程2,获得锁!
线程1,获得锁!
线程2,获得锁!
线程1,获得锁!
线程2,获得锁!
线程1,获得锁!
线程2,获得锁!
线程1,获得锁!
线程2,获得锁!
线程1,获得锁!
线程2,获得锁!
线程1,获得锁!
Copy the code

 

  2.当参数设置为 false 时: 此时可以看到线程1 可以持续拿到锁 等线程1 执行完后 线程2 才可以拿到线程 然后多次执行 ; 这就是使用 可重入锁后 是非公平机制 线程可以优先多次拿到执行权

Copy the code
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程1,获得锁!
线程2,获得锁!
线程2,获得锁!
线程2,获得锁!
线程2,获得锁!
Copy the code

 

  修改重入锁是否公平,观察输出结果,如果公平,输出结果始终为两个线程交替的获得锁,如果是非公平,输出结果为一个线程占用锁很长时间,然后才会释放锁,另个线程才能执行。

  引出第二个问题:为什么公平锁例子中出现,公平锁线程是不断切换的,而非公平锁出现同一线程连续获取锁的情况?

   何为重进入(重入)?

  重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题:

  • 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  • 锁的最终释放。线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已经成功释放。

以非公平锁源码分析:

Copy the code
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
Copy the code

  acquireQueued 方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程,来决定获取操作是否成功,如果获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,也就是要求 ReentrantLock 在释放同步状态时减少同步状态值,释放锁源码如下:

Copy the code
public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
Copy the code

  如果锁被获取 n 次,那么前 (n-1) 次 tryRelease(int releases) 方法必须返回 false,只有同步状态完全释放了,才能返回 true。该方法将同步状态是否为 0 作为最终释放的条件,当同步状态为 0 时,将占有线程设置为 null,并返回 true,表示释放成功。

通过对获取与释放的分析,就可以解释,以上两个例子中出现的两个问题:为什么 ReentrantLock 锁能够支持一个线程对资源的重复加锁?为什么公平锁例子中出现,公平锁线程是不断切换的,而非公平锁出现同一线程连续获取锁的情况?

  • 为什么支持重复加锁?因为源码中用变量 c 来保存当前锁被获取了多少次,故在释放时,对 c 变量进行减操作,只有 c 变量为 0 时,才算锁的最终释放。所以可以 lock 多次,同时 unlock 也必须与 lock 同样的次数。
  • 为什么非公平锁出现同一线程连续获取锁的情况?tryAcquire 方法中增加了再次获取同步状态的处理逻辑;

    小结

    对上面ReentrantLock的几个重要方法整理如下:

    • lock():获得锁,如果锁被占用,进入等待。
    • lockInterruptibly():获得锁,但优先响应中断。
    • tryLock():尝试获得锁,如果成功,立即放回 true,反之失败返回 false。该方法不会进行等待,立即返回。
    • tryLock(long time, TimeUnit unit):在给定的时间内尝试获得锁。
    • unLock():释放锁。

    对于其实现原理,下篇博文将详细分析,其主要包含三个要素:

    • 原子状态:原子状态有 CAS(compareAndSetState) 操作来存储当前锁的状态,判断锁是否有其他线程持有。
    • Waiting queue: All requests not to lock the thread goes into the waiting queue waiting. After there is a thread releases the lock, the system will be able to wake up a thread from the waiting queue, to continue working. See: queue synchronizer --AQS (to be updated)
    • Blocking primitives park () and unpark (), used to suspend and resume threads. Not lock the thread will be suspended. About blocking primitives, see: thread blocking tools --LockSupport (to be updated).

Guess you like

Origin www.cnblogs.com/albertzhangyu/p/11889613.html