前言
Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。
在典型的Java面试中, 面试官会从线程的基本概念问起,
如:为什么你需要使用线程, 如何创建线程,用什么方式创建线程比较好(比如:继承thread类还是调用Runnable接口),
然后逐渐问到并发问题像在Java并发编程的过程中遇到了什么挑战,Java内存模型,JDK1.5引入了哪些更高阶的并发工具,并发编程常用的设计模式,
经典多线程问题如生产者消费者,哲学家就餐,读写器或者简单的有界缓冲区问题。仅仅知道线程的基本概念是远远不够的,
你必须知道如何处理死锁,竞态条件,内存冲突和线程安全等并发问题。
1、多线程有什么用?
一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?
在我看来,这个回答更扯淡。
所谓”知其然知其所以然”,”会用”只是”知其然”,”为什么用”才是”知其所以然”,只有达到”知其然知其所以然”的程度才可以说是把一个知识点运用自如。
OK,下面说说我对这个问题的看法:
(1)发挥多核CPU的优势
-
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,
如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。
单核CPU上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了。
多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
(2)防止阻塞
-
从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。
但是单核CPU我们还是要应用多线程,就是为了防止阻塞。
试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。
多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
(3)便于建模
-
这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。
但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
2、 什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。
比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。
Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。
3、线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。
不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。
别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。
不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。
别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
4、创建线程的方式
一般就是两种:
(1)继承Thread类
(2)实现Runnable接口
至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,面向接口编程也是设计模式6大原则的核心。
5、Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。
如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
6、Java中Runnable和Callable有什么不同?
Runnable和Callable都代表那些要在不同的线程中执行的任务。
Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。
它们的主要区别是:
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?
某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?
无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。
而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
7、Java中CyclicBarrier 和 CountDownLatch有什么不同?
CyclicBarrier 和 CountDownLatch,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,都可以用来让一组线程等待其它线程。
二者的区别在于:
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;
CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
8、Java内存模型是什么?
Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。
Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。
这个关系定义了一些规则让程序员在并发编程时思路更清晰。
比如,先行发生关系确保了:
-
线程内的代码能够按先后顺序执行,这被称为程序次序规则。
对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
一个线程的所有操作都会在线程终止之前,线程终止规则。
一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
可传递性
9、Java中的volatile 变量是什么?
理解volatile关键字的作用的前提是要理解Java内存模型,volatile关键字的作用主要有两个:
(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,
即每次读取到volatile变量,一定是最新的数据
(2)代码底层执行不像我们看到的高级语言—-Java程序这么简单,
它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,
现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。
使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,
详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。
简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
volatile是一个特殊的修饰符,只有成员变量才能使用它。
在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。
volatile变量可以保证下一个读取操作会在前一个写操作之后发生。
10、volatile 变量和 atomic 变量有什么不同?
首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。
Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。
例如用volatile修饰count变量那么 count++ 操作就不是原子性的。
而AtomicInteger类提供的atomic方法可以让这种操作具有原子性。
如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
11、什么是线程安全
这是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:
-
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
(1)不可变
-
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,
因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
-
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,
实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
-
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,
如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
(4)线程非安全
-
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类
12、synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,
ReentrantLock比synchronized的扩展性体现在几点上:
-
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。
13、ReadWriteLock是什么
首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。
如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,
但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,
ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,
读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
14、你如何在Java中获取线程堆栈?
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。
所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
(1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
(2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,
因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,
15、Linux环境下如何查找哪个线程使用CPU最长
这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:
-
(1)获取项目的pid,jps或者ps -ef | grep java
(2)top -H -p pid,顺序不能改变
这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。
注意:这里打出的是LWP,也就是操作系统原生线程的线程号。
- 使用”top -H -p pid”+”jps pid”可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。
最后提一点,”top -H -p pid”打出来的LWP是十进制的,”jps pid”打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。
16、同步方法和同步块,哪个是更好的选择
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。
这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,
我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,
因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,
因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,
将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。
17、Java中如何停止一个线程?
Java提供了很丰富的API但没有为停止线程提供API。
JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,
之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。
当run() 或者 call() 方法执行完的时候线程会自动结束,
如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。
- 示例代码
private class Runner extends Thread{
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);
}
}
}
}
18、一个线程如果出现了运行时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停止执行了。
简单的说,如果异常没有被捕获该线程将会停止执行。
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。
当一个未捕获异常将造成线程中断的时候JVM会使Thread.getUncaughtExceptionHandler()
来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放
19、如何在两个线程间共享数据?
你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。
通过在线程之间共享对象就可以了,
然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,
比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的
用wait和notify方法实现了生产者消费者模型。
package concurrency;
import java.util.LinkedList;
import java.util.Queue;
import org.apache.log4j.Logger;
public class InterThreadCommunicationExample {
public static void main(String args[]) {
final Queue sharedQ = new LinkedList();
Thread producer = new Producer(sharedQ);
Thread consumer = new Consumer(sharedQ);
producer.start();
consumer.start();
}
}
public class Producer extends Thread {
private static final Logger logger = Logger.getLogger(Producer.class);
private final Queue sharedQ;
public Producer(Queue sharedQ) {
super("Producer");
this.sharedQ = sharedQ;
}
@Override
public void run() {
for (int i = 0; i < 4; i++) {
synchronized (sharedQ) {
//waiting condition - wait until Queue is not empty
while (sharedQ.size() >= 1) {
try {
logger.debug("Queue is full, waiting");
sharedQ.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
logger.debug("producing : " + i);
sharedQ.add(i);
sharedQ.notify();
}
}
}
}
public class Consumer extends Thread {
private static final Logger logger = Logger.getLogger(Consumer.class);
private final Queue sharedQ;
public Consumer(Queue sharedQ) {
super("Consumer");
this.sharedQ = sharedQ;
}
@Override
public void run() {
while(true) {
synchronized (sharedQ) {
//waiting condition - wait until Queue is not empty
while (sharedQ.size() == 0) {
try {
logger.debug("Queue is empty, waiting");
sharedQ.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
int number = sharedQ.poll();
logger.debug("consuming : " + number );
sharedQ.notify();
//termination condition
if(number == 3){break; }
}
}
}
}
Output:
05:41:57,244 0 [Producer] DEBUG concurrency.Producer - producing : 0
05:41:57,260 16 [Producer] DEBUG concurrency.Producer - Queue is full, waiting
05:41:57,260 16 [Consumer] DEBUG concurrency.Consumer - consuming : 0
05:41:57,260 16 [Consumer] DEBUG concurrency.Consumer - Queue is empty, waiting
05:41:57,260 16 [Producer] DEBUG concurrency.Producer - producing : 1
05:41:57,260 16 [Producer] DEBUG concurrency.Producer - Queue is full, waiting
05:41:57,260 16 [Consumer] DEBUG concurrency.Consumer - consuming : 1
05:41:57,260 16 [Consumer] DEBUG concurrency.Consumer - Queue is empty, waiting
05:41:57,260 16 [Producer] DEBUG concurrency.Producer - producing : 2
05:41:57,260 16 [Producer] DEBUG concurrency.Producer - Queue is full, waiting
05:41:57,260 16 [Consumer] DEBUG concurrency.Consumer - consuming : 2
05:41:57,260 16 [Consumer] DEBUG concurrency.Consumer - Queue is empty, waiting
05:41:57,260 16 [Producer] DEBUG concurrency.Producer - producing : 3
05:41:57,276 32 [Consumer] DEBUG concurrency.Consumer - consuming : 3
20、生产者消费者模型的作用是什么
这个问题很理论,但是很重要:
(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约
在现实中你解决的许多线程问题都属于生产者消费者模型,就是一个线程生产任务供其它线程进行消费,
你必须知道怎么进行线程间通信来解决这个问题。
比较低级的办法是用wait和notify来解决这个问题,比较赞的办法是用或者 BlockingQueue来实现生产者消费者模型
21、Java中Semaphore是什么?
Java中的Semaphore是一种新的同步类,它是一个计数信号。
从概念上讲,信号量维护了一个许可集合。
如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。
每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。
但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动
。- 信号量常常用于多线程的代码中,比如数据库连接池
- 示例代码:
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
Semaphore binary = new Semaphore(1);
public static void main(String args[]) {
final SemaphoreTest test = new SemaphoreTest();
new Thread(){
@Override
public void run(){
test.mutualExclusion();
}
}.start();
new Thread(){
@Override
public void run(){
test.mutualExclusion();
}
}.start();
}
private void mutualExclusion() {
try {
binary.acquire();
//mutual exclusive region
System.out.println(Thread.currentThread().getName() + " inside mutual exclusive region");
Thread.sleep(1000);
} catch (InterruptedException ie) {
ie.printStackTrace();
} finally {
binary.release();
System.out.println(Thread.currentThread().getName() + " outside of mutual exclusive region");
}
}
}
Output:
Thread-0 inside mutual exclusive region
Thread-0 outside of mutual exclusive region
Thread-1 inside mutual exclusive region
Thread-1 outside of mutual exclusive region
22、Semaphore有什么作用
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。
Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,
如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。
由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了
23、sleep方法和wait方法有什么区别
这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,
不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
24、Java中notify 和 notifyAll有什么区别?
为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。
而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
25、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁
26、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别
wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:
wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
27、什么是ThreadLocal变量?
简单说ThreadLocal就是一种以空间换时间的做法,
在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了
ThreadLocal是Java里一种特殊的变量。
每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。
它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,
因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。
- 首先,通过复用减少了代价高昂的对象的创建个数。
- 其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
- 线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。
- 示例代码:
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
*
* @author
*/
public class ThreadLocalTest {
public static void main(String args[]) throws IOException {
Thread t1 = new Thread(new Task());
Thread t2 = new Thread( new Task());
t1.start();
t2.start();
}
/*
* Thread safe format method because every thread will use its own DateFormat
*/
public static String threadSafeFormat(Date date){
DateFormat formatter = PerThreadFormatter.getDateFormatter();
return formatter.format(date);
}
}
/*
* Thread Safe implementation of SimpleDateFormat
* Each Thread will get its own instance of SimpleDateFormat which will not be shared between other threads. *
*/
class PerThreadFormatter {
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = new ThreadLocal<SimpleDateFormat>() {
/*
* initialValue() is called
*/
@Override
protected SimpleDateFormat initialValue() {
System.out.println("Creating SimpleDateFormat for Thread : " + Thread.currentThread().getName());
return new SimpleDateFormat("dd/MM/yyyy");
}
};
/*
* Every time there is a call for DateFormat, ThreadLocal will return calling
* Thread's copy of SimpleDateFormat
*/
public static DateFormat getDateFormatter() {
return dateFormatHolder.get();
}
}
class Task implements Runnable{
@Override
public void run() {
for(int i=0; i<2; i++){
System.out.println("Thread: " + Thread.currentThread().getName() + " Formatted Date: " + ThreadLocalTest.threadSafeFormat(new Date()) );
}
}
}
Output:
Creating SimpleDateFormat for Thread : Thread-0
Creating SimpleDateFormat for Thread : Thread-1
Thread: Thread-1 Formatted Date: 30/05/2012
Thread: Thread-1 Formatted Date: 30/05/2012
Thread: Thread-0 Formatted Date: 30/05/2012
Thread: Thread-0 Formatted Date: 30/05/2012
28、线程池
- 什么是线程池:
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。
为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。
- 线程池作用:
避免频繁地创建和销毁线程,达到线程对象的重用。
另外,使用线程池还可以根据项目灵活地控制并发的数目。
- Executor:
从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。
比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)
29、如果你提交任务时,线程池队列已满,这时会发生什么
如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,
继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;
如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,
ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
30、Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,
execute()方法的返回类型是void,
它定义在Executor接口中,- 而submit()方法可以返回持有计算结果的Future对象,
- 它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
31、什么是FutureTask?
在Java并发程序中FutureTask表示一个可以取消的异步运算。
- 它有启动和取消运算、查询运算是否完成和取回运算结果等方法。
- 只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。
FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
- 一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,
- 由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中,它可以提交给Executor来执行。
32、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,
因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,
和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
- (3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,
- 看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。
- 最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
33、多线程最佳实践
- 1.给你的线程起个有意义的名字
-
这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,
给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
2.避免锁定和缩小同步的范围
-
锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。
因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
3.多用同步类少用wait 和 notify
-
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,
而用wait和notify很难实现对复杂控制流的控制。
其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,
使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
4.多用并发集合少用同步集合
-
这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。
如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。