多线程与高并发(四)--LockSupport、淘宝面试题与源码阅读方法论

一、LockSupport

1.传统方法的局限性

  • 以前我们要阻塞和唤醒某一个具体的线程有很多限制

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

2.LockSupport

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

  • 举个栗子1

首先使用lombda表达式创建了线程对象 " t " ,然后,通过 " t " 对象调用线程的启动方法start(),然后我们再看线程的内容,在for循环中,当 i 的值等于5的时候,我们调用了LockSupport的.park()方法使当前线程阻塞,注意看方法并没有加锁,就默认使当前线程阻塞了,由此可以看出LockSupprt.park()方法并没有加锁的限制

public class test {
   
    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();
    }
}
输出:012345
  • 举个栗子2

LockSupport的unpark()方法(唤醒线程)可以先于LockSupport的park()方法执行。

public class test {
   

    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);
    }
}
输出:0123456789
  • 举个栗子3

如果一个线程处于等待状态,连续调用了两次park()方法,就会使该线程永远无法被唤醒,运行后发现只有当i==5时的park被唤醒,i==8时依然会阻塞。

原因是LockSupport的unpark()方法就像是获得了一个“令牌”,而park()方法就像是在识别“令牌”,当主线程调用了LockSupport.unpark(t)方法也就说明线程 " t " 已经获得了”令牌”,i等于5时候,线程 " t " 调用LockSupport的park()方法时,线程 " t " 已经有令牌了,这样他就会马上再继续运行,也就不会被阻塞了。
但是当i==8时线程 " t " 再次调用了LockSupport的park()方法使线程再次进入阻塞状态,这个时候“令牌”已经被使用作废掉了,主线程处于等待令牌的状态,也就无法阻塞线程 " t " 了。所以当主线程处于等待“令牌”状态时,线程 " t " 再次调用了LockSupport的park()方法,那么线程 " t "就会永远阻塞下去,即使调用unpark()方法也无法唤醒了。

public class test {
   

    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);
        LockSupport.unpark(t);

    }
}输出:012345678
  • LockSupport中park()和unpark()方法的实现原理

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

二、面试题1

  • 实现一个容器,提供两个方法add、size,写两个线程:线程1,添加10个元素到容器中,线程2,实时监控元素个数,当个数到5个时,线程2给出提示并结束。

1.实现方式1

结论:第一这个方案没有加同步,第二while(true)中的c.size()方法永远没有检测到,没有检测到的原因是线程与线程之间是不可见的,导致无法break跳出循环。

public class test {
   

    List lists = new ArrayList();

    public void add(Object o) {
   
        lists.add(o);
    }

    public int size() {
   
        return lists.size();
    }

    public static void main(String[] args) {
   
        test c = new test();
        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();
    }
}
输出:
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9

2.实现方式2

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

  • 注释掉睡眠1秒将会持续输出,未注释时候输出满足。可以理解为睡眠时候线程去检测list里面的数是否达到5。
public class test {
   

    //添加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) {
   
        test c = new test();
        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();
    }
}
输出:
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9

3.实现方式3

用了锁的方式(利用wait()和notify()),通过给object对象枷锁然后调用wait()和notify()实现。

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

    //添加volatile,使t2能够得到通知
    volatile List lists = new ArrayList
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值