线程
1.Java中如何保证线程安全性
答:首先线程安全主要体现在以下三个部分:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
通过合理的时间调度,避开共享资源的存取冲突。另外,在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源,设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。
所以保证线程安全无非以下方法:
- synchronized关键字
- lock接口
- volatile+CAS【单纯的volatile是轻量级的同步机制保证可见性但是不具备原子性所以要配合CAS来实现线程安全】
- atomic原子类(JDK里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean它们是通过CAS完成原子性。)
2.请你简要说明一下线程的基本状态以及状态之间的关系?
答:线程的基本状态有创建状态(new)、运行状态(runnable)、阻塞状态(Blocked)、等待状态(waiting)、时间等待状态(timed waiting)、终止状态(terminated)
-
新建new:新创建一个线程对象。
-
可运行runnable:线程对象创建后,其它线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权。
-
阻塞blocked:阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu time slice,暂时停止运行。直到线程进入可运行状态,才有机会再次获得cpu timeslice转到运行状态。阻塞的情况分三种:
(一) 等待阻塞:running线程执行wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二) 同步阻塞:running线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三) 其它阻塞: running线程执行**Thread.sleep(long ms)或t.join()**方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行状态。
-
死亡terminated:run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可重生。
3.请你解释一下什么是线程池(thread pool)?
答:线程池就是存放线程的池子,当需要用到线程时就可以从线程池里面拿线程,用完了就将线程放回线程池。
第一:为什么要使用池化技术
- 这就涉及到线程的创建与销毁,首先,线程创建是一个复杂的过程需要加载内存资源,这样既耗时间又耗内存,其次就是线程的销毁涉及到jvm垃圾回收机制,它需要跟踪线程在线程使用完后将其销毁。由此来看,创建和销毁线程不仅耗时还耗资源,所以引入池化技术。
第二:五种创建线程池的方法
1、newCachedThreadPool
作用:创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要时使用提供的 ThreadFactory 创建新线程。
特征:
(1)线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
(2)线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
(3)当线程池中,没有可用线程,会重新创建一个线程
创建方式: Executors.newCachedThreadPool();
2、newFixedThreadPool
作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
特征:
(1)线程池中的线程处于一定的量,可以很好的控制线程的并发量
(2)线程可以重复被使用,在显示关闭之前,都将一直存在
(3)超出一定量的线程被提交时候需在队列中等待
创建方式:
(1)Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
(2)Executors.newFixedThreadPool(int nThreads,ThreadFactory threadFactory);//nThreads为线程的数量,threadFactory创建线程的工厂方式
3、newSingleThreadExecutor
作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
特征:
(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
创建方式:
(1)Executors.newSingleThreadExecutor() ;
(2)Executors.newSingleThreadExecutor(ThreadFactory threadFactory);// threadFactory创建线程的工厂方式
4、newScheduleThreadPool
作用: 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
特征:
(1)线程池中具有指定数量的线程,即便是空线程也将保留
(2)可定时或者延迟执行线程活动
创建方式:
(1)Executors.newScheduledThreadPool(int corePoolSize);// corePoolSize线程的个数
(2)newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory);// corePoolSize线程的个数,threadFactory创建线程的工厂
5、newSingleThreadScheduledExecutor
作用: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。
特征:
(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
(2)可定时或者延迟执行线程活动
创建方式:
(1)Executors.newSingleThreadScheduledExecutor() ;
(2)Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory) ;//threadFactory创建线程的工厂
4.请介绍一下线程同步和线程调度的相关方法。
答:有如下方法
- wait方法 :使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
- notify方法:唤醒一个处于等待的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
- notifyAll方法:唤醒所有在等待的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
- sleep方法:线程进入睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
- yield方法:暂停当前正在执行的线程对象 执行yield()方法后转入就绪(ready)状态;
- join方法:主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。
通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),**信号量可以用来限制对某个共享资源进行访问的线程的数量。**在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
5.请简述一下线程的sleep()方法和yield()方法有什么区别?
答:对于sleep方法和yield方法区别从以下几个方面进行比较。
- **优先级考虑:**sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
- **执行后转入什么状态:**sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
- **是否抛异常:**sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
- **可移植性:**seep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
6.简述下java创建线程的三个方法
答:第一种方法是继承Thread类、第二种方法是实现Runnable接口、第三种是通过Callable和Future接口创建线程
public class MyThread extends Thread{//第一种:继承Thread类
public void run(){
//重写run方法 设置线程任务
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
public class MyThread2 implements Runnable {//实现Runnable接口
public void run(){
//重写run方法 设置线程任务
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
//或者 new Thread(new MyThread2()).start(); 匿名内部类 也可以lambda表达式
}
}
call方法相较run方法优势:
- call()方法可以有返回值;
- call()方法可以声明抛出异常;
Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口,并实现了Runnable接口,所以FutureTask可以作为Thread类的target,同时也解决了Callable对象不能作为Thread类的target这一问题。
什么时候用Callable接口?
- 如果要有返回值的话,那么必须得实现Callable接口,重写call方法,并且get返回值。
public class ThirdThreadImp {
public static void main(String[] args) {
//这里call()方法的重写是采用lambda表达式,没有新建一个Callable接口的实现类
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i = 0;
for(;i < 50;i++) {
System.out.println(Thread.currentThread().getName() +
" 的线程执行体内的循环变量i的值为:" + i);
}
//call()方法的返回值
return i;
});
for(int j = 0;j < 50;j++) {
System.out.println(Thread.currentThread().getName() +
" 大循环的循环变量j的值为:" + j);
if(j == 20) {
new Thread(task,"有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:" + task.get());//V get():返回Callable任务里call()方法的返回值
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结:比较Thread和Runnable哪一个更好?实现Runnable接口更好,使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享。
7.请说明一下线程中的同步和异步有何异同?并且请举例说明在什么情况下会使用到同步和异步?
答:具有本质上的区别线程同步体现在线程实时监控一个状态,异步体现在耗时提高性能。
- **使用线程同步:**如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
- **使用线程异步:**当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
8.请说明一下sleep() 和 wait() 有什么区别?
答:sleep方法和wait方法主要从来源以及功能上进行比较
- 来源上:sleep方法是线程类的一个方法,wait方法是Object类的方法
- 功能上:sleep方法睡眠完设定的时间就会自动进入争夺CPU执行权的队列,而wait方法只有等notify或者notifyAll方法唤醒。
9.请分析一下同步方法和同步代码块的区别是什么?
答:主要从锁对象和粒度程度进行比较。
- 锁对象:首先同步方法的锁的对象为this或者当前类class对象(在静态方法中出现的),而同步代码快的锁对象是任意的(obj)
- 粒度:同步代码块的粒度更细,可以选择特定的代码进行同步操作(而不是将方法中的所有代码都进行同步操作)
所以,综合来看同步代码块的相对同步方法是更加合适的选择,性能高,效率高。
10.请说明一下线程池有什么优势?
答:主要是从性能、管理方面去考虑
- 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。
- 第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
11.请简述一下线程池的运行流程,使用参数以及方法策略等
答:3个临界:第一个临界是核心线程数(执行任务的线程是否到达了核心线程数)、第二个临界是任务队列是否满了(满了就创建新的线程)、第三个临界是是否到达了最大线程数(到达之后采用饱和策略)。
线程池主要就是指定线程池核心线程数大小corePoolSize,最大线程数maximumPoolSize,存储的队列runnableTaskQueue,拒绝策略handler,空闲线程存活时长keepAliveTime。
-
当需要任务大于核心线程数时候,就开始把任务往存储任务的队列里,当存储队列满了的话,就开始增加线程池创建的线程数量。
-
如果当线程数量也达到了最大,就开始执行拒绝策略
-
线程池的创建new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,milliseconds,
runnableTaskQueue,handler);其中最后一个参数handler就是饱和策略
-
java线程池框架提供的4种可策略:
- AbortPolicy:直接抛出异常(默认)抛出来一个ThreadPoolRejectException
- CallerRunsPolicy:只用调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列中最老的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
12.请介绍一下什么是生产者消费者模式?
答:
- 解耦:假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化, 可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
- 支持并发:由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。
- 支持忙闲不均:缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。 等生产者的制造速度慢下来,消费者再慢慢处理掉。
13.请简述一下实现多线程同步的方法?
答:synchronized关键字 lock接口 volatile
- synchronized 修饰同步方法修饰同步代码块实现线程同步
- 使用关键字volatile修饰共享变量实现伪同步(使用特殊域变量(volatile)实现线程同步)
1. volatile为所修饰的共享变量提供了一种免锁机制,并没有为共享变量上锁。
2. 使用volatile;
相当于告诉JVM某个共享变量随时可能被其他线程修改,
需要线程到内存中去读取这个共享变量的值,
而不是从缓存中读取
3. volatile仅保证可见性;
volatile所修饰的共享变量的值被修改时,会被立即更新到内存中
4. volatile不保证原子性
5. volatile不能修饰final变量
基于第2点,volatile修饰的变量随时可能会被修改,
而final修饰的变量是不会被修改,
因此volatile不能修饰final变量
主要基于以上的第3,4点原因,volatile保证了可见性,相对于没有用volatile修饰的共享变量,大大提高了线程读取到最新数据的可能性,但是volatile不保证原子性,说明线程读到的最新数据可能并不是真正意义上的最新
为什么volatile能同步?它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是从缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。
- 使用重入锁实现线程同步(private
Lock lock = ``new
ReentrantLock(); )