Java并发编程相关面试问题-含程序答案
来源:互联网 发布:淘宝卖的黄金是真的吗 编辑:程序博客网 时间:2024/06/05 13:37
总结了并发编程面试中可能遇到的大部分编程题,写出答案供大家参考,如果问题请指出,谢过。
并发容器和框架
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;//glablevoid 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 均有体现。
欢迎各位大佬评论指出问题,谢过。
- Java并发编程相关面试问题-含程序答案
- Java并发编程相关面试问题
- Java集合相关面试问题和答案
- java并发编程中的面试问题
- Java并发面试问题
- JAVA并发多线程的面试问题及答案(基础部分)
- JAVA并发多线程的面试问题及答案(基础部分)
- java并发编程面试宝典
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 【Java并发编程】:线程间通信中notifyAll造成的早期通知问题(含代码)
- 【Java并发编程】:volatile变量修饰符—意料之外的问题(含代码)
- 【Java并发编程】:第五篇中volatile意外问题的正确分析解答(含代码)
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
- 使用ElasticSearch快速搭建数据搜索服务
- zend studio编辑器打开项目和显示项目
- android水印开发
- 【OSG运行错误】Unhandled exception at 0x7709B872 in XXXX.exe:Microsoft C++exception:std::bad_alloc
- JavaScript 全局变量和局部变量
- Java并发编程相关面试问题-含程序答案
- opencv3.0车牌号字符分割—通过字符的四点定位
- 如何高效阅读代码
- EventBus设置TAG,可以更灵活的发送和接收消息
- axis客户端接收不同参数类型
- 关于域名解析到服务器的问题
- 网卡的若干知识
- 超强大在线词频统计功能,从一篇文章到一个G的文本均可轻松统计
- angular2使用中问题解决集(不断更新)