提升--08---LockSupport淘宝面试题与源码阅读方法论

Posted 高高for 循环

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提升--08---LockSupport淘宝面试题与源码阅读方法论相关的知识,希望对你有一定的参考价值。


LockSupport

背景:

我们会以几个小程序为案例,展开对LockSupport的讲解,在以前我们要阻塞和唤醒某一个具体的线程有很多限制比如:

  1. ​因为wait()方法需要释放锁,所以必须在synchronized中使用,否则会抛出异常IllegalMonitorStateException
  2. notify()方法也必须在synchronized中使用,并且应该指定对象
  3. ​synchronized()、wait()、notify()对象必须一致,一个synchronized()代码块中只能有一个线程调用wait()或notify()

以上诸多限制,体现出了很多的不足,所以LockSupport的好处就体现出来了。

概念;

  • 在JDK1.6中的java.util.concurrent的子包locks中引了LockSupport这个API,LockSupport是一个比较底层的工具类,用来创建锁和其他同步工具类的基本线程阻塞原语。
  • java锁和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通过调用 LockSupport
    .park()和 LockSupport .unpark()的方法,来实现线程的阻塞和唤醒的

案例解析1:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T13_TestLockSupport {
    public static void main(String[] args) {
        //使用lombda表达式创建一个线程t
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
                    //使用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }
                try {
                    //使当前线程t休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
         //启动当前线程t
        t.start();

      
        try {
         //主线程休眠8s
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main线程 8s later !!!");
        
        LockSupport.unpark(t);
    }

}

  1. 从以上的小程序中,我们不难看出LockSupport使用起来的是比较灵灵活的,没有了所谓的限制。我们来分析一下代码的执行过程,首先我们使用lombda表达式创建了线程对象 " t " ,然后通过 " t " 对象调用线程的启动方法start(),然后我们再看线程的内容,
  2. 在for循环中,当 i 的值等于5的时候,我们调用了LockSupport的.park()方法使当前线程阻塞,注意看方法并没有加锁,就默认使当前线程阻塞了,由此可以看出LockSupprt.park()方法并没有加锁的限制。
  3. 同时进行的main主线程在执行 t.start();后,主动休眠了8s,8s后,调用了LockSupport的unpark()方法来唤醒线程 " t "

案例解析2:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T13_TestLockSupport2 {
    public static void main(String[] args) {
        //使用lombda表达式创建一个线程t
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
                    //使用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }
                try {
                    //使当前线程t休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //启动当前线程t
        t.start();
        //唤醒线程t
        LockSupport.unpark(t);
    }
}

  • 我们来分析一下以上小程序,我们只需要在第一个小程序的主线程中,调用LockSupport的unpark()方法,就可以唤醒某个具体的线程,这里我们指定了线程 " t " ,代码运行以后结果显而易见,线程并没有被阻塞,我们成功唤醒了线程 " t " ,在这里还有一点,需要我们来分析一下,在主线程中线程 " t " 调用了start()方法以后,因为紧接着执行了LockSupport的unpark()方法,所以也就是说,在线程 " t "还没有执行还没有被阻塞的时候,已经调用了LockSupport的unpark()方法来唤醒线程 " t " ,之后线程 " t "才启动调用了LockSupport的park()来使线程 " t " 阻塞,但是线程 " t " 并没有被阻塞,

由此可以看出,LockSupport的unpark()方法可以先于LockSupport的park()方法执行。

案例解析3:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T13_TestLockSupport2 {
    public static void main(String[] args) {
        //使用lombda表达式创建一个线程t
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
                    //调用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }
                if(i == 8){
                    //调用LockSupport的park()方法阻塞当前线程t
                    LockSupport.park();
                }

                try {
                    //使当前线程t休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //启动当前线程t
        t.start();
        //唤醒线程t
        LockSupport.unpark(t);
    }

}


我们来分析一下以上小程序,在第二个小程序的基础上又添加了一个if判断,在i等于8的时候再次调用LockSupport的park()方法来使线程 " t " 阻塞, 我们可以看到线程被阻塞了,原因是LockSupport的unpark()方法就像是获得了一个“令牌”,而LockSupport的park()方法就像是在识别“令牌”,当主线程调用了LockSupport.unpark(t)方法也就说明线程 " t " 已经获得了”令牌”,当线程 " t " 再调用LockSupport的park()方法时,线程 " t " 已经有令牌了,这样他就会马上再继续运行,也就不会被阻塞了,但是当i等于8的时候线程 " t " 再次调用了LockSupport的park()方法使线程再次进入阻塞状态,这个时候“令牌”已经被使用作废掉了,也就无法阻塞线程 " t " 了,而且如果主线程处于等待“令牌”状态时,线程 " t " 再次调用了LockSupport的park()方法,那么线程 " t "就会永远阻塞下去,即使调用unpark()方法也无法唤醒了

小结:

  • ​ LockSupport不需要synchornized加锁就可以实现线程的阻塞和唤醒
  • LockSupport.unpartk()可以先于LockSupport.park()执行,并且线程不会阻塞
  • 如果一个线程处于等待状态,连续调用了两次park()方法,就会使该线程永远无法被唤醒

LockSupport中park()和unpark()方法的实现原理

  • park()和unpark()方法的实现是由Unsefa类提供的,而Unsefa类是由C和C++语言完成的,
  • 其实原理也是比较好理解的,它主要通过一个变量作为一个标识,变量值在0,1之间来回切换,当这个变量大于0的时候线程就获得了“令牌”,从这一点我们不难知道,其实park()和unpark()方法就是在改变这个变量的值,来达到线程的阻塞和唤醒的,具体实现不做赘述了。

淘宝面试题 1

需求:

实现一个容器,提供两个方法add、size,写两个线程:

  • 线程1,添加10个元素到容器中
  • 线程2,实时监控元素个数,当个数到5个时,线程2给出提示并结束

小程序1

public class T01_WithoutVolatile {
    List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T01_WithoutVolatile c = new T01_WithoutVolatile();
        new Thread(() -> {
            for(int i=0; i<10; i++) {
                c.add(new Object());
                System.out.println("add " + i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            while(true) {
                if(c.size() == 5) {
                    break;
                }
            }
            System.out.println("t2 结束");
        }, "t2").start();
    }
}


执行流程:

通过内部的list来new一个ArrayList,在自定义的add方法直接调用list的add方法,在自定义的size方法直接调用list的size方法,想法很简单,首先小程序化了这个容器,接下来启动了t1线程,t1线程中做了一个循环,每次循环就添加一个对象,加一个就打印显示一下到第几个了,然后给了1秒的间隔,在t2线程中写了了一个while循环,实时监控着集合中对象数量的变化,如果数量达到5就结束t2线程。

结果分析:

  • 方法并没有按预期的执行,我们注意看t2线程中c.size()这个方法,当对象添加以后,ArraylList的size()方肯定是要更新的,我们分析一下,当t1线程中的size()放法要更新的时候,还没有更新t2线程就读了,这个时候t2线程读到的值就与实际当中加入的值不一致了.
  • 所以得出两结论,第一这个方案没有加同步,第二while(true)中的c.size()方法永远没有检测到,没有检测到的原因是线程与线程之间是不可见的

小程序 2 -----volatile

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;


public class T02_WithVolatile {

    volatile List lists = Collections.synchronizedList(new LinkedList<>());
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T02_WithVolatile c = new T02_WithVolatile();
        new Thread(() -> {
            for(int i=0; i<10; i++) {
                c.add(new Object());
                System.out.println("add " + i);
				// try {
				// 	TimeUnit.SECONDS.sleep(1);
				// } catch (InterruptedException e) {
				// 	e.printStackTrace();
				// }
            }
        }, "t1").start();
        new Thread(() -> {
            while(true) {
                if(c.size() == 5) {
                    break;
                }
            }
            System.out.println("t2 结束");
        }, "t2").start();
    }
}

结果分析:

小程序2是在小程序1的基础上做了一些改动,用volatile修饰了一下List集合,实现线程间信息的传递,但是还是有不足之处,程序还是无法运行成功,而且我们还得出,volatile一定要尽量去修饰普通的值,不要去修饰引用值,因为volatile修饰引用类型,这个引用对象指向的是另外一个new出来的对象对象,如果这个对象里边的成员变量的值改变了,是无法观察到的,所以小程序2也是不理想的。

volatile修饰引用类型,是同步地址值,如果这个对象里边的成员变量的值改变了,是无法观察到的

另外:

  • 如果把上代码的注释的休眠语句打开,程序可以达到需求,线程t2能够监控到数据size的变化.

分析:

  • 操作系统在他线程t1休眠的时候,有主动跟新内存的值,并及时通知线程t2进行同步

小程序 3 -----wait notify

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class T03_NotifyHoldingLock { //wait notify
    //添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T03_NotifyHoldingLock c = new T03_NotifyHoldingLock();
        final Object lock = new Object();
        //需要注意先启动t2再启动t1
        new Thread(() -> {
            synchronized(lock) {
                System.out.println("t2 启动");
                if(c.size() != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2 结束");
            }
        }, "t2").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        new Thread(() -> {
            System.out.println("t1 启动");
            synchronized(lock) {
                for(int i=0; i<10; i++) {
                    c.add(new Object());
                    System.out.println("add " + i);

                    if(c.size() == 5) {
                        lock.notify();
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();
    }
}


执行流程:

小程序3用了锁的方式(利用wait()和notify()),通过给object对象枷锁然后调用wait()和notify()实现这道面试题,我们从头到尾分析一下,首先List集合实现的add和size方法不多做解释,我们把重点放在mian方法上,main方法里我们创建了object对象,让后写了两个线程t1和t2,t1用来增加对象,t2用来监控list集合添加的对象个数,在t2线程我们给object对象加锁,然后判断list集合对象的个数为5的时候,就调用wait()方法阻塞t2线程,并给出相应提示,t1线程里我们给object对象加锁,通过for循环来给list集合添加对象,当对象添加到5个的时候,唤醒t2线程来完成对象个数的监控,这里我们需要保证先启动的是第二个线程,让它直接进入监控状态,以完成实时监控。

结果分析:

  • 当试过了小程序3,我们会发现,这种写法也是行不通的,原因是notify()方法不释放锁,当t1线程调用了notify()方法后,并没有释放当前的锁,所以t1还是会执行下去,待到t1执行完毕,t2线程才会被唤醒接着执行,这个时候对象已经不只有5个了,所以这个方案也是行不通的。

如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁

改良版小程序 3-----wait notify

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class T03_NotifyHoldingLock2 {
    //添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList();
    public void add(Object o) {
        lists.add(o);
    }
    public int size() {
        return lists.size();
    }
    public static void main(String[] args) {
        T03_NotifyHoldingLock2 c = new 淘宝网前端开发面试题--HTML & CSS 面试题

java后端应届生面试题,快来收藏!

华为腾讯淘宝面试流程+面试技术题分析,速看

华为腾讯淘宝面试流程+面试技术题分析,速看...

作用域与变量提升的面试题方法总结

企业面试:作用域和函数提升的面试题