Java并发编程相关面试问题-含程序答案

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_34039315/article/details/78542498

总结了并发编程面试中可能遇到的大部分编程题,写出答案供大家参考,如果问题请指出,谢过。

并发容器和框架

1.如何让一段程序并发的执行,并最终汇总结果?

使用CyclicBarrier 在多个关口处将多个线程执行结果汇总,
CountDownLatch 在各线程执行完毕后向总线程汇报结果。

CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
从api上理解就是CountdownLatch有主要配合使用两个方法countDown()和await(),countDown()是做事的线程用的方法,await()是等待事情完成的线程用个方法,这两种线程是可以分开的(下面例子:CountdownLatchTest2),当然也可以是同一组线程;CyclicBarrier只有一个方法await(),指的是做事线程必须大家同时等待,必须是同一组线程的工作。

CountdownLatch例子:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 各个线程执行完成后,主线程做总结性工作的例子
 * @author xuexiaolei
 * @version 2017年11月02日
 */
public class CountdownLatchTest2 {
    private final static int THREAD_NUM = 10;
    public static void main(String[] args) {
        CountDownLatch lock = new CountDownLatch(THREAD_NUM);
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < THREAD_NUM; i++) {
            exec.submit(new CountdownLatchTask(lock, "Thread-"+i));
        }
        try {
            lock.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("大家都执行完成了,做总结性工作");
        exec.shutdown();
    }

    static class CountdownLatchTask implements Runnable{
        private final CountDownLatch lock;
        private final String threadName;
        CountdownLatchTask(CountDownLatch lock, String threadName) {
            this.lock = lock;
            this.threadName = threadName;
        }
        @Override public void run() {
            System.out.println(threadName + " 执行完成");
            lock.countDown();
        }
    }
}

CyclicBarrier例子:

import java.util.concurrent.*;

/**
 *
 * @author xuexiaolei
 * @version 2017年11月02日
 */
