java进阶1 -「线程」

一 进程和线程

什么是进程

进程是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建, 运行到消亡的过程。

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

如下图所示,在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(.exe 文件的运行)。

什么是线程

线程与进程相似,但线程是一个比进程更小的执行单位,线程是操作系统能够进行运算调度的最小单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈.

总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

 

二 线程状态机

 

阻塞和等待的区别

阻塞: 当一个线程试图获得对象锁(非juc库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由jvm调度器来决定唤醒自己,而不是需要由另一个线程来显式唤醒自己,不响应中断

等待: 当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语意更丰富,可响应中断。例如: Object.wait(), Thread.join() 以及LockSupport.park()

需要强调的是虽然synchronized和JUC里的Lock都实现锁的功能,但线程进入的状态是不一样的。synchronized会让线程进入阻塞态,而JUC里的lock是用LockSupport.park()/unpark()来实现阻塞/唤醒的,会让线程进入等待态。但事实上,虽然等待锁时进入的状态不一样,但被唤醒后又都进入runnable态,从行为效果来看又是一样的。

 

 

三 面试问题

1 谈谈对 sleep, yield, join, interrupt, suspend的理解

(1) sleep

让线程睡眠,交出cpu,让cpu去执行其他的任务。不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

 

(2) yield

让线程交出cpu,让cpu去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出cpu的时间。

注意调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获得cpu执行时间。

 

(3) join

join()实际是利用了wait(),只不过它不用等待notify()/notifyAll(),且不受其影响。它结束的条件是:1)等待时间到;2)目标线程已经run完(通过isAlive()来判断)。

join和synchronized的区别是: join在内部使用wait()方法进行等待,而synchronized关键字使用的是"对象监视器"作为同步

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    
    //0则需要一直等到目标线程run完
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        //如果目标线程未run完且阻塞时间未到,那么调用线程会一直等待。
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

 

(4) interrupt

此操作会中断等待中的线程,并将线程的中断标志位置位。如果线程在运行态则不会受此影响

可以通过以下三种方式来判断中断:

1)isInterrupted()

此方法只会读取线程的中断标志位,并不会重置。

2)interrupted()

此方法读取线程的中断标志位,并会重置。

3)throw InterruptException

抛出该异常的同时,会重置中断标志位。

 

(5) suspend/resume

挂起线程,直到被resume,才会苏醒

但调用suspend()的线程和调用resume()的线程,可能会因为争锁的问题而发生死锁,所以JDK 7开始已经不推荐使用了。

 

 

2 java如何停止一个线程

Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法。但是由于潜在的死锁威胁,在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。

    private class Runner extends Thread{
    volatile boolean bExit = false;
  
    public void exit(boolean bExit){
        this.bExit = bExit;
    }
  
    @Override
    public void run(){
        while(!bExit){
            System.out.println("Thread is running");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ex) {
                    Logger.getLogger(ThreadTester.class.getName()).log(Level.SEVERE, null, ex);
                }
        }
    }
}

 

 

3 在java中wait和sleep方法的不同

共同点:两者都可以暂停线程的执行。

区别

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。为什么这样设计呢?下一个问题就会聊到。

 

 

4 为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

类似的问题:为什么 sleep() 方法定义在 Thread 中?

因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

 
 

5 为什么Thread类的sleep()和yield()方法是静态的

Thread类的sleep和yield都是作用在当前正在执行的线程上运行,所以其他处于等待状态的线程上调用这些方法是没有意义的。设置为静态表明在当前执行的线程上工作,避免开发错误地认为可以在其他非运行线程调用这些方法。

 

 

6 线程的优先级

Java中线程的优先级分为1-10这10个等级,如果小于1或大于10则JDK抛出IllegalArgumentException()的异常,默认优先级是5。在Java中线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。注意程序正确性不能依赖线程的优先级高低,因为操作系统可以完全不理会Java线程优先级

  

 

7 java线程池中submit()和execute()方法有什么区别

两者都可以向线程池提交任务,execute()方法的返回类型是void, 它定义在Executor接口中,而submit()方法可以返回有计算结果得到Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。

public interface ExecutorService extends Executor {
   ...
}

 

 

8 java中Runnable和Callable有什么不同

