Java的锁

一、锁的使用
Java的多线程环境下,由于共享资源的竞态可能导致错误的结果。因此锁的使用就是保护临界代码,这个就不再赘述。
代码: 没有锁的时候,临界区没有保护,可能出现错误的结果(出错概率跟你的并发程度有关,小概率事件),尽管代码比较简单,可以通过其他手段来规避多线程问题,这里还是以这个代码为例,仅仅用来说明多线程问题

 

package top.huster.thread.lock;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by longan.rtb on 2018/11/6.
 * lock test
 */
public class LockTestThread {
    private static int count = 0;
    private static List threadList = new ArrayList<>();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i< 10; i++) { Thread thread = new Thread( () -> {
                count++;
            }, "thread - " + i);
            threadList.add(thread);
        }
        for (Thread thread : threadList) {
            thread.start();
            thread.join();
        }
        System.out.println("count is " + count);
    }
}

二、同步代码块和lock
要解决这种问题,就得在临界值的时候,加上保护。通常做法有使用synchronize代码块和lock两种方式。
代码: 使用synchronize代码块
package top.huster.thread.lock;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by longan.rtb on 2018/11/6.
 * lock test
 */
public class LockTestThread {
    private static int count = 0;
    private static Object object = new Object();
    private static List threadList = new ArrayList<>();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i< 10; i++) { Thread thread = new Thread( () -> {
                synchronized (object) {
                    count++;
                }
            }, "thread - " + i);
            threadList.add(thread);
        }
        for (Thread thread : threadList) {
            thread.start();
            thread.join();
        }
        System.out.println("count is " + count);
    }
}

问题是解决了,但是我们知道在java里还有锁的方式。可能你会问,为什么有了同步代码块,还需要有锁呢?那是因为同步代码块还不够灵活,很多时候用起来远远达不到lock的灵活性。举例说,synchronize在出了代码块/方法区以后,就自动解锁了,而lock我可以选择任意我想要的时候解锁。另外lock还提供了读写锁能够适应更多的使用场景。
另外一方面,由于lock使用的是CAS实现,所以具有更好的性能。

三、我们不妨来简单的来实现一个自己的锁

代码: 重点只关注lock和unlock部分

package top.huster.thread.lock;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * Created by longan.rtb on 2018/11/6.
 * 基于object上的通信手段实现多线程的锁
 */
public class SimpleLock implements Lock {

    private static volatile Boolean hasLocked = Boolean.FALSE;

    private final static Object object = new Object();


    public void lock() {
            while (hasLocked) {
                try {
                    synchronized (object) {
                        object.wait();
                    }
                } catch (InterruptedException e) {
                }
            }
            hasLocked = true;
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock() {
        return false;
    }

    public boolean tryLock(long timeout, TimeUnit unit) {
        return false;
    }

    public void unlock() {
        //while throw IllegalMonitorStateException if not synchronized
        synchronized (object) {
            object.notify();
        }
        hasLocked = false;
    }

    public Condition newCondition() {
        return new Condition() {
            @Override
            public void await() throws InterruptedException {

            }

            @Override
            public void awaitUninterruptibly() {

            }

            @Override
            public long awaitNanos(long nanosTimeout) throws InterruptedException {
                return 0;
            }

            @Override
            public boolean await(long time, TimeUnit unit) throws InterruptedException {
                return false;
            }

            @Override
            public boolean awaitUntil(Date deadline) throws InterruptedException {
                return false;
            }

            @Override
            public void signal() {

            }

            @Override
            public void signalAll() {

            }
        };
    }

}

大体思路是使用一个标识为锁有没有被别人拿走,如果拿走了就wait,直到有锁的线程释放锁,再通知线程醒来继续。那么要注意的是什么呢?如果一个线程在lock的时候,再次调用了lock(比如递归函数)的时候,整个程序就卡死了。因为这个时候的标志位已经是true表示有人拿到了锁,而这人就是他自身。他就只能永久得等待下去,产生了死锁,为了解决这个问题,就是下一章节。
四、可重入锁。可重入锁意思就是,如果获取锁的线程就是自身,那他可以继续获取这个锁。在Java的并发包里可重入锁的实现ReentrantLock,我们来看看他的实现。
重点代码:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

解释下,也是用了一个标志位,不过他用的是一个long型变量,使用的是CAS,当他为0的时候,代表这个锁还未被使用,所以将这个标志位改成1代表被我使用了,并且记下占用的线程。

接着,如果是已经被占用了,他也没直接结束,而是尝试去做以下操作

注意其中的红框框的部分,为什么重复把上面的代码写了一遍呢? 这是因为当你判断了c不等于0(一种情况是自己占用了这把锁,一种情况是别人占用了这把锁),试着再去acquire的时候,这个时候恰好另外一个线程已经释放了锁,所以这里他又判断了一次,思维很缜密。
整理大概做的事情就是,判断当前占用这个锁的线程是不是自己,如果是自己则把计数加1,如果不是呢? 我们看如下代码

可以看到如果不是则把当前的线程生成一个node,然后加入到队列的末尾。这个队列就是所有等待这把锁的线程。加入队列之后,最终调用的是unsafe这个类里的park阻塞了当前抢锁的线程

至此,我们就实现了整个的加锁过程,并且是可重入的。
那么unlock的过程是怎样的,且看如下代码

这个过程依然会判断当前的线程是否是持有锁的线程,如果是的话,将计数器-1,如果这个时候是最后一层锁,那么直接将当前持锁线程清空,这个时候state=0代表没有加锁。如果这个时候state还未等于0,则说明这个线程多次进行了lock,则将计数器-1,等待着下次的unlock。

如果全部解完,在这时候,他调用了unsafe的unpark,唤醒了队尾的线程。

六、上面的示例是一个非公平锁的实现,我们可以在ReentrantLock的构造函数里看到,fair的参数,也可以看到fair和unfair的实现。那么这个是什么意思呢? 有什么用呢?
如果一把锁是非公平的话,那么会随机的选择一个线程来执行,这就会导致线程饥饿的问题。有的线程运气特别差,总是轮不到他拿到cpu资源,所以最后他就饿死了。那么公平锁是怎么做的呢?他严格的按照FIFO的原则,根据时间的先后顺序来选择执行的线程。
他的实现和unfair的实现仅仅是lock的时候有所不同。

注意红色部分就是和非公平锁的不同之处。他会判断当前线程的先前节点还有没有,如果还有说明自己不是队头的那个,他会把自己加入队尾,阻塞,并且让给他的前面的一个节点。我们想象下,如果这个过程不断的迭代,他的前面,前面的前面总不是头,经过几次迭代终于找到了头。那么这个线程做的事情,就比unfair的要多得多,所以公平锁的性能比非公平锁要差,在使用的时候要特别注意。

不过这里我一直有个问题没有想明白,那就是unlock的时候,唤醒的是队头的节点,那么下一次抢锁的一定是头节点,为什么还有公平和非公平之说呢?现在我突然想明白了,有可能头结点去抢锁的时候,别的新进来的线程也在抢锁。如果没有公平性,这个时候新进来的线程,可能可以立刻获得锁,这个就是非公平的。而公平的锁,则会判断当前的节点是否是头结点,发现这个节点根本不是头结点,他只能都后面去排队了。这个设计很巧妙。

七、读写锁。为了更进一步提升性能。在一个读多写少的系统,我们可以把读写分开加锁,这样读的时候,根本不需要抢锁(不需要抢读锁),岂不是性能更高?
读的时候,需要获得写锁,保证读到的都是最新的值,但是不需要读锁。因为写比较少,所以性能高。
写的时候,需要读锁和写锁,保证没有别的线程读到旧值,并且保证只有一个线程同时写入值。
java的读写锁实现,ReentrantReadWriteLock 使用起来也比较简单,不需要赘述,不过他的实现还是蛮巧妙的。他的实现其实跟上面提到的差不多,不过他是使用的一个32位的数来记录锁的情况。

八、接近尾声。再说一说Condition对象。锁的接口里有newCondition方法,他会产出一个Condition对象,用于线程间的通信。他也有wait、notify和notifyAll方法。我们的Object上也有这三个方法。不同的是,Object上调用wait的时候,必须加锁,用synchronize修饰,而lock上的condition则不需要,所以他用起来更方便。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注