多线程与高并发四-面试题与AQS

面试题1

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

线程1,添加10个元素到容器中

线程2,实时监控元素个数,当个数到5个时,线程2给出提示并结束

wait&notify

/**
 * 曾经的面试题:(淘宝?)
 * 实现一个容器,提供两个方法,add,size
 * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
 * 
 * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢?
 * 
 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
 * 
 * 阅读下面的程序,并分析输出结果
 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
 * 想想这是为什么?
 * 
 * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行
 * 整个通信过程比较繁琐
 * @author mashibing
 */
package com.mashibing.juc.c_020_01_Interview;

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


public class T04_NotifyFreeLock {

   //添加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) {
      T04_NotifyFreeLock c = new T04_NotifyFreeLock();
      
      final Object lock = new Object();
      
      new Thread(() -> {
         synchronized(lock) {
            System.out.println("t2启动");
            if(c.size() != 5) {
               try {
                  lock.wait();
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
            }
            System.out.println("t2 结束");
            //通知t1继续执行
            lock.notify();
         }
         
      }, "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();
                  //释放锁,让t2得以执行
                  try {
                     lock.wait();
                  } catch (InterruptedException e) {
                     e.printStackTrace();
                  }
               }
               
               try {
                  TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
            }
         }
      }, "t1").start();
      
      
   }
}

LockSupport

/**
 * 曾经的面试题:(淘宝?)
 * 实现一个容器,提供两个方法,add,size
 * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
 * 
 * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢?
 * 
 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
 * 
 * 阅读下面的程序,并分析输出结果
 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
 * 想想这是为什么?
 * 
 * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行
 * 整个通信过程比较繁琐
 * 
 * 使用Latch(门闩)替代wait notify来进行通知
 * 好处是通信方式简单,同时也可以指定等待时间
 * 使用await和countdown方法替代wait和notify
 * CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
 * 当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了
 * 这时应该考虑countdownlatch/cyclicbarrier/semaphore
 * @author mashibing
 */
package com.mashibing.juc.c_020_01_Interview;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

//TODO park unpark

public class T07_LockSupport_WithoutSleep {

   // 添加volatile,使t2能够得到通知
   volatile List lists = new ArrayList();

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

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

   static Thread t1 = null, t2 = null;

   public static void main(String[] args) {
      T07_LockSupport_WithoutSleep c = new T07_LockSupport_WithoutSleep();

      t1 = new Thread(() -> {
         System.out.println("t1启动");
         for (int i = 0; i < 10; i++) {
            c.add(new Object());
            System.out.println("add " + i);

            if (c.size() == 5) {
               LockSupport.unpark(t2);
               LockSupport.park();
            }
         }
      }, "t1");

      t2 = new Thread(() -> {
         //System.out.println("t2启动");
         //if (c.size() != 5) {

            LockSupport.park();

         //}
         System.out.println("t2 结束");
         LockSupport.unpark(t1);


      }, "t2");

      t2.start();
      t1.start();
   }
}

面试题1总结

面试题1中的9个小程序9种方案,5种技术分别是volatile、wait()和notify()、Semaphore、CountDownLatch、LockSupport,其中wait()和notify()这个方案建议牢牢掌握,其它的可以用作巩固技术。

面试题2

写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。

wait&notify

/**
 * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
 * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
 * 
 * 使用wait和notify/notifyAll来实现
 * 
 * @author mashibing
 */
package com.mashibing.juc.c_021_01_interview;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

public class MyContainer1<T> {
   final private LinkedList<T> lists = new LinkedList<>();
   final private int MAX = 10; //最多10个元素
   private int count = 0;
   
   
   public synchronized void put(T t) {
      while(lists.size() == MAX) { //想想为什么用while而不是用if?
         try {
            this.wait(); //effective java
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
      
      lists.add(t);
      ++count;
      this.notifyAll(); //通知消费者线程进行消费
   }
   
   public synchronized T get() {
      T t = null;
      while(lists.size() == 0) {
         try {
            this.wait();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
      t = lists.removeFirst();
      count --;
      this.notifyAll(); //通知生产者进行生产
      return t;
   }
   
   public static void main(String[] args) {
      MyContainer1<String> c = new MyContainer1<>();
      //启动消费者线程
      for(int i=0; i<10; i++) {
         new Thread(()->{
            for(int j=0; j<5; j++) System.out.println(c.get());
         }, "c" + i).start();
      }
      
      try {
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      
      //启动生产者线程
      for(int i=0; i<2; i++) {
         new Thread(()->{
            for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
         }, "p" + i).start();
      }
   }
}

ReentrantLock

创建两个Condition,本质上是两个等待队列:在生产者线程里叫醒consumer等待队列的线程也就是消费者线程,在消费者线程里叫醒producer待队列的线程也就是生产者线程

/**
 * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
 * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
 * 
 * 使用wait和notify/notifyAll来实现
 * 
 * 使用Lock和Condition来实现
 * 对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒
 * 
 * @author mashibing
 */
package com.mashibing.juc.c_021_01_interview;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyContainer2<T> {
   final private LinkedList<T> lists = new LinkedList<>();
   final private int MAX = 10; //最多10个元素
   private int count = 0;
   
   private Lock lock = new ReentrantLock();
   private Condition producer = lock.newCondition();
   private Condition consumer = lock.newCondition();
   
   public void put(T t) {
      try {
         lock.lock();
         while(lists.size() == MAX) { //想想为什么用while而不是用if?
            producer.await();
         }
         
         lists.add(t);
         ++count;
         consumer.signalAll(); //通知消费者线程进行消费
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         lock.unlock();
      }
   }
   
   public T get() {
      T t = null;
      try {
         lock.lock();
         while(lists.size() == 0) {
            consumer.await();
         }
         t = lists.removeFirst();
         count --;
         producer.signalAll(); //通知生产者进行生产
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         lock.unlock();
      }
      return t;
   }
   
   public static void main(String[] args) {
      MyContainer2<String> c = new MyContainer2<>();
      //启动消费者线程
      for(int i=0; i<10; i++) {
         new Thread(()->{
            for(int j=0; j<5; j++) System.out.println(c.get());
         }, "c" + i).start();
      }
      
      try {
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      
      //启动生产者线程
      for(int i=0; i<2; i++) {
         new Thread(()->{
            for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
         }, "p" + i).start();
      }
   }
}

源码阅读

阅读源码不是一件容易的事!

  • 理解别人的思路,要先让自己的思维达到别人的高度
  • 读懂思路就可以了,不要读得太细
  • 框架源码有很多设计模式
  • 不要直接顺序地把一整个类从头读到尾

源码阅读原则

读源码的时候我建议大家自己去画图比如:

甬道图

这种图叫甬道图,这是UML图的一种叫甬道,这个图诠释了哪个类里的哪个方法又去调用了哪个类里的的哪个方法,你读源码的时候,你一定要把它画成图给画出来。

AQS源码解析

AQS的底层是 CAS +volatile

aqs

AQS等待队列

state 根据子类不同的实现,取不同的意义。
AQS类中有一个内部类Node,里面装的是它的成员变量Thread。很多Node组成一个双向链表,就是一个等待队列。
非公平的时候,如果抢不到锁,就进入队列继续等待。
如果抢到了,就用CAS的方式设定当前线程为独占这把锁的线程。

acquire

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquire

posted @ 2021-08-15 15:33  gary2048  阅读(289)  评论(0编辑  收藏  举报