两者都代表那些要在不同的线程中执行的任务。Runnable从jdk1.0就开始有了,Callable是在jdk1.5增加的。它们的主要区别是Callable的call()方法可以返回值抛出异常,而Runnable的run()没有这些功能。Callable可以装载有计算结果的Future对象。

 

 

9 守护进程及其典型应用

Java中有两种线程,一种是用户线程,另一种是守护线程。当进程中不存在非守护线程了,则守护线程自动销毁。通过setDaemon(true)设置线程为后台线程。注意thread.setDaemon(true)必须在thread.start()之前设置,否则会报IllegalThreadStateException异常, 你不能把正在运行的常规线程设置为守护线程;在Daemon线程中产生的新线程也是Daemon的.

守护线程最典型的应用就是 GC (垃圾回收器)

User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

 

 

10 线程间的同步的方式有哪些?

线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。

下面是几种常见的线程同步的方式:

  1. 互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  2. 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
  3. 信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  4. 屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 CyclicBarrier 是这种机制。
  5. 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步。

 

11 wait, notify, notifyAll用法

首先要明确,只能在synchronized同步方法或者同步代码块中使用这些。在执行wait方法后,当前线程释放锁(这点与sleep, yield不同)。调用了wait函数的线程会一直等待,直到有其他线程调用了同一个对象的notify或notifyAll方法。需要注意的是,被唤醒并不代表立刻获得对象的锁,要等待执行notify方法的线程执行完,也即退出synchronized代码块后,当前线程才会释放锁,进而wait状态的线程才可以获得该对象锁

不在同步代码块会有IllegalMonitorStateException异常(RuntimeException)

* @throws  IllegalMonitorStateException  if the current thread is not
*               the owner of the object's monitor.

notify方法只会(随机)唤醒一个正在等待的线程,而notifyAll方法会唤醒所有正在等待的线程。如果一个对象之前没有调用wait方法,那么调用notify方法是没有任何影响的

 

 

12 interrupted和isInterrupted的区别

interrupted   判断当时线程是否已经是中断状态,执行后清除状态标志

isInterrupted 判断当时线程是否已经是中断状态,执行后清除状态标志

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
    return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);

 

 

13 单核 CPU 上运行多个线程效率一定会高吗?

单核 CPU 同时运行多个线程的效率是否会高,取决于任务的性质。一般来说有两种类型的任务:CPU密集型和IO密集型。CPU密集型的线程主要进行计算和逻辑处理,需要占用大量的CPU资源。IO密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待IO设备的响应,而不占用太多的CPU资源。

在单核CPU上,同一时刻只能有一个线程在运行,其他线程需要等待CPU的时间片分配。如果线程是CPU密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是IO 密集型的,那么多个线程同时运行可以利用CPU在等待IO时的空闲时间,提高了效率。

 

 

14 什么是线程死锁?如何避免死锁?(衍生问题众多)

认识线程死锁

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

输出

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。

上面的例子符合产生死锁的四个必要条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。

如何预防和避免线程死锁?

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件:一次性申请所有的资源。
  2. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3.....Pn> 序列为安全序列。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();

输出

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

Process finished with exit code 0

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

 

 

15 可以直接调用 Thread 类的 run 方法吗?

这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结:调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

 

 

16 进程间通信的方式

1. 管道( pipe ):管道是一种半双工(什么是半双工 https://tech.hqew.com/news_3834739)的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2. 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

3. 信号量( semaphore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

4. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5. 信号 ( signal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

6. 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

7. 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

 
 

 

17 高频设计题 - 有线程T1/T2/T3,如何确保T1执行之后执行T2,T2执行之后执行T3 

a join方法

        final Thread T1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("T1...");
            }
        });
        final Thread T2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    T1.join();
                }catch (InterruptedException ex){
                    ex.printStackTrace();
                }
                System.out.println("T2...");
            }
        });
        final Thread T3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    T2.join();
                }catch (InterruptedException ex){
                    ex.printStackTrace();
                }
                System.out.println("T3...");
            }
        });

        T3.start();
        T2.start();
        T1.start();

其他参看 https://www.cnblogs.com/yy3b2007com/p/8784383.html

 

 

18 高频设计题 - 生产者消费者模型

 最常用的有界生产者-消费者模型,简单概括如下:
  • 生产者持续生产,直到缓冲区满,阻塞;缓冲区不满后,继续生产
  • 消费者持续消费,直到缓冲区空,阻塞;缓冲区不空后,继续消费
  • 生产者可以有多个,消费者也可以有多个

