自己动手写把”锁”---终极篇

锁是整个Java并发包的实现基础,通过学习本系列文章,将对你理解Java并发包的本质有很大的帮助。
 
前边几篇中,我已经把实现锁用到的技术,进行了一一讲述。这其中有原子性、内存模型、LockSupport还有CAS,掌握了这些技术,即使没有本篇,你也完全有能力自己写一把锁出来。但为了本系列的完整性,我在这里还是把最后这一篇补上。
 
先说一下锁的运行流程:多个线程抢占同一把锁,只有一个线程能抢占成功,抢占成功的线程继续执行下边的逻辑,抢占失败的线程进入阻塞等待。抢占成功的线程执行完毕后,释放锁,并从等待的线程中挑一个唤醒,让它继续竞争锁。
 
转变成程序实现:我们首先定一个state变量,state=0表示未被加锁,state=1表示被加锁。多个线程在抢占锁时,竞争将state变量从0修改为1,修改成功的线程则加锁成功。state从0修改为1的过程,这里使用cas操作,以保证只有一个线程加锁成功,同时state需要用volatile修饰,已解决线程可见的问题。加锁成功的线程执行完业务逻辑后,将state从1修改回0,同时从等待的线程中选择一个线程唤醒。所以加锁失败的线程,在加锁失败时需要将自己放到一个集合中,以等待被唤醒。这个集合需要支持多线程并发安全,在这里我通过一个链表来实现,通过CAS操作来实现并发安全。
 
把思路说清楚了,咱们看下代码实现。
 
首先咱们实现一个ThreadList,这是一个链表结合,用来存放等待的处于等待唤醒的线程:

public class ThreadList{
    private volatile Node head = null;
    private static  long headOffset;
    private static Unsafe unsafe;
    static {
        try {
            Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]);
            constructor.setAccessible(true);
            unsafe = constructor.newInstance(new Object[0]);
            headOffset = unsafe.objectFieldOffset(ThreadList.class.getDeclaredField("head"));
        }catch (Exception e){
        }
    }
    /**
     *
     * @param thread
     * @return 是否只有当前一个线程在等待
     */
    public boolean insert(Thread thread){
        Node node = new Node(thread);
        for(;;){
            Node first = getHead();
            node.setNext(first);
            if(unsafe.compareAndSwapObject(this, headOffset,first,node)){
                return first==null?true:false;
            }
        }
    }
    public Thread pop(){
        Node first = null;
        for(;;){
            first = getHead();
            Node next = null;
            if(first!=null){
                next = first.getNext();
            }
            if(unsafe.compareAndSwapObject(this, headOffset,first,next)){
                break;
            }
        }
        return first==null?null:first.getThread();
    }
    private Node getHead(){
        return this.head;
    }
    private static class Node{
        volatile Node next;
        volatile Thread thread;
        public Node(Thread thread){
            this.thread = thread;
        }
        public void setNext(Node next){
            this.next = next;
        }
        public Node getNext(){
            return next;
        }
        public Thread getThread(){
            return this.thread;
        }
    }
}
加锁失败的线程,调用insert方法将自己放入这个集合中,insert方法里将线程封装到Node中,然后使用cas操作将node添加到列表的头部。同样为了线程可见的问题,Node里的thread和next都用volatile修饰。
加锁成功的线程,调用pop方法获得一个线程,进行唤醒,这里边同样使用了cas操作来保证线程安全。
 
接下来在看看锁的实现:
public class MyLock {
    private volatile int state = 0;
    private ThreadList threadList = new ThreadList();
    private static  long stateOffset;
    private static Unsafe unsafe;
    static {
       try {
           Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]);
           constructor.setAccessible(true);
           unsafe = constructor.newInstance(new Object[0]);
           stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state"));
       }catch (Exception e){
       }

    }
    public void lock(){
        if(compareAndSetState(0,1)){
        }else{
            addNodeAndWait();
        }
    }
    public void unLock(){
        compareAndSetState(1,0);
        Thread thread = threadList.pop();
        if(thread != null){
            LockSupport.unpark(thread);
        }
    }
    private void addNodeAndWait(){
        //如果当前只有一个等待线程时,重新获取一下锁,防止永远不被唤醒。
        boolean isOnlyOne = threadList.insert(Thread.currentThread());
        if(isOnlyOne && compareAndSetState(0,1)){
            return;
        }
        LockSupport.park(this);//线程被挂起
        if(compareAndSetState(0,1)){//线程被唤醒后继续竞争锁
            return;
        }else{
            addNodeAndWait();
        }
    }
    private boolean compareAndSetState(int expect,int update){
        return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
    }
}

 

线程调用lock方法进行加锁,cas将state从0修改1,修改成功则加锁成功,lock方法返回,否则调用addNodeAndWait方法将线程加入ThreadList队列,并使用LockSupport将线程挂起。(ThreadList的insert方法,返回一个boolean类型的值,用来处理一个特殊情况的,稍后再说。)
获得锁的线程执行完业务逻辑后,调用unLock方法释放锁,即通过cas操作将state修改回0,同时从ThreadList拿出一个等待线程,调用LockSupport的unpark方法,来将它唤醒。
 
 
将我们在《自己动手写把"锁"---锁的作用》的例子修改为如下,来测试下咱们的锁的效果:
public class TestMyLock {
    private static  List<Integer> list = new ArrayList<>();
    private static MyLock myLock = new MyLock();
    public static void main(String[] args){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10000;i++){
                    add(i);
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                print();
            }
        });
        t1.start();
        t2.start();
    }
    private static void add(int i){
        myLock.lock();
        list.add(i);
        myLock.unLock();
    }
    private static void print(){
        myLock.lock();
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
        myLock.unLock();
    }
}
ok,正常运行了,不在报错。
 
到这里咱们的一个简单地锁已经实现了。接下来我再把上边的,一个没讲的细节说一下。即如下这段代码:

boolean isOnlyOne = threadList.insert(Thread.currentThread());
        if(isOnlyOne && compareAndSetState(0,1)){
            return;
        }
ThreadList的insert方法,在插入成功后,会判断当前链表中是否只有自己一个线程在等待,如果是则返回true。从而进入后边的if语句。这个逻辑的用意就是:如果只有自己一个线程在等待时,则试着通过cas操作重新获取锁,如果获取失败才进入阻塞等待。它是用来解决以下边界情况:

在只有线程A和线程B两个线程的时候,如果没有以上判断逻辑,线程B将有可能会永远处于阻塞不被唤醒。 

 

以下是本系列其他的文章:

自己动手写把”锁”之---锁的作用

自己动手写把”锁”之---JMM和volatile

自己动手写把”锁”---原子性操作

自己动手写把”锁”---LockSupport深入浅出

 

-------------------------------------------------

有兴趣的朋友,可以加入我的知识圈,一起研究讨论。

我正在「JAVA互联网技术」和朋友们讨论有趣的话题,你一起来吧?

https://t.zsxq.com/EUn6IIE

 

posted @ 2018-01-12 08:26  清泉^_^  阅读(1556)  评论(2编辑  收藏  举报