面试系列(六):多线程
差点把多线程给忘了。。。。 多线程基本上去每个公司面试都会问到……
1、谈谈你对多线程的理解
线程:表示程序的执行流程,是CPU调度执行的基本单位
多线程:指的是一个程序(一个进程)运行时产生了不止一个线程,使用多线程的好处,在于并行的执行多任务,彼此独立,可以提高执行效率。
2、实现多线程的方式
在java中实现多线程有多种途径:继承Thread类,实现Runnable接口,实现Callable接口,线程池负责创建。
一个线程对象只能启动一个线程,无论你调用多少遍start()方法,结果只有一个线程。
Thread.start()方法(native)启动线程,使之进入就绪状态,当cpu分配时间该线程时,由JVM调度执行run()方法。 (调用start时不一定立即执行)
比较推荐实现Runnable接口的方式,原因如下:
(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。 (可联想到模拟火车站卖票的例子)
(2)可以避免由于Java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。 (单继承多实现)
(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象时,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。
3、线程的状态
1)6种状态
新建(New)---使用new来新建一个线程
可运行(Runnable)----调用start()方法,线程处于运行或可运行状态
阻塞(Blocked)---线程需要获得内置锁,当该锁被其他线程使用时,此线程处于阻塞状态
等待(Waiting)---当线程等待其他线程通知调度表可以运行时,此时线程处于等待状态
计时等待(Timed Waiting)---当线程调用含有时间参数的方法(如sleep())时,线程可进入计时等待状态
终止(Terminated)--当线程的run()方法结束或者出现异常时,线程处于终止状态
2)sleep和wait的区别?
sleep()方法是属于Thread类中的; 而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu,一般wait不会加时间限制,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
4、线程的安全
1)synchronized关键字是多线程并发环境的执行有序性的方式之一,当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。
2)成员(全局)变量的类用于多线程时是不安全的,不安全体现在这个成员变量可能发生非原子性的操作,而变量定义在方法内也就是局部变量是线程安全的。
3)生产--消费者模式
其实是一种很经典的线程同步模型,很多时候,并不是光保证多个线程对某共享资源操作的互斥性就够了,往往多个线程之间都是有协作的。
class Plate {
List<Object> eggs = new ArrayList<Object>();
public synchronized Object getEgg() {
while(eggs.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
}
}
Object egg = eggs.get(0);
eggs.clear();// 清空盘子
notify();// 唤醒阻塞队列的某线程到就绪队列
System.out.println("拿到鸡蛋");
return egg;
}
public synchronized void putEgg(Object egg) {
while(eggs.size() > 0) {
try {
wait();
} catch (InterruptedException e) {
}
}
eggs.add(egg);// 往盘子里放鸡蛋
notify();// 唤醒阻塞队列的某线程到就绪队列
System.out.println("放入鸡蛋");
}
}
class AddThread extends Thread{
private Plate plate;
private Object egg=new Object();
public AddThread(Plate plate){
this.plate=plate;
}
public void run(){
for(int i=0;i<5;i++){
plate.putEgg(egg);
}
}
}
class GetThread extends Thread{
private Plate plate;
public GetThread(Plate plate){
this.plate=plate;
}
public void run(){
for(int i=0;i<5;i++){
plate.getEgg();
}
}
}
测试下:
public static void main(String args[]){
try {
Plate plate=new Plate();
Thread add=new Thread(new AddThread(plate));
Thread get=new Thread(new GetThread(plate));
add.start();
get.start();
add.join();
get.join();//等到取和拿线程执行完毕后再继续往下执行System.out.println("测试结束");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("测试结束");
}
打印结果:
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
测试结束
4)显示地调用Lock(实现类比如ReentrantLock)
Lock bankLock = new ReentrantLock();
bankLock.lock();
//....
bankLock.unlock();//通常在finally里释放锁
5)ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
从线程的角度看,这个变量就像是线程的本地变量。
public class ThreadLocalTest {
public static void main(String [] args) {
SequenceNumber sn = new SequenceNumber();
// ③ 3个线程共享sn,各自产生***
TestClient tc1 = new TestClient(sn);
TestClient tc2 = new TestClient(sn);
TestClient tc3 = new TestClient(sn);
tc1.start();
tc2.start();
tc3.start();
}
}
class SequenceNumber {
// ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
public Integer initialValue() {
return 0;
}
};
public int getNextNum() {
seqNum.set(seqNum.get()+1);
return seqNum.get();
}
}
class TestClient extends Thread {
private SequenceNumber sn;
public TestClient(SequenceNumber sn) {
this.sn = sn;
}
public void run() {
// ④每个线程打出3个序列值
for(int i = 0; i<3; i++) {
System.out.println("thread[" + Thread.currentThread().getName() + "] sn[" + sn.getNextNum() +"]");
}
}
}
结果:
thread[Thread-1] sn[1]
thread[Thread-0] sn[1]
thread[Thread-2] sn[1]
thread[Thread-0] sn[2]
thread[Thread-1] sn[2]
thread[Thread-0] sn[3]
thread[Thread-2] sn[2]
thread[Thread-1] sn[3]
thread[Thread-2] sn[3]
5、高并发
注:以下部分不要求全部答到,可以选择一个点答就行~
1)数据结构
java.util.concurrent包中提供了一些适合多线程程序使用的高性能数据结构,包括队列和集合类对象等。
1、队列
a、BlockingQueue接口:线程安全的阻塞式队列;当队列已满时,向队列添加会阻塞;当队列空时,取数据会阻塞。(非常适合消费者-生产者模式)
阻塞方式:put()、take()。
非阻塞方式:offer()、poll()。
实现类:基于数组的固定元素个数的ArrayBolockingQueue和基于链表结构的不固定元素个数的LinkedBlockQueue类。
b、BlockingDeque接口: 与BlockingQueue相似,但可以对头尾进行添加和删除操作的双向队列;方法分为两类,分别在队首和对尾进行操作。
实现类:标准库值提供了一个基于链表的实现,LinkedBlockgingDeque。
2、集合类
在多线程程序中,如果共享变量是集合类的对象,则不适合直接使用java.util包中的集合类。这些类要么不是线程安全,要么在多线程下性能比较差。
应该使用java.util.concurrent包中的集合类。
a、ConcurrentMap接口: 继承自java.util.Map接口
putIfAbsent():只有在散列表不包含给定键时,才会把给定的值放入。
remove():删除条目。
replace(key,value):把value 替换到给定的key上。
replace(key, oldvalue, newvalue):CAS的实现。
实现类:ConcurrentHashMap(若干个segements,每个segement都有自己的锁,常见的HashMap可以看作只有一个segement的ConcurrentHashMap):
创建时,如果可以预估可能包含的条目个数,可以优化性能。(因为动态调整所能包含的数目操作比较耗时,这个HashMap也一样,只是多线程下更耗时)。
创建时,预估进行更新操作的线程数,这样实现中会根据这个数把内部空间划分为对应数量的部分。(默认是16,如果只有一个线程进行写操作,其他都是读取,那么把值设为1 可以提高性能)。
注:当从集合中创建出迭代器遍历Map元素时,不一定能看到正在添加的数据,只能和集合保证弱一致性。(当然使用迭代器不会因为查看正在改变的Map,而抛出java.util.ConcurrentModifycationException)
b、CopyOnWriteArrayList接口:继承自java.util.List接口。
是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略;
顾名思义,在CopyOnWriteArrayList的实现类,所有对列表的更新操作都会新创建一个底层数组的副本,并使用副本来存储数据;对列表更新操作加锁,读取操作不加锁。
适合多读取少修改的场景,如果更新操作多,那么不适合用,同样迭代器只能表示创建时列表的状态,更新后使用了新的底层数组,迭代器还是引用旧的底层数组。
2)多线程任务的执行
过去线程的执行,是先创建Thread类,再调用start方法启动,这种做法要求开发人员对线程进行维护,在线程较多时,一般创建一个线程池同一管理,同时降低重复创建线程的开销
在J2SE5.0中,java.util.concurrent包提供了丰富的用来管理线程和执行任务的实现。
1、基本接口(描述任务)
a、Callable接口:
Runnable接口受限于run方法的类型签名,而Callable只有一个方法call(),可以有返回值,可以抛出受检异常。
b、Future接口:
过去,需要异步线程的任务执行结果,要求主线程和任务执行线程之间进行同步和数据传递。
Future简化了任务的异步执行,作为异步操作的一个抽象。调用get()方法可以获取异步的执行结果,如果任务没有执行完,会等待,直到任务完成或被取消,cancel()可以取消。
——Callable(一个产生结果)和Future(一个拿到结果)。
FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值:
public class CallableAndFuture {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
return new Random().nextInt(100);
}
};
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
try {
Thread.sleep(5000);// 可能做一些事情
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
下面来看另一种方式使用Callable和Future,通过ExecutorService的submit方法执行Callable,并返回Future,代码如下
public class CallableAndFuture {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<Integer> future = threadPool.submit(new Callable<Integer>() {
public Integer call() throws Exception {
return new Random().nextInt(100);
}
});
try {
Thread.sleep(5000);// 可能做一些事情
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
c、Delayed接口:
延迟执行任务,getDelay()返回当前剩余的延迟时间,如果不大于0,说明延迟时间已经过去,应该调度并执行该任务。
2、组合接口(描述任务)
a、RunnableFuture接口:继承自Runnable接口和Future接口。
当来自Runnalbe接口中的run方法成功执行之后,相当于Future接口表示的异步任务已经完成,可以通过get()获取运行结果。
b、ScheduledFuture接口:继承Future接口和Delayed接口,表示一个可以调用的异步操作。
c、RunnableScheduledFuture接口:继承自Runnable、Delayed和Future,接口中包含isPeriodic,表明该异步操作是否可以被重复执行。
3、Executor接口、ExcutorServer接口、ScheduleExecutorService接口和CompletionService接口(描述任务执行)
a、executor接口,execute()用来执行一个Runnable接口的实现对象,不同的Executor实现采取不同执行策略,但提供的任务执行功能比较弱。
b、excutorServer接口,继承自executor;
提供了对任务的管理:submit(),可以吧Callable和Runnable作为任务提交,得到一个Future作为返回,可以获取任务结果或取消任务。
提供批量执行:invokeAll()和invokeAny(),同时提交多个Callable;invokeAll(),会等待所有任务都执行完成,返回一个包含每个任务对应Future的列表;invokeAny(),任何一个任务成功完成,即返回该任务结果。
提供任务关闭:shutdown()、shutdownNow()来关闭服务,前者不允许新的任务提交,后者试图终止正在运行和等待的任务,并返回已经提交单没有被运行的任务列表。(两个方法都不会等待服务真正关闭,只是发出关闭请求。)。shutdownDow,通常做法是向线程发出中断请求,所以确保提交的任务实现了正确的中断处理逻辑。
c、ScheduleExecutorService接口,继承自excutorServer接口:支持任务的延迟执行和定期执行,可以执行Callable或Runnable。
schedule(),调度一个任务在延迟若干时间之后执行;
scheduleAtFixedRate():在初始延迟后,每隔一段时间循环执行;在下一次执行开始时,上一次执行可能还未结束。(同一时间,可能有多个)
scheduleWithFixedDelay:同上,只是在上一次任务执行完后,经过给定的间隔时间再开始下一次执行。(同一时间,只有一个)
以上三个方法都返回ScheduledFuture接口的实现对象。
d、CompletionService接口,共享任务执行结果。
通常在使用ExecutorService接口,通过submit提交任务,并得到一个Future接口来获取任务结果,如果任务提交者和执行结果的使用者是程序的不同部分,那就要把Future在不同部分进行传递;而CompletionService就是解决这个问题,程序不同部分可以共享CompletionService,任务提交后,执行结果可以通过take(阻塞),poll(非阻塞)来获取。
标准库提供的实现是 ExecutorCompletionService,在创建时,需要提供一个Executor接口的实现作为参数,用来实际执行任务。
6、线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
用法举例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(index);
}
});
}
ps: Servlet是线程安全的么?
Servlet是单实例多线程的。
Servlet不是线程安全的。
要解释为什么Servlet为什么不是线程安全的,需要了解Servlet容器(即Tomcat)是如何响应HTTP请求的。
当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,之后找到该请求对应的Servlet对象并进行初始化,然后调用service()方法。要注意的是每一个Servlet对象在Tomcat容器中只有一个实例对象,即是单例模式。如果多个HTTP请求请求的是同一个Servlet,那么这两个HTTP请求对应的线程将并发调用Servlet的service()方法。
上一篇: PCL点云特征描述与提取