可通过如下条件验证模型实现的正确性:

  • 同一产品的消费行为一定发生在生产行为之后
  • 任意时刻,缓冲区大小不小于0,不大于限制容量

前置接口定义

消费者:

public abstract class AbstractConsumer implements Runnable {
    protected abstract void consume() throws InterruptedException;

    @Override
    public void run() {
        while (true) {
            try {
                consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}

生产者:

public abstract class AbstractProducer implements Runnable {
    protected abstract void produce() throws InterruptedException;

    @Override
    public void run() {
        while (true) {
            try {
                produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}

bean:

public interface Model {
    Runnable newRunnableConsumer();
    Runnable newRunnableProducer();
}


public class Task {
    private int no;
    public Task(int no) {
        this.no = no;
    }

    public int getNo() {
        return no;
    }
}

实现1 - blockingQueue

BlockingQueue的写法最简单。核心思想是,把并发和容量控制封装在缓冲区中。

public class BlockingQueueModel implements Model {
    private final BlockingQueue<Task> blockingQueue;
    BlockingQueueModel(int capacity) {
        this.blockingQueue = new LinkedBlockingQueue<>(capacity);
    }

    private final AtomicInteger taskNo = new AtomicInteger(0);

    @Override
    public Runnable newRunnableConsumer() {
        return new AbstractConsumer() {
            @Override
            public void consume() throws InterruptedException {
                Task task = blockingQueue.take();
                // 固定时间范围的消费,模拟相对稳定的服务器处理过程
                TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500));
                System.out.println("consume: " + task.getNo());
            }
        };
    }

    @Override
    public Runnable newRunnableProducer() {
        return new AbstractProducer() {
            @Override
            public void produce() throws InterruptedException {
                // 不定期生产,模拟随机的用户请求
                TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));
                Task task = new Task(taskNo.getAndIncrement());
                blockingQueue.put(task);
                System.out.println("produce: " + task.getNo());
            }
        };
    }

    public static void main(String[] args) {
        Model model = new BlockingQueueModel(3);
        Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start());
        Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start());
    }
}

 

运行结果:

由于数据操作和日志输出是两个事务,所以上述日志的绝对顺序未必是真实的数据操作顺序,但对于同一个任务号task.getNo,其consume日志一定出现在其produce日志之后,即:同一任务的消费行为一定发生在生产行为之后。

 

实现2 - wait和notify

Object类提供的wait()方法与notifyAll()方法。 朴素的wait&&notify机制不那么灵活,但足够简单

public class WaitNotifyModel implements Model {

    private final Object BUFFER_LOCK = new Object();

    private final Queue<Task> queue = new LinkedList<>();
    private final int capacity;

    public WaitNotifyModel(int capacity) {
        this.capacity = capacity;
    }

    private final AtomicInteger taskNo = new AtomicInteger(0);

    @Override
    public Runnable newRunnableConsumer() {
        return new AbstractConsumer() {
            @Override
            public void consume() throws InterruptedException {
                synchronized (BUFFER_LOCK) {
                    while (queue.isEmpty()) {
                        BUFFER_LOCK.wait();
                    }

                    Task task = queue.poll();
                    assert task != null;
                    TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500));
                    System.out.println("consume: " + task.getNo());
                    BUFFER_LOCK.notifyAll();
                }
            }
        };
    }

    @Override
    public Runnable newRunnableProducer() {
        return new AbstractProducer() {
            @Override
            public void produce() throws InterruptedException {
                TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));
                synchronized (BUFFER_LOCK) {
                    while (queue.size() == capacity) {
                        BUFFER_LOCK.wait();
                    }
                    Task task = new Task(taskNo.getAndIncrement());
                    queue.offer(task);
                    System.out.println("produce: " + task.getNo());
                    BUFFER_LOCK.notifyAll();
                }
            }
        };
    }

    public static void main(String[] args) {
        Model model = new BlockingQueueModel(3);
        Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start());
        Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start());
    }
}
View Code

 

实现3 - lock和condition

java.util.concurrent包提供的Lock && Condition,对于实现二的简单变形

public class LockConditionModel implements Model {

    private final Lock BUFFER_LOCK = new ReentrantLock();
    private final Condition CONDITION = BUFFER_LOCK.newCondition();
    private final Queue<Task> queue = new LinkedList<>();

