面试总结-并发篇
说说java中实现多线程有几种方法?
创建线程的常用三种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(jdk1.5以上)
- 线程池方式创建
通过继承Thread类或者实现Runnable接口、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法返回值,可以声明抛出异常而已。因此将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下:
继承Thread类:编写简单,访问当前线程直接使用this,无需使用Thread.currentThread()方法。但因为java是单继承的,如果线程类已经继承了Thread类,就不能再继承其他父类了。
实现Runnable或Callable接口:线程类只是实现了接口,还可以继承其他类。这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将cpu、代码、和数据分开,形成清晰的模型,较好的体现了面向对象的思想。但是编程稍微复杂,如需访问当前线程,必须使用Thread.currentThread()方法
如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend以及resume一样都是过期作废的方法。
3、使用interrupt方法中断线程。
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
System.out.println("Stopping application...");
}
}
notify()和notifyAll()有什么区别?
notify可能会导致死锁,而notifyAll不会
任何时候只有一个线程可以获取锁,也就是说只有一个线程可以运行synchronixed中的代码,使用notifyAll可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
wait()应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行
notify()是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中。
sleep()和wait()有什么区别?
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法是属于object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
volatile是什么?可以保证有序性吗?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
2. 禁止禁止指令重排序
什么叫保证部分有序性?
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
如果flag变量是volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
使用volatile一般用于状态标记量和单例模式的双检锁。
为什么wait,notify和notifyAll这些方法不在thread类里面?
明显的原因是java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在object类中因为锁属于对象。
为什么wait和notify方法要在同步块中调用?
- 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait,notify和notifyAll方法
- 如果你不这么做,你的代码会抛出IIegalMonitorStateException异常
- 还有一个原因是为了避免wait和notify之间产生竞态条件。
wait方法强制当前线程释放对象锁。这意味着在调用某对象的wait方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait方法。notify和notifyAll方法同理。
调用wait方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify和notifyAll方法的原因通常是,调用线程希望告诉其他等待中的线程:特殊状态已被设置。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
java中interrupted和isinterruptedd方法的区别?
interrupted和isinterrupted的注意区别是前者会将中断状态清除而后者不会。java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出interruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有可能被其它线程调用中断来改变。
java中synchronized和ReentrantLock有什么不同?
相似点:
它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentreantLock它是jdk1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1;当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另外一个线程释放为止。由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供一些高级功能,主要有以下3项:
- 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相对于Synchronized来说可以避免出现死锁的情况。
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设置为公平锁,但公平锁表现的性能不是很好。
- 锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。
有三个线程T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join方法在一个线程中启动另外一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
实际上先启动三个线程中哪一个都行,因为在每个线程的run方法中用join方法限定了三个线程的执行顺序。
public class JoinTest2 {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();//这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}
SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap和Hashtable一样,实际上在调用map所有方法时,都对整个map进行同步,所以只要有一个线程访问map,其他线程就无法进入map。而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以如果一个线程在访问ConcurrentHashMap某个桶时,其他线程仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap更加有优势。同时,同步操作精确控制到桶,这样即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException.
Thread类中的yield方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,它是一个静态方法而且只能保证当前线程放弃cpu占用而不能保证其它线程一定能占用cpu,执行yield方法的线程可能在进入到暂停状态后马上又被执行。