一、多线程基础(多线程的创建方式,线程生命周期,死锁)
1、线程和进程
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。都拥有单独的栈内存用来存储本地数据。
2、线程生命周期
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
3、死锁的条件
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
4、死锁与活锁的区别,死锁与馅饼的区别?
活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
5、三种实现线程方式
继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程以及实现Callable接口(有返回值)
6、run方法和start方法区别(略)
7、竞态条件
竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。如当其中一个线程需要根据某个变量的状态来相应执行某个操作的之前,该变量很可能已经被其它线程修改。这种bugs很难发现而且会重复出现,因为线程间的随机竞争。
8、同步块锁
同步块
锁
同步块和锁的区别
1)相比synchronized,锁更灵活。
2)支持tryLock()方法,没有获取锁是可继续执行不阻塞
3)支持读写分离,多个线程读一个线程写reentrantreadwritelock
4)Lock性能更好
二、多线程对象
1、 Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?volatile变量怎样在并发环境中确保可见性、顺序性和一致性?
每次读取volatile的变量时都要从它的内存地址中读取,volatile只提供了内存可见性,而没有提供原子性,volatile实用于一个线程修改变量,多个线程读取变量的地方
2、volatile 变量和 atomic 变量有什么不同?
这是个有趣的问题。首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加1,其它数据类型和引用变量也可以进行相似操作。
3、什么是ThreadLocal变量?
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个ThreadLocalMap变量。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。
public static DateFormat getDateFormat() {
DateFormat df = (DateFormat) threadLocal.get();
if (df == null) {
df = new SimpleDateFormat("yyyy-MM-dd");
threadLocal.set(df);
}
return df;
}
三、多线程方法。(wait/notify/sleep/yield/join/setPriority/setDaemon)及由此引发的面试难题。wait和sleep不同是在等待时wait会释放锁,而sleep释放线程但一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。线程一旦wait,只有等其他线程notify通知后才能重新获取锁,notify不会释放锁,会继续执行,一直到wait。sleep/yield/join(阻塞主线程,执行调用线程,主要作用是同步)是thread的方法
如何停止一个线程
Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。
wait的使用
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(); //b线程释放锁,c线程可以执行
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
logger.debug("producing : " + i);
sharedQ.add(i);
sharedQ.notify();//换起b线程的wait位置,b开始竞争线程 ,只是唤起,不会执行,只有当当前线程wait后才会执行
}
}
}
}
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(); //c线程释放锁,b线程可以执行
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
int number = sharedQ.poll();
logger.debug("consuming : " + number );
sharedQ.notify();//换起b线程的wait位置,b开始竞争线程 ,只是唤起,不会执行,只有当当前线程wait后才会执行
//termination condition
if(number == 3){break; }
}
}
}
}
主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件
四、面试题
1)T1、T2、T3三个线程依次执行?使用join;
T3 t3= t.new T3("T3");
t3.start();//启动t3线程
t3.join();//阻塞主线程,执行完t3再返回
T2 t2= t.new T2("T2");
t2.start();//启动t3线程
t2.join();//阻塞主线程,执行完t2再返回
T1 t1= t.new T1("T1");
t1.start();//启动t3线程
t1.join();//阻塞主线程,执行完t1再返回
四个线程 A B C D,其中 D 要等到 A B C 全执行完毕后才执行,而且 A B C 是同步运行的。CountdownLatch 三个运动员各自准备,等到三个人都准备好后,再一起跑。CycliBarriar 2)在Java中CycliBarriar和CountdownLatch有什么区别? CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。与 CyclicBarrier 不同的是,CountdownLatch 不能重新使用 创建子线程来做一些耗时任务,怎么把执行结果回传给主线程使用? Callable 两个线程按照指定方式有序交叉运行。 wait() notify() ; 3)在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它? lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。 4)用Java编程一个会导致死锁的程序,你将怎么解决? 这是我最喜欢的Java线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有N 个资源和N个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n可以替换为2,越大的数据会使问题看起来更复杂。通过避免Java中的死锁来得到关于死锁的更多信息。
5) 什么是原子操作,Java中的原子操作是什么,怎么同步一个原子操作?
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指对于方位同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
6) 什么是不可变对象,它对写并发应用有什么帮助?
如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变为什么String是不可变:value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。
BigDecimal:是不可变的,BigDecimal加减乘除最终都返回的是一个新的BigDecimal对象,c=c.add(d)。顺便说说BigDecimal中的bug,1、初始化尽量使用String类型的构造,2、比较不能用equals而是compareTo,3、除不尽,养成习惯去设置精度负责抛异常,.divide(new BigDecimal("12").setScale(2, BigDecimal.ROUND_HALF_DOWN)。
7) 什么是线程安全?Vector是一个线程安全类吗? (详见这里)
进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。
8) 如何在两个线程间共享数据?
1、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有一个共享变量,Runnable对象都可以访问。
2、如果代码不同,将Runnable对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥。
9) 如果同步块内的线程抛出异常会发生什么?
无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在finally block里释放锁实现。
10) 多线程最佳实践
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。
11) 在线程中你怎么处理不可捕捉异常?
java为我们提供了一种线程内发生异常时能够在线程代码边界之外处理异常的回调机制,即Thread对象提供的setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法。通过该方法给某个thread设置一个UncaughtExceptionHandler,可以确保在该线程出现异常时能通过回调UncaughtExceptionHandler接口的public void uncaughtException(Thread t, Throwable e) 方法来处理异常,这样的好处或者说目的是可以在线程代码边界之外(Thread的run()方法之外),有一个地方能处理未捕获异常。但是要特别明确的是:虽然是在回调方法中处理异常,但这个回调方法在执行时依然还在抛出异常的这个线程中!另外还要特别说明一点:如果线程是通过线程池创建,线程异常发生时UncaughtExceptionHandler接口不一定会立即回调。
ThreadException implements Thread.UncaughtExceptionHandler;
thread.setUncaughtExceptionHandler(new ThreadException())