    private final int capacity;

    public LockConditionModel(int capacity) {
        this.capacity = capacity;
    }

    private final AtomicInteger taskNo = new AtomicInteger(0);

    @Override
    public Runnable newRunnableConsumer() {
        return new AbstractConsumer() {
            @Override
            public void consume() throws InterruptedException {
                BUFFER_LOCK.lockInterruptibly();
                try {
                    while (queue.isEmpty()) {
                        CONDITION.await();
                    }

                    Task task = queue.poll();
                    assert task != null;
                    TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500));
                    System.out.println("consume: " + task.getNo());
                    CONDITION.signalAll();
                } finally {
                    BUFFER_LOCK.unlock();
                }
            }
        };
    }

    @Override
    public Runnable newRunnableProducer() {
        return new AbstractProducer() {
            @Override
            public void produce() throws InterruptedException {
                TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));

                BUFFER_LOCK.lockInterruptibly();

                try {
                    while (queue.size() == capacity) {
                        CONDITION.await();
                    }
                    Task task = new Task(taskNo.getAndIncrement());
                    queue.offer(task);
                    System.out.println("produce: " + task.getNo());
                    CONDITION.signalAll();
                } finally {
                    BUFFER_LOCK.unlock();
                }
            }
        };
    }

    public static void main(String[] args) {
        Model model = new BlockingQueueModel(3);
        Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start());
        Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start());
    }
}
View Code

 

实现4 - 更高并发性能的lock和condition

实现三有一个问题,通过实践可以发现,实现二,三的效率明显低于实现一,并发瓶颈很明显,因为在锁 BUFFER_LOCK 看来,任何消费者线程与生产者线程都是一样的。换句话说,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)操作缓冲区 buffer。

而实际上,如果缓冲区是一个队列的话,“生产者将产品入队”与“消费者将产品出队”两个操作之间没有同步关系,可以在队首出队的同时,在队尾入队。理想性能可提升至两倍。

去掉这个瓶颈

那么思路就简单了:需要两个锁 CONSUME_LOCKPRODUCE_LOCKCONSUME_LOCK控制消费者线程并发出队,PRODUCE_LOCK控制生产者线程并发入队;相应需要两个条件变量NOT_EMPTYNOT_FULLNOT_EMPTY负责控制消费者线程的状态(阻塞、运行),NOT_FULL负责控制生产者线程的状态(阻塞、运行)。以此让优化消费者与消费者(或生产者与生产者)之间是串行的;消费者与生产者之间是并行的。 

public class LockConditionPreferModel implements Model {

    private final Lock CONSUMER_LOCK = new ReentrantLock();
    private final Condition NOT_EMPTY_CONDITION = CONSUMER_LOCK.newCondition();

    private final Lock PRODUCER_LOCK = new ReentrantLock();
    private final Condition NOT_FULL_CONDITION = PRODUCER_LOCK.newCondition();
    private AtomicInteger bufLen = new AtomicInteger(0);
    private final Buffer<Task> buffer = new Buffer<>();

    private final int capacity;

    public LockConditionPreferModel(int capacity) {
        this.capacity = capacity;
    }

    private final AtomicInteger taskNo = new AtomicInteger(0);

    @Override
    public Runnable newRunnableConsumer() {
        return new AbstractConsumer() {
            @Override
            public void consume() throws InterruptedException {
                int newBufSize;
                CONSUMER_LOCK.lockInterruptibly();
                try {
                    while (bufLen.get() == 0) {
                        System.out.println("buffer is empty...");
                        NOT_EMPTY_CONDITION.await();
                    }

                    Task task = buffer.poll();
                    newBufSize = bufLen.decrementAndGet();
                    assert task != null;
                    TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500));
                    System.out.println("consume: " + task.getNo());
                    if (newBufSize > 0) {
                        NOT_EMPTY_CONDITION.signalAll();
                    }
                } finally {
                    CONSUMER_LOCK.unlock();
                }

                if (newBufSize < capacity) {
                    PRODUCER_LOCK.lockInterruptibly();
                    try {
                        NOT_FULL_CONDITION.signalAll();
                    } finally {
                        PRODUCER_LOCK.unlock();
                    }
                }
            }
        };
    }

    @Override
    public Runnable newRunnableProducer() {
        return new AbstractProducer() {
            @Override
            public void produce() throws InterruptedException {
                TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));
                int newBufSize;
                PRODUCER_LOCK.lockInterruptibly();

                try {
                    while (bufLen.get() == capacity) {
                        System.out.println("buffer is full...");
                        NOT_FULL_CONDITION.await();
                    }
                    Task task = new Task(taskNo.getAndIncrement());
                    buffer.offer(task);
                    newBufSize = bufLen.incrementAndGet();
                    System.out.println("produce: " + task.getNo());
                    NOT_FULL_CONDITION.signalAll();
                } finally {
                    PRODUCER_LOCK.unlock();
                }

                if (newBufSize > 0) {
                    CONSUMER_LOCK.unlock();
                    try {
                        NOT_EMPTY_CONDITION.signalAll();
                    } finally {
                        CONSUMER_LOCK.unlock();
                    }
                }
            }
        };
    }

    private static class Buffer<E> {
        private Node head;
        private Node tail;

        Buffer() {
            head = tail = new Node(null);
        }

        private void offer(E e) {
            tail.next = new Node(e);
            tail = tail.next;
        }

        private E poll() {
            head = head.next;
            E e = head.item;
            head.item = null;
            return e;
        }

        private class Node {
            E item;
            Node next;

            Node(E item) {
                this.item = item;
            }
        }
    }

    public static void main(String[] args) {
        Model model = new BlockingQueueModel(3);
        Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start());
        Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start());
    }
}
View Code

 

需要注意的是,由于需要同时在UnThreadSafe的缓冲区 buffer 上进行消费与生产,我们不能使用实现二、三中使用的队列了,需要自己实现一个简单的缓冲区 Buffer。Buffer要满足以下条件:

  • 在头部出队,尾部入队
  • 在poll()方法中只操作head
  • 在offer()方法中只操作tail

实现要点:

1. 持有两种锁

2. 每次生产(/消费)结束会检验数据状态更新另一种锁,当然更新的过程要用相应的锁同步。

还能进一步优化吗

我们已经优化掉了消费者与生产者之间的瓶颈,还能进一步优化吗?

如果可以,必然是继续优化消费者与消费者(或生产者与生产者)之间的并发性能。然而,消费者与消费者之间必须是串行的,因此,并发模型上已经没有地方可以继续优化了。

不过在具体的业务场景中,一般还能够继续优化。如:

  • 并发规模中等,可考虑使用CAS代替重入锁
  • 模型上不能优化,但一个消费行为或许可以进一步拆解、优化,从而降低消费的延迟
  • 一个队列的并发性能达到了极限,可采用“多个队列”(如分布式消息队列等)

  

补充一下LinkedBlockingQueue在新增和删除时候的各个方法的区别:

一般情况建议用offer和poll,是即时操作,如果带时间的offer和poll相当于限时的同步等待

永久的同步等待使用put和take(@see 上面的实现一)

  

 

19 高频设计题 - 交替打印A,B

public class Test {
    public static void main(String[] args) throws Exception {
        ReentrantLock lock = new ReentrantLock(true);
        Condition condition = lock.newCondition();
        AtomicBoolean isA = new AtomicBoolean(true);
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
 
                lock.lock();
                try {
                    while (!isA.get()) {
                        //不是自己打印的标志就释放锁
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    System.out.print("A");
                    isA.set(false);
                    condition.signalAll();
                } finally {
                    lock.unlock();
                }
            }
        }).start();
 
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                lock.lock();
                try {
                    while (isA.get()) {
                        //不是自己打印的标志就释放锁
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    System.out.print("B");
                    isA.set(true);
                    condition.signalAll();
                } finally {
                    lock.unlock();
                }
            }
        }).start();
 
    }
}

 

public class Test {
    public static void main(String[] args) {
        BlockingQueue<Integer> queueA = new LinkedBlockingQueue<>(1);
        BlockingQueue<Integer> queueB = new LinkedBlockingQueue<>(1);
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    queueA.put(i);
                    System.out.print("A");
                    queueB.put(i);
                }catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
 
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    queueB.take();
                    System.out.print("B");
                    queueA.take();
                }catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
}

 

 

 

 

 

 

 
posted @ 2018-03-26 11:53  balfish  阅读(397)  评论(0编辑  收藏  举报