public class CyclicBarrierTest {
    private final static int THREAD_NUM = 10;
    public static void main(String[] args) {
        CyclicBarrier lock = new CyclicBarrier(THREAD_NUM, new Runnable() {
            @Override public void run() {
                System.out.println("这阶段大家都执行完成了,我总结一下,然后开始下一阶段");
            }
        });
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < THREAD_NUM; i++) {
            exec.submit(new CountdownLatchTask(lock, "Task-"+i));
        }
        exec.shutdown();
    }

    static class CountdownLatchTask implements Runnable{
        private final CyclicBarrier lock;
        private final String threadName;
        CountdownLatchTask(CyclicBarrier lock, String threadName) {
            this.lock = lock;
            this.threadName = threadName;
        }
        @Override public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println(threadName + " 执行完成");
                try {
                    lock.await();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

2.如何合理的配置java线程池?如CPU密集型的任务,基本线程池应该配置多大?IO密集型的任务,基本线程池应该配置多大?用有界队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?

虽然Exectors可以生成一些很常用的线程池,但毕竟在什么情况下使用还是开发者最清楚的。在某些自己很清楚的使用场景下,java线程池还是推荐自己配置的。下面是java线程池的配置类的参数,我们逐一分析一下:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

  • corePoolSize - 池中所保存的线程数,包括空闲线程。
  • maximumPoolSize - 池中允许的最大线程数。
  • keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
  • unit - keepAliveTime 参数的时间单位。
  • workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。用BlocingQueue的实现类都可以。
  • threadFactory - 执行程序创建新线程时使用的工厂。自定义线程工厂可以做一些额外的操作,比如统计生产的线程数等。
  • handler - 饱和策略,即超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。策略有:Abort终止并抛出异常,Discard悄悄抛弃任务,Discard-Oldest抛弃最老的任务策略,Caller-Runs将任务退回给调用者策略。

至于线程池应当配置多大的问题,一般有如下的经验设置:
1. 如果是CPU密集型应用,则线程池大小设置为N+1。
2. 如果是IO密集型应用,则线程池大小设置为2N+1。

用有界队列好还是无界队列好?这种问题的答案肯定是视情况而定:
1. 有界队列有助于避免资源耗尽的情况发生。但他带来了新的问题:当队列填满后,新的任务怎么办?所以有界队列适用于执行比较耗资源的任务,同时要设计好相应的饱和策略。
2. 无界队列和有界队列刚好相反,在资源无限的情况下可以一直接收新任务。适用于小任务,请求和处理速度相对持平的状况。
3. 其实还有一种同步移交的队列 SynchronousQueue ,这种队列不存储任务信息,直接将任务提交给线程池。可以理解为容量只有1的有界队列,在特殊场景下有特殊作用,同样得设计好相应的饱和策略。

3.如何使用阻塞队列实现一个生产者和消费者模型?请写代码。

用有界的BlockingQueue来实现,其实BlockingQueue已经将很多调用细节隐去了,实现很简单了。

/**
 * 用阻塞队列快速实现生产者-消费者
 * @author xuexiaolei
 * @version 2017年11月01日
 */
public class ProduceAndConsumer {
    public static void main(String[] args) {
        final BlockingQueue<Integer> list = new ArrayBlockingQueue<Integer>(10);
        Procude procude = new Procude(list);
        Consumer consumer = new Consumer(list);
        procude.start();
        consumer.start();
    }

    static class Procude extends Thread{
        private final BlockingQueue<Integer> list;
        Procude(BlockingQueue<Integer> list) {
            this.list = list;
        }
        @Override public void run() {
            while(true){
                try {
                    Integer take = list.take();
                    System.out.println("消费数据:" + take);
//                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer extends Thread{
        private final BlockingQueue<Integer> list;
        Consumer(BlockingQueue<Integer> list) {
            this.list = list;
        }
        @Override public void run() {
            while (true){
                try {
                    int i = new Random().nextInt(100);
                    list.put(i);
                    System.out.println("生产数据:" + i);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4.多读少写的场景应该使用哪个并发容器,为什么使用它?比如你做了一个搜索引擎,搜索引擎每次搜索前需要判断搜索关键词是否在黑名单里,黑名单每天更新一次。

用CopyOnWriteArrayList、CopyOnWriteArraySet。

CopyOnWriteArrayList特性:
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

搜索引擎黑名单缓存实现:(自己复查了很多遍,如有问题,请指出)

import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.*;

/**
 * 做了一个搜索引擎,搜索引擎每次搜索前需要判断搜索关键词是否在黑名单里,黑名单每天更新一次。
 * @author xuexiaolei
 * @version 2017年11月03日
 */
public class SearchEngineBlackListCache {
    private final Set<String> blackList = new CopyOnWriteArraySet<>();//黑名单集合
    private final Set<String> todayBlackList = new ConcurrentSkipListSet<>();//当天添加的黑名单


    /******内部类单例写法 start******/
    private SearchEngineBlackListCache(){}
    private static class Holder {
        private static SearchEngineBlackListCache singleton = new SearchEngineBlackListCache();
    }
    public static SearchEngineBlackListCache getInstance(){
        return Holder.singleton;
    }
    /******内部类单例写法 end******/

    /**
     * 获取黑名单列表
     * @return
     */
    public Set<String> getBlackList() {
        return Collections.unmodifiableSet(blackList);
    }

    /**
     * 判断是否在黑名单内
     * @param name
     * @return
     */
    public boolean isBlack(String name){
        return blackList.contains(name);
    }

    /**
     * 将今天的黑名单加入到黑名单内,外部系统可以定时每天执行这个方法
     */
    public void mergeBlackList(){
        synchronized (todayBlackList){
            blackList.addAll(todayBlackList);
            todayBlackList.clear();
        }
    }

    /**
     * 加入黑名单
     * @param name
     */
    public void addBlackList(String name){
        synchronized (todayBlackList) {
            todayBlackList.add(name);
        }
    }

    /**
     * 随机生成50个线程来测试
     * @param args
     */
    public static void main(String[] args) {
        SearchEngineBlackListCache cache = SearchEngineBlackListCache.getInstance();
        final int COUNT = 50;
        CountDownLatch countDownLatch = new CountDownLatch(COUNT);
        ExecutorService exec = Executors.newFixedThreadPool(COUNT);
        for (int i = 0; i < COUNT; i++) {
            exec.execute(new Runnable() {
                @Override public void run() {
                    try {
                        countDownLatch.countDown();
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    cache.addBlackList(UUID.randomUUID().toString());//随机增加黑名单字符串
                    cache.mergeBlackList();
                    System.out.println(cache.getBlackList().size());
                }
            });
        }
        exec.shutdown();
    }
}

Java中的锁

1.如何实现乐观锁(CAS)?如何避免ABA问题?

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

ABA问题:
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
解决方法:通过版本号(version)的方式来解决,每次比较要比较数据的值和版本号两项内容即可。

2.读写锁可以用于什么应用场景?

比如网上共享白板,共享文档等都适用。

读写锁逻辑:
1. 当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞。
2. 当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行枷锁的线程将阻塞。
3. 当读写锁在读模式锁状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞。

3.什么时候应该使用可重入锁?

举例来说明锁的可重入性

public class UnReentrant{
    Lock lock = new Lock();
    public void outer(){
        lock.lock();
        inner();
        lock.unlock();
    }
    public void inner(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。

synchronized、ReentrantLock都是可重入的锁,可重入锁相对来说简化了并发编程的开发。

4.什么场景下可以使用volatile替换synchronized?

状态标志:把简单地volatile变量作为状态标志,来达成线程之间通讯的目的,省去了用synchronized还要wait,notify或者interrupt的编码麻烦。
替换重量级锁:如果某个变量仅是单次读或者单次写操作,没有复合操作(i++,先检查后判断之类的)就可以用volatile替换synchronized。

并发工具

1.如何实现一个流控程序,用于控制请求的调用次数?

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 阻塞访问的线程,直到获取了访问令牌
 * @author xuexiaolei
 * @version 2017年11月15日
 */
public class FlowControl2 {
    private final static int MAX_COUNT = 10;
    private final Semaphore semaphore = new Semaphore(MAX_COUNT);
    private final ExecutorService exec = Executors.newCachedThreadPool();

    public void access(int i){
        exec.submit(new Runnable() {
            @Override public void run() {
                semaphore.acquireUninterruptibly();
                doSomething(i);
                semaphore.release();
            }
        });
    }

    public void doSomething(int i){
        try {
            Thread.sleep(new Random().nextInt(100));
            System.out.println(String.format("%s 通过线程:%s 访问成功",i,Thread.currentThread().getName()));
        } catch (InterruptedException e) {
        }
    }

    public static void main(String[] args) {
        FlowControl2 web = new FlowControl2();
        for (int i = 0; i < 2000; i++) {
            web.access(i);
        }
    }
}

几道笔试题目

1.(百度笔试题)以下多线程对int型变量x的操作,哪几个不需要进行同步:

A. x=y;
B. x++;
C. ++x;
D. x=1;

答案:D
前三个都至少需要先读取,再操作,非原子操作。而D的话,直接赋值。

2.(阿里巴巴笔试题)多线程中栈与堆是公有的还是私有的

A:栈公有, 堆私有
B:栈公有,堆公有
C:栈私有, 堆公有
D:栈私有,堆私有

答案:C

3.一个全局变量tally,两个线程并发执行(代码段都是ThreadProc),问两个线程都结束后,tally取值范围。

int tally = 0;//glable
void ThreadProc()
{
    for(int i = 1; i <= 50; i++)
        tally += 1;
}

答案:50到100
tolly+=1,要分为三个指令(读tolly,tolly+1,写回tolly)
50的一种情况是:线程一读x,线程二也读x,线程一寄存器加一,线程二寄存器加一,放回x,线程二放加x,这种情况虽然二个线程都对x加1,但显然只加了一次。所以到最后只加50次。

4.子线程循环 10 次,接着主线程循环 100 次,接着又回到子线程循环 10 次,接着再回到主线程又循环 100 次,如此循环50次,试写出代码。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 
 * @author xuexiaolei
 * @version 2017年11月06日
 */
public class Interview4_2 {
    private static final int COUNT = 50;
    private static final Object lock = new Object();
    private static AtomicBoolean permit = new AtomicBoolean(true);//控制当前执行线程的权力

    public static void main(String[] args) {
        ExecutorService exec = Executors.newSingleThreadExecutor();
        exec.execute(new Runnable() {
            @Override public void run() {
                for (int i = 0; i < COUNT; i++) {
                    synchronized (lock) {
                        while (!permit.get()) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                        System.out.println("子线程循环十次");
                        permit.set(false);
                        lock.notifyAll();
                    }
                }
                System.out.println("子线程结束");
            }
        });
        for (int i = 0; i < COUNT; i++) {
            synchronized (lock) {
                while (permit.get()) {
                    try {
                        System.out.println("主线程等待");
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("主线程循环一百次");
                permit.set(true);
                lock.notifyAll();
            }
        }
        System.out.println("主线程结束");
        exec.shutdown();
    }
}

5.(迅雷笔试题):编写一个程序,开启3个线程,这3个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示;如:ABCABC….依次递推。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 *5.(迅雷笔试题):编写一个程序,开启3个线程,这3个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示;如:ABCABC….依次递推。
 * @author xuexiaolei
 * @version 2017年11月10日
 */
public class Interview5 {
    private static final int COUNT = 10;
    private static final Object lock = new Object();
    private static volatile String permit = "A";//控制当前执行的线程标识

    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(3);
        exec.execute(new Task("A"));
        exec.execute(new Task("B"));
        exec.execute(new Task("C"));
        exec.shutdown();
    }

    static class Task implements Runnable {
        private final String name;
        Task(String name) {
            this.name = name;
        }
        @Override public void run() {
            for (int i = 0; i < COUNT; i++) {
                synchronized (lock){
                    while (!this.name.equals(permit)){
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(this.name);
                    permit = nextPermit(permit);
                    lock.notifyAll();
                }
            }
        }

        private String nextPermit(String permit) {
            if (permit.equals("A")) return "B";
            if (permit.equals("B")) return "C";
            if (permit.equals("C")) return "A";
            throw new RuntimeException("hello");
        }
    }
}

6.(Google面试题)有四个线程1、2、3、4。线程1的功能就是输出1,线程2的功能就是输出2,以此类推………现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式:

A:1 2 3 4 1 2….
B:2 3 4 1 2 3….
C:3 4 1 2 3 4….
D:4 1 2 3 4 1….
请设计程序。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 (Google面试题)有四个线程1、2、3、4。线程1的功能就是输出1,线程2的功能就是输出2,以此类推.........现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式:
 A:1 2 3 4 1 2....
 B:2 3 4 1 2 3....
 C:3 4 1 2 3 4....
 D:4 1 2 3 4 1....
 请设计程序。

 版本一:设计的四个线程,线程间完全独立。和轮询的思想十分相似,线程各自尝试去获取文件锁,然后再看是否能写入当前文件。
        当前十分低效,但如果 写操作 消耗的时间越多,效率就越高
 一次输出例子如下:
         1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1
         2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1
         3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3
         4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3
 * @author xuexiaolei
 * @version 2017年11月14日
 */
public class Interview6 {
    private static final AtomicInteger count = new AtomicInteger(0);
    ///模拟四个文件
    private static final StringBuffer fileA = new StringBuffer();
    private static final StringBuffer fileB = new StringBuffer();
    private static final StringBuffer fileC = new StringBuffer();
    private static final StringBuffer fileD = new StringBuffer();
    //四个文件的锁
    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();
    private static final ReentrantLock lockC = new ReentrantLock();
    private static final ReentrantLock lockD = new ReentrantLock();
    //四个文件的书写位置
    private static final AtomicInteger numberA = new AtomicInteger(1);
    private static final AtomicInteger numberB = new AtomicInteger(2);
    private static final AtomicInteger numberC = new AtomicInteger(3);
    private static final AtomicInteger numberD = new AtomicInteger(4);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(4);
        exec.execute(new writeTask(1));
        exec.execute(new writeTask(2));
        exec.execute(new writeTask(3));
        exec.execute(new writeTask(4));
        exec.shutdown();
        Thread.sleep(5000);//等待线程池结束后输出文件内容
        System.out.println(fileA);
        System.out.println(fileB);
        System.out.println(fileC);
        System.out.println(fileD);
    }

    static class writeTask implements Runnable{
        private final int wirteContent;//输出的内容,线程1的功能就是输出1,线程2的功能就是输出2
        writeTask(int wirteContent) {
            this.wirteContent = wirteContent;
        }
        @Override public void run() {
            while (count.get() < 1000) {//1000为多个线程总共大概尝试的次数
                //尝试写入A文件
                try {
                    boolean a = lockA.tryLock(1, TimeUnit.MILLISECONDS);
                    if (a) {
                        if (numberA.get() % 4 == wirteContent%4) {
                            fileA.append(wirteContent + " ");
                            numberA.incrementAndGet();
                        }
                        lockA.unlock();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //尝试写入文件
                try {
                    boolean a = lockB.tryLock(1, TimeUnit.MILLISECONDS);
                    if (a) {
                        if (numberB.get() % 4 == wirteContent%4) {
                            fileB.append(wirteContent + " ");
                            numberB.incrementAndGet();
                        }
                        lockB.unlock();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //尝试写入C文件
                try {
                    boolean a = lockC.tryLock(1, TimeUnit.MILLISECONDS);
                    if (a) {
                        if (numberC.get() % 4 == wirteContent%4) {
                            fileC.append(wirteContent + " ");
                            numberC.incrementAndGet();
                        }
                        lockC.unlock();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //尝试写入D文件
                try {
                    boolean a = lockD.tryLock(1, TimeUnit.MILLISECONDS);
                    if (a) {
                        if (numberD.get() % 4 == wirteContent%4) {
                            fileD.append(wirteContent + " ");
                            numberD.incrementAndGet();
                        }
                        lockD.unlock();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count.incrementAndGet();
            }
        }
    }
}

7.启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5, 然后是线程2打印6,7,8,9,10, 然后是线程3打印11,12,13,14,15. 接着再由线程1打印16,17,18,19,20….以此类推, 直到打印到75. 程序的输出结果应该为:

线程1: 1
线程1: 2
线程1: 3
线程1: 4
线程1: 5

线程2: 6
线程2: 7
线程2: 8
线程2: 9
线程2: 10

线程3: 71
线程3: 72
线程3: 73
线程3: 74
线程3: 75

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 * 7.启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5, 然后是线程2打印6,7,8,9,10, 然后是线程3打印11,12,13,14,15. 接着再由线程1打印16,17,18,19,20....以此类推, 直到打印到75. 程序的输出结果应该为:
 线程1: 1
 线程1: 2
 线程1: 3
 线程1: 4
 线程1: 5

 线程2: 6
 线程2: 7
 线程2: 8
 线程2: 9
 线程2: 10
 ...

 线程3: 71
 线程3: 72
 线程3: 73
 线程3: 74
 线程3: 75

 处理边界条件有点烦,其他还是不错的
 * @author xuexiaolei
 * @version 2017年11月14日
 */
public class Interview7 {
    private static final Object lock = new Object();
    private static final AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(3);
        exec.execute(new Task("线程1", 0));
        exec.execute(new Task("线程2", 1));
        exec.execute(new Task("线程3", 2));
        exec.shutdown();
    }

    static class Task implements Runnable {
        private final String threadName;
        private final int count;
        Task(String threadName, int count) {
            this.threadName = threadName;
            this.count = count;
        }

        @Override public void run() {
            do {
                synchronized (lock) {
                    while ((counter.get()/5)%3 != count) {//counter每次输出完成肯定是5的倍数,除以5然后对3取余判断是否是当前线程来写
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    for (int i = 0; i < 5; i++) {
                        System.out.println(threadName+":"+counter.incrementAndGet());
                    }
                    lock.notifyAll();
                }
            } while (counter.get() < 65);
        }
    }
}

8.在Java中创建线程安全的Singleton。

一共五种写法:

  • 饿汉式写法
  • 懒汉式写法
  • 双重检查锁定
  • 内部类写法
  • 枚举写法
/**
 * 饿汉式写法
 *
 * 就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。
 * 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
 *
 * @author xuexiaolei
 * @version 2017年11月15日
 */
public class Singleton01 {
    private static Singleton01 instance = new Singleton01();
    private Singleton01(){}
    public static Singleton01 getInstance() {
        return instance;
    }
}

/**
 * 懒汉式写法
 *
 * 线程安全,但是getInstance方法效率十分低
 *
 * @author xuexiaolei
 * @version 2017年11月15日
 */
public class Singleton02 {
    private static Singleton02 instance = null;
    private Singleton02(){}
    public static synchronized Singleton02 getInstance(){
        if (instance == null){
            instance = new Singleton02();
        }
        return instance;
    }
}

/**
 * 双重检查锁写法
 *
 * getInstance()方法中,进行两次null检查。看似多此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。
 *
 * 注意volatile的语义:可见性和禁止指令重排。可见性是jdk一直支持的,禁止指令重拍在jdk1.5之后才开始支持,所以此方法在jdk1.5以上才可运行。
 *
 * @author xuexiaolei
 * @version 2017年11月15日
 */
public class Singleton03 {
    private static volatile Singleton03 instance = null;
    private Singleton03(){}
    public static Singleton03 getInstance(){
        if(instance == null){
            synchronized (Singleton03.class){
                if (instance == null){
                    instance = new Singleton03();
                }
            }
        }
        return instance;
    }
}


/**
 * 静态内部类法
 *
 * Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。
 *
 * @author xuexiaolei
 * @version 2017年11月15日
 */
public class Singleton04 {
    private static class Inner{
        private static Singleton04 instance = new Singleton04();
    }
    private Singleton04(){}
    public static Singleton04 getInstance(){
        return Inner.instance;
    }
}

/**
 * 枚举写法
 *
 * 优雅。使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。
 *
 * @author xuexiaolei
 * @version 2017年11月15日
 */
public enum Singleton05 {
    INSTANCE;
    /****任意方法直接用****/
    public void method(){

    }
}

代码在 https://gitee.com/xuea/alltest/tree/master/concurrency/src/main/java/com/leo/interview 均有体现。
欢迎各位大佬评论指出问题,谢过。

展开阅读全文

没有更多推荐了,返回首页