1、线程的4种创建方法的优缺点和选择
1、继承Thread
每次new Thread新建对象性能差。
线程缺乏统一管理,可能无限制新建线程,相互之间竞争及可能占用过多系统资源导致死机或 oom。
缺乏更多功能,如定时执行、定期执行、线程中断。
单根继承,一般创建线程对象时不建议使用extends Thread方法
2、实现Runnable接口
一般没有返回结果时建议使用Runnable接口
3、使用Callable和Future接口创建线程
一般有返回值一般建议使用Callable接口
4、线程池
重用存在的线程,减少对象创建、消亡的开销,性能佳
可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
提供定时执行、定期执行、单线程、并发数控制等功能。
如果使用线程比较频繁建议使用线程池
2、线程池的种类和优缺点
①newCachedThreadPool
特点:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,
它可以灵活的回收空闲的线程,当需要增加时,它可以灵活的添加新的线程,而不会对池的长度作任何限制
缺点:它虽然可以无限的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置
为Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去
重写一个方法限制一下这个最大值
总结:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次
新建线程
②newFixedThreadPool
特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系
统资源进行设置。
缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易
导致OOM(超出内存空间)
总结:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。
如Runtime.getRuntime().availableProcessors()
③newScheduledThreadPool
特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于
Timer(Timer是Java的一个定时器类)
缺点:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,
前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
④newSingleThreadExecutor
特点:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,
那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定
顺序(FIFO, LIFO, 优先级)执行。
缺点:缺点的话,很明显,他是单线程的,高并发业务下有点无力
总结:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它
3、线程池的参数以及含义
1.corePoolSize
线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,
直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提
交的任务被保存到阻塞队列中,等待被执行。
2.maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建
新的线程执行任务,前提是当前线程数小于maximumPoolSize。
3.keepAliveTime
线程空闲时的存活时间。默认情况下,只有当线程池中的线程数大于corePoolSize
时,keepAliveTime才会起作用,如果一个线程空闲的时间达到keepAliveTime,则
会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了
allowCoreThreadTimeOut(boolean)方法,keepAliveTime参数也会起作用,直到
线程池中的线程数为0。
4.unit
keepAliveTime参数的时间单位。
例如TimeUnit.DAYS
5.workQueue
任务缓存队列,用来存放等待执行的任务。如果当前线程数为corePoolSize,继续提交的
任务就会被保存到任务缓存队列中,等待被执行。
一般来说,这里的BlockingQueue有以下三种选择:
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作
,否则插入操作一直处于阻塞状态。因此,如果线程池中始终没有空闲线程(任务提交的平均速度快于
被处理的速度),可能出现无限制的线程增长。
- LinkedBlockingQueue:基于链表结构的阻塞队列,如果不设置初始化容量,其容量
Integer.MAX_VALUE,即为无界队列。因此,如果线程池中线程数达到了corePoolSize,且始终没有
空闲线程(任务提交的平均速度快于被处理的速度),任务缓存队列可能出现无限制的增长。
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO(先进先出)排序任务。
6.threadFactory
线程工厂,创建新线程时使用的线程工厂。
7.handler
任务拒绝策略,当阻塞队列满了,且线程池中的线程数达到maximumPoolSize,如果继续提交任务,
就会采取任务拒绝策略处理该任务,线程池提供了4种任务拒绝策略:
- AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
- CallerRunsPolicy:由调用execute方法的线程执行该任务;
- DiscardPolicy:丢弃任务,但是不抛出异常;
- DiscardOldestPolicy:丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)。
当然也可以根据应用场景实现RejectedExecutionHandler接口自定义饱和策略,如记录日志或持久
化存储不能处理的任务。
4、概述线程的生命周期
线程的生命周期包含5个阶段,包括新建,就绪,运行,阻塞,销毁。
新建:就是使用new方法,new出来的线程;
就绪:调用的线程的start()方法后,线程处于等待CPU分配资源阶段,谁先抢到CPU资源,谁开始执行;
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
阻塞:在运行状态的时候可能因为某些原因导致运行状态的线程变成了阻塞状态。比如sleep(),wait(),之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。wait超过时间后,进入锁池,同步锁释放后进入就绪状态。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态。
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。
5、概述volatile关键字的含义以及如何使用
一旦一个共享变量被volatile修饰,具备了两层含义
1、保证了不同进程间的可见性,即一个线程修改了该变量,那么该变量所在cpu线程缓存变量全部失效,读取缓存的
数据只能去主线程去获取,这就是线程间的可见性
2、禁止对其进行重排序,也就是保证了有序性
3、并未保证原子性操作
被volatile修饰的变量,一个线程一旦修改其值,其他线程都能看见
6、synchronized和Lock之间的区别
1.synchronized是java中的关键字,而Lock是一个接口;
2.synchronized是隐式的加锁(超出作用范围自动释放),lock是手动显式的加锁,手动的释放锁;
3.synchronized可以作用于方法上,和代码块上;而lock只能作用于方法块;
4.synchronized底层采用的是objectMonitor,lock采用的AQS;
5.synchronized是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁;
6.synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列;
7.synchronized只支持非公平锁,lock支持非公平锁和公平锁(构造方法中加true);
8.synchronized使用了object类的wait和notify进行等待和唤醒, lock使用了condition接口进行等待和唤醒(await和signal);
9.lock支持个性化定制, 使用了模板方法模式,可以自行实现lock方法;
10.synchronized发生异常时,会自动的释放线程占有的锁,而lock锁没有显示的释放锁,则可能会造成死锁,因此lock的锁释放需要在finally中。
7、生产者消费者模式手写编程的两种实现
public class Basket1 {
private volatile Object obj = null;
public synchronized void produce(Object obj) {
while (this.obj != null)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.obj = obj;
this.notifyAll();
System.out.println("生产了一个对象:" + obj);
}
public synchronized void consume() {
while (this.obj == null)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费了一个对象:" + obj);
this.obj = null;
this.notifyAll();
}
}
import java.util.Date;
public class Test1 {
public static void main(String[] args) {
Basket1 basket = new Basket1();
Thread t1 = new Producer(basket);
Thread t2 = new Consumer(basket);
t1.start();
t2.start();
}
}
class Producer extends Thread {
private Basket1 basket;
public Producer(Basket1 basket) {
this.basket = basket;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
basket.produce(new Date());
}
}
}
class Consumer extends Thread {
private Basket1 basket;
public Consumer(Basket1 basket) {
this.basket = basket;
}
@Override
public void run() {
for(int i=0;i<20;i++)
basket.consume();
}
}
import java.util.Date;
public class Test2 {
public static void main(String[] args) {
Basket2 basket = new Basket2();
Thread t1 = new Producer2(basket);
Thread t2 = new Consumer2(basket);
t1.start();
t2.start();
}
}
class Producer2 extends Thread {
private Basket2 basket;
public Producer2(Basket2 basket) {
this.basket = basket;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
basket.produce(new Date());
}
}
}
class Consumer2 extends Thread {
private Basket2 basket;
public Consumer2(Basket2 basket) {
this.basket = basket;
}
@Override
public void run() {
for(int i=0;i<20;i++)
basket.consume();
}
}
8、synchronized实现原理
在添加synchronized关键字后就可以保证在一个时刻上只有一个线程在调用某个方法或者代码块,不会出现并发的情形,达到排队执行的效果。
在Java中synchronized可保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),这点确实也是很重要的。
9、是否可以直接调用线程对象的run方法,为什么
不能
new一个Thread,线程就进入了新建状态。当调用start()方法的时候,会启动一个线程并使线程进入就绪状态。
当分配到时间片后就可以开始运行。
start()方法会执行线程的相应准备工作,然后自动调用run()方法的内容,这才是多线程的工作模式。
但是,直接执行run()方法的话,会把run()方法当做main()线程下的普通方法来执行。
并不会创建一个新的线程来执行它,所以这并不是多线程工作。
10、wait和sleep的区别
1, wait()属于Object类中的方法,sleep()属于Thread类中的方法
2,wait()方法进入状态后是不能自动唤醒的,需要其他线程调 用notify或者notifyAll才行;而sleep()不需要被唤醒,休眠时开始阻塞,线程的监控状态依然保持,当指定的休眠时间到了就会自动恢复
3,wait()方法会释放lock,并重新加入到等待队列中;sleep()方法不会释放lock
4,wait()方法需要依赖关键字synchorized() ,而sleep()方法不需要依赖