1. 并行和并发有什么区别?
- 并行是指两个或多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
- 并行:同时做不同事情的能力(在多台处理器上同时处理多个任务)
- 并发:交替做不同事情的能力(在一台处理器上“同时”处理多个任务,这个同时实际上是交替进行)
2.进程和线程的区别
- 进程是程序运行和资源分配的基本单位;线程是cpu调度和分派的基本单位。
- 内存分配:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU之外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源,线程组之间只能共享资源。
- 开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;而多个线程共享资源(代码和数据空间),减少切换次数,效率高,开销小。
- 包含关系:进程可以包含多个线程,同一个进程的多个线程并发执行;没有线程的进程可以看做单线程。
3.守护线程
- 守护线程(Daemon Thread):是个服务线程,准确来说是服务其他线程。
- 如果一个JVM里面的所有非daemon线程都退出了,JVM就会退出;而不管此时是否还有daemon线程在运行。只要非daemon在运行就不会退出JVM。
- 例子
(1)非守护线程
public class DaemonDemo {
public static void main(String[] args) {
WorkerThread thread = new WorkerThread();
System.out.println("work thread daemon:"+thread.isDaemon());
System.out.println("main thread daemon:"+Thread.currentThread().isDaemon());
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread exit");
}
}
class WorkerThread extends Thread{
public WorkerThread(){
//false:非守护线程
//true:设为守护线程
setDaemon(false);
}
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
work thread daemon:false
main thread daemon:false
0
1
2
main thread exit
3
4
5
6
7
8
9
等到非守护线程运行完毕,JVM才退出。
(2)守护线程
把上面例子进行修改,该为守护线程setDaemon(true);
运行结果:
work thread daemon:true
main thread daemon:false
0
1
2
main thread exit
不管守护线程是否运行完毕,JVM就退出。
4.创建线程的方式
- 继承Thread类
- 定义Thread类的子类MyThread,并重写run()方法,该run()方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建MyThread的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
public class ThreadDemo {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello thread");
}
}
- 实现Runnable接口
- 定义Runnable接口的实现类MyThread02,并重写此接口的run()方法,该方法是线程的执行体。
- 创建MyThread02的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
public class RunnableDemo {
public static void main(String[] args) {
MyThread02 myThread02 = new MyThread02();
new Thread(myThread02).start();
}
}
class MyThread02 implements Runnable{
@Override
public void run() {
System.out.println("hello runnable");
}
}
- 实现Callable接口
- 创建Callable接口的实现类MyThread03,并实现call()方法,该方法作为线程的执行体,并且还有返回值。
- 创建MyThread03的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 调用FutureTask对象的get()方法来或得子线程执行结束后的返回值。
- 使用FutureTask对象作为Thread对象的target,创建线程。
- 调用srart()方法,启动线程。
public class CallableDemo {
public static void main(String[] args) {
MyThread03 thread03 = new MyThread03();
FutureTask<String> futureTask = new FutureTask<String>(thread03);
new Thread(futureTask).start();
try {
String s = futureTask.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread03 implements Callable{
@Override
public Object call() throws Exception {
return "hello callable";
}
}
5.Runnable和Callable的区别
- Runnable接口中的run()方法没有返回值,它做的事情是纯粹去执行run()方法中的代码。
- Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
6.线程有哪些状态?
线程通常有五种状态:创建、就绪、运行、阻塞和死亡。
- 创建状态:new一个线程对象,并没有调用该对象的start()方法,这就是线程处于创建状态。
- 就绪状态:当调用了该线程对象的start()方法之后,该线程就进入就绪状态,开始去抢占CPU时间片。
- 运行状态:线程开始运行run()方法中的代码,表明已经抢占到了CPU时间片。
- 阻塞状态:一个线程在运行的过程中,受到某些操作的影响,放弃了已经获取到的CPU时间片,并且不再参与CPU时间片的争抢。
- 死亡状态:对于已经死亡的线程,无法再使用start()方法令其进入就绪状态。
7.sleep()和wait()有什么区别?
- sleep():方法是线程类(Thread)的静态方法。让调用线程进入睡眠状态,让出CPU给其他线程。等到休眠时间结束后,线程就进程就绪状态跟其他线程一起抢占CPU时间片。
- wait():方法是Object类的方法,当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池,同时让出CPU执行权和占有的锁,使得其他线程能够访问,可以通过notify(),notifyAll()来唤醒等待的线程。
8.notify()和notifyAll()有什么区别?
- 如果线程调用了对象的wait()方法,那么线程便会处于在该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- notify():随机唤醒一个wait线程
notifyAll():唤醒所有wait线程
被唤醒的线程就会进入该对象的锁池中,锁池中的线程就会去竞争该对象锁。 - 优先级高的线程竞争对象锁的概率大,假设某线程没有竞争到该对象锁,它还会留在锁池中。只有线程再次调用wait(),它才会重新回到等待池中。而竞争到对象锁的线程继续往下执行,直到执行完了synchronized代码块,它才会释放该对象。这时锁池中的线程会继续竞争该对象锁。
9.线程的run()和start()有什么区别?
- 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的。方法run()称为线程体。通过调用Thread类的start()来启动一个线程。
- start()启动一个线程,真正实现了多线程运行,这时无需等待run()方法代码执行完毕,可以直接继续执行下面的代码。这时此线程处于就绪状态,并没有运行。然后通过Thread类调用run()来完成其运行状态,这里run()称为线程体,run()方法结束,此线程终止,然后CPU调度到其他线程。
10.创建线程池的方式
- newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。 - newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,可灵活自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。 - newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。 - newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
11.线程池的状态:
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图:
线程池状态图
12.线程池中submit()和execute()方法的区别
- 接收的参数不一样
- submit有返回值,而execute()没有
- submit方便Exception管理
13.在java程序中怎么保证多线程的运行安全?
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic,synchronized)
- 可见性:一个线程对主内存的修改可以及时被其他线程看到(synchronized,volatile)
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序。
14.多线程锁的升级原理是什么?
在java中,锁有4中状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁升级的图示:
锁升级图
15.什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,互相持有对方所需要的资源,导致这些线程处于等待状态,若无外力作用,它们将无法推进下去。
16.怎么防止死锁?
死锁产生的四个必要条件:
- 互斥条件:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
- 请求和保持条件:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进行释放该资源。
- 不可剥夺条件:进程已或得的资源,在未使用之前,不可被剥夺,只能使用完后自己释放
- 循环等待条件:指进程发生死锁之后,若干个进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
17.ThreadLocal是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境(web服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完后没有释放,java应用就存在内存泄露的风险。
18.synchronized底层实现原理?
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法进入到临界区,同时它还可以保证共享变量的内存可见性。
java中的每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁的是当前实例对象
- 静态同步方法,锁的是当前类的class对象
- 同步方法块,锁的是括号里面的对象
19.synchronized和volatile的区别是什么?
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile只能使用在变量上;synchronized则可以使用在变量、方法和类上。
- volatile只能实现变量的修改可见性;而synchronized则可以保证变量修改的原子性和可见性。
- volatile不会造成线程阻塞;而sychronized可能会造成线程阻塞。
4.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
20. synchronized和Lock有什么区别?
- synchronized是java内置关键字;在jvm层面,Lock是个java类。
- synchronized无法判断是否获取锁的状态。Lock可以判断是否获取到锁。
- synchronized会自动释放锁(a线程执行完同步代码会释放锁;b线程执行过程中出现异常会释放锁);Lock需在finally中手动释放锁(unlock()方法释放锁),否则容易造成线程死锁。
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
21. synchronized 和 ReentrantLock 区别是什么?
- synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
- 既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知 - 二者的锁机制其实也是不一样的:
ReentrantLock底层调用的是Unsafe的park方法加锁;synchronized操作的应该是对象头中mark word。
22.atomic的原理
- Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
- Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。