1.什么是线程池?
线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
如果用生活中的列子来说明,我们可以把线程池当做一个客服团队,如果同时有1000个人打电话进行咨询,按照正常的逻辑那就是需要1000个客服接听电话,服务客户。现实往往需要考虑到很多层面的东西,比如:资源够不够,招这么多人需要费用比较多。正常的做法就是招100个人成立一个客服中心,当有电话进来后分配没有接听的客服进行服务,如果超出了100个人同时咨询的话,提示客户等待,稍后处理,等有客服空出来就可以继续服务下一个客户,这样才能达到一个资源的合理利用,实现效益的最大化。
execute 和 submit的区别?
execute实用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了,而submit方法实用于需要关注返回值的场景。
五种线程池的使用场景
-
newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
-
newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
-
newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
-
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
-
newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
线程池的关闭
关闭线程池可以调用shutdownNow和shutdown两个方法来实现
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开开始执行的任务全部取消,并且返回还没开始的任务列表
shutdown:当我们使用shutdown后,线程池将不再接受新的任务,但也不会强制终止已经提交或者正在执行的任务
2.实现多线程有哪几种方式?
1)实现Runnable接口,并且实现该接口的run()方法。
自定义类并实现Runnable接口,实现run()方法
创建Thread对象,用实现Runnable接口的对象作为参数实例化该Thread对象
调用Thread的start()方法
class MyThread implement Runnable{
public void run(){//重写run方法
System.out.println("Thread body");
}
}
public class Test{
public static void main(String[] args){
MyThread thread = new MyThread();
Thread t = new Thread(thread);
t.start();//开启线程
}
}
2)继承Thread类,重写run方法
继承Thread,重写run()方法
创建一个MyThread实例
执行start()方法
class MyTread extends Thread{
public void run(){
System.out.println("Thread body");
}
}
public class Test{
public static void main(String[] args){
MyThread thread = new MyThread();
thread.start();
}
}
3)实现Callable接口,重写call()方法
Callable对象实际是属于Executor框架中的功能类,Callable接口与Runnable接口类似,但是提供了比Runnable更强大的功能,主要表现以下三点:
Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能。
Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。
运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果。它提供了检查计算是否完成的方法。由于线程属于异步计算模型,所以无法从其他线程中得到方法的返回值,在这种情况下,就可以使用Futrue来监视目标线程调用call()方法的情况,当调用Futrue的get()方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。
总结:在以上三种方式中,前两种方式线程执行完之后都没有返回值,只有最后一种是带返回值的。但需要实现多线程时,一般推荐实现Runnable接口的方式,原因如下:首先,Thread类定义了多种方法可以被派生类使用或重新,但是只有run()方法是必须被重写的,在run方法中实现这个线程的主要功能。这当然是实现Runnable接口所需的同样的方法。而且,很多java开发人员认为,一个类仅在他们需要被加强或修改时才会被继承。因此,如果没有必要重写Thread类中的其他方法,那么通过继承Thread的实现方式与实现Runnable接口的效果相同,在这种情况下最好通过实现Runnable接口方式来创建线程。
3. sleep() 和 wait() 的区别
sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。
因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
综上所述:
sleep() 和 wait() 的区别就是调用sleep方法的线程不会释放对象锁,而调用wait()方法会释放对象锁
4.要想使i++线程安全,应该怎么做(不仅仅限于synchronized修饰)
volatile解决了线程间共享变量的可见性问题
使用volatile会增加性能开销
volatile并不能解决线程同步问题
解决i++或者++i这样的线程同步问题需要使用synchronized或者AtomicXX系列的包装类,同时也会增加性能开销。
5.单例模式下的多线程会有什么后果?
(1)单例模式的懒汉式[线程不安全,不可用]
public class Singleton {
private static Singleton instance=null;
private Singleton() {};
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
这种方式是在调用getInstance方法的时候才创建对象的,所以它就被称为懒汉模式。
这是存在线程安全问题的,那具体是存在怎样的线程安全问题?怎样导致这种问题的?好,我们来说一下什么情况下这种写法会有问题。在运行过程中可能存在这么一种情况:多个线程去调用getInstance方法来获取Singleton的实例,那么就有可能发生这样一种情况,当第一个线程在执行if(instance==null)时,此时instance是为null的进入语句。在还没有执行instance=new Singleton()时(此时instance是为null的)第二个线程也进入了if(instance==null)这个语句,因为之前进入这个语句的线程中还没有执行instance=new Singleton(),所以它会执行instance = new Singleton()来实例化Singleton对象,因为第二个线程也进入了if语句所以它会实例化Singleton对象。这样就导致了实例化了两个Singleton对象。所以单例模式的懒汉式是存在线程安全的,既然它存在问题,那么可能有解决办法,于是就有下面加锁这种写法。
(2)懒汉式线程安全的[线程安全,效率低不推荐使用]
public class Singleton {
private static Singleton instance=null;
private Singleton() {};
public static synchronized Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
缺点:效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面想获得该实例,直接return就行了。方法进行同步效率太低要改进。
(3)单例模式懒汉式[线程不安全,不可用]
public class Singleton7 {
private static Singleton instance=null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
这种写法也是不安全的,当一个线程还没有实例化Singleton时另一个线程执行到if(instance == null)这个判断时语句机会进入if语句,虽然加了锁,但是等到第一个线程执行完instance=new Singleton()跳出这个锁时,另一个进入if语句的线程同样会实例化另外一个SIngleton对象。因为这种改进方法不可行。
6.servlet是线程安全吗?
servlet不是线程安全的。
要解释为什么Servlet不是线程安全的,需要了解一下Servlet容器(即Tomcat)如何响应HTTP请求的。
当Tomcat接受到Client的HTTP请求时,Tomcat从线程池取出一个线程,之后找到该请求对应的Servlet对象并且进行初始化,之后调用service()方法。要注意的是每一个Servlet对象再Tomcat容器中只有一个实力对象,即是单例模式。如果多个HTTP请求的是同一个Servlet,那么这两个HTTP请求对应的线程将并发调用Servlet的service()方法。
如果Thread1和Thread2调用了同一个Servlet1,此时如果Servlet1中定义了实例变量或静态变量,那么可能会发生线程安全问题(因为所有的线程都可能使用这些变量)。