解析:尽管面临很多挑战,多线程有一些优点仍然使得它一直被使用,而这些优点我们应该了解。
(1)资源利用率更好
想象一下,一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要5秒,处理一个文件需要2秒。处理两个文件则需要:
总的说来,CPU能够在等待IO的时候做一些其他的事情。这个不一定就是磁盘IO。它也可以是网络的IO,或者用户输入。通常情况下,网络和磁盘的IO比CPU和内存的IO慢的多。
(2)程序设计在某些情况下更简单
在单线程应用程序中,如果你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每个文件读取和处理的状态。相反,你可以启动两个线程,每个线程处理一个文件的读取和操作。线程会在等待磁盘读取文件的过程中被阻塞。在等待的时候,其他的线程能够使用CPU去处理已经读取完的文件。其结果就是,磁盘总是在繁忙地读取不同的文件到内存中。这会带来磁盘和CPU利用率的提升。而且每个线程只需要记录一个文件,因此这种方式也很容易编程实现。
(3)程序响应更快
有时我们会编写一些较为复杂的代码(这里的复杂不是说复杂的算法,而是复杂的业务逻辑),例如,一笔订单的创建,它包括插入订单数据、生成订单赶快找、发送邮件通知卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快地完成呢?
答:不一定。
比如,我们尝试使用并行和串行来分别执行累加的操作观察是否并行执行一定比串行执行更快:
以下是我测试的结果,可以看出,当不超过1百万的时候,并行是明显比串行要慢的,为什么并发执行的速度会比串行慢呢?这是因为线程有创建和上下文切换的开销。
解析:这是对多线程基础知识的考察
答:同步和异步通常用来形容一次方法调用。
同步方法调用一旦开始,调用者必须等到方法返回后,才能继续后续的行为。这就好像是我们去商城买一台空调,你看中了一台空调,于是就跟售货员下了单,然后售货员就去仓库帮你调配物品,这天你热的实在不行,就催着商家赶紧发货,于是你就在商店里等着,知道商家把你和空调都送回家,一次愉快的购物才结束,这就是同步调用。
面试官:那并发(Concurrency)和并行(Parallelism)的区别呢?
解析:并行性和并发性是既相似又有区别的两个概念。
如果在计算机系统中有多个处理机,这些可以并发执行的程序就可以被分配到多个处理机上,实现并发执行,即利用每个处理机处理一个可并发执行的程序。这样,多个程序便可以同时执行。以此就能提高系统中的资源利用率,增加系统的吞吐量。
线程上下文的切换比进程上下文切换要快很多。
面试官:进程间如何通讯?线程间如何通讯?
答:进程间通讯依靠IPC资源,例如管道(pipes)、套接字(sockets)等;
线程间通讯依靠JVM提供的API,例如wait()、notify()、notifyAll()等方法,线程间还可以通过共享的主内存来进行值的传递。
答:阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个而资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执行。
面试官:临界区是什么?
答:临界区用来表示一种公共资源或者说是共享资源,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
比如,在一个办公室里有一台打印机,打印机一次只能执行一个任务。如果小王和小明同时需要打印文件,很显然,如果小王先下发了打印任务,打印机就开始打印小王的文件了,小明的任务就只能等待小王打印结束后才能打印,这里的打印机就是一个临界区的例子。
在并行程序中,临界区资源是保护的对象,如果意外出现打印机同时执行两个打印任务,那么最可能的结果就是打印出来的文件就会是损坏的文件,它既不是小王想要的,也不是小明想要的。
面试官:如何避免死锁?(经常接着问这个问题哦~)
答:指定获取锁的顺序,举例如下:
解析:面试官会给你举个例子,如何让10个线程按照顺序打印0123456789?(写代码实现)
答:六种(查看Java源码也可以看到是6种),并且某个时刻Java线程只能处于其中的一个状态。
注意:从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED状态的线程也不能再回到RUNNABLE状态。
答:底层实现:
含义:(monitor机制)
Synchronized是在加锁,加对象锁。对象锁是一种重量锁(monitor),synchronized的锁机制会根据线程竞争情况在运行时会有偏向锁(单一线程)、轻量锁(多个线程访问synchronized区域)、对象锁(重量锁,多个线程存在竞争的情况)、自旋锁等。
该关键字是一个几种锁的封装。
答:该关键字可以保证可见性不保证原子性。
功能:
答:能。
一个典型的例子是在类中有一个long类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为volatile。为什么?因为Java中读取long类型变量不是原子的,需要分成两步,如果一个线程正在修改该long变量的值,另一个线程可能只能看到该值的一半(前32位)。但是对一个volatile型的long或double变量的读写是原子。
面试官:volatile修饰符的有过什么实践?
答:当使用ThreadLocal维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
ThreadLocal内部实现机制:
答:java.util.concurrent.ThreadPoolExecutor类就是一个线程池。客户端调用ThreadPoolExecutor.submit(Runnabletask)提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有3种形态:
面试官:我们为什么要使用线程池?
面试官:核心线程池内部实现了解吗?
答:对于核心的几个线程池,无论是newFixedThreadPool()方法,newSingleThreadExecutor()还是newCachedThreadPool()方法,虽然看起来创建的线程有着完全不同的功能特点,但其实内部实现均使用了ThreadPoolExecutor实现,其实都只是ThreadPoolExecutor类的封装。
为何ThreadPoolExecutor有如此强大的功能呢?我们可以来看一下ThreadPoolExecutor最重要的构造函数:
publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueue
答:有两种创建线程的方法:一是实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;二是直接继承Thread类。
面试官:两种方式有什么区别呢?
继承方式:
实现方式:
答:start()方法会新建一个线程并让这个线程执行run()方法;而直接调用run()方法知识作为一个普通的方法调用而已,它只会在当前线程中,串行执行run()中的代码。
答:Java中的线程可以有自己的优先级。优先极高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这只是一个概率问题。如果运行不好,高优先级线程可能也会抢占失败。
由于线程的优先级调度和底层操作系统有密切的关系,在各个平台上表现不一,并且这种优先级产生的后果也可能不容易预测,无法精准控制,比如一个低优先级的线程可能一直抢占不到资源,从而始终无法运行,而产生饥饿(虽然优先级低,但是也不能饿死它啊)。因此,在要求严格的场合,还是需要自己在应用层解决线程调度的问题。
在Java中,使用1到10表示线程优先级,一般可以使用内置的三个静态标量表示:
publicfinalstaticintMIN_PRIORITY=1;publicfinalstaticintNORM_PRIORITY=5;publicfinalstaticintMAX_PRIORITY=10;数字越大则优先级越高,但有效范围在1到10之间,默认的优先级为5。
答:Java提供了很丰富的API但没有为停止线程提供API。
JDK1.0本来有一些像stop(),suspend()和resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后JavaAPI的设计者就没有提供一个兼容且线程安全的方法来停止任何一个线程。
当run()或者call()方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile布尔变量来退出run()方法的循环或者是取消任务来中断线程。
答:忙循环就是程序员用循环让一个线程等待,不像传统方法wait(),sleep()或yield()它们都放弃了CPU控制权,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存。
答:从写代码的角度来说,两者的复杂度是相同的,因为同步代码与线程数量是相互独立的。但是同步策略的选择依赖于线程的数量,因为越多的线程意味着更大的竞争,所以你需要利用同步技术,如锁分离,这要求更复杂的代码和专业知识。
答:wait()方法应该在循环调用,因为当线程获取到CPU开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用wait和notify方法的代码:
//Thestandardidiomforusingthewaitmethodsynchronized(obj){while(conditiondoesnothold)obj.wait();//(Releaseslock,andreacquiresonwakeup)...//Performactionappropriatetocondition}参见EffectiveJava第69条,获取更多关于为什么应该在循环中来调用wait方法的内容。
答:伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行,如下图所示:
伪共享问题很难被发现,因为线程可能访问完全不同的全局变量,内存中却碰巧在很相近的位置上。如其他诸多的并发问题,避免伪共享的最基本方式是仔细审查代码,根据缓存行来调整你的数据结构。
解析:这是常考的基础类型的题,只要记住在同步块中调用wait()和notify()方法,如果阻塞,通过循环来测试等待条件。
1.饿汉式单例
饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:
publicclassSingleton{privatestaticSingletoninstance=newSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returninstance;}}2.加入synchronized的懒汉式单例
所谓懒汉式单例模式就是在调用的时候才去创建这个实例,我们在对外的创建实例方法上加如synchronized关键字保证其在多线程中很好的工作:
publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}}3.使用静态内部类的方式创建单例
这种方式利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式的区别是:饿汉式只要Singleton类被加载了,那么instance就会被实例化(没有达到lazyloading的效果),而这种方式是Singleton类被加载了,instance不一定被初始化。只有显式通过调用getInstance()方法时才会显式装载SingletonHoder类,从而实例化singleton
publicclassSingleton{privateSingleton(){}privatestaticclassSingletonHolder{//静态内部类privatestaticSingletonsingleton=newSingleton();}publicstaticSingletongetInstance(){returnSingletonHolder.singleton;}}4.双重校验锁
为了达到线程安全,又能提高代码执行效率,我们这里可以采用DCL的双检查锁机制来完成,代码实现如下:
publicclassSingleton{privatestaticSingletonsingleton;privateSingleton(){}publicstaticSingletongetInstance(){if(singleton==null){synchronized(Singleton.class){if(singleton==null){singleton=newSingleton();}}}returnsingleton;}}这种是用双重判断来创建一个单例的方法,那么我们为什么要使用两个if判断这个对象当前是不是空的呢?因为当有多个线程同时要创建对象的时候,多个线程有可能都停止在第一个if判断的地方,等待锁的释放,然后多个线程就都创建了对象,这样就不是单例模式了,所以我们要用两个if来进行这个对象是否存在的判断。
5.使用static代码块实现单例
静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性的实现单例设计模式。
publicclassSingleton{privatestaticSingletoninstance=null;privateSingleton(){}static{instance=newSingleton();}publicstaticSingletongetInstance(){returninstance;}}6.使用枚举数据类型实现单例模式
枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例: