文章目录
- 1:并行和并发有什么区别 ?
- 2:线程和进程的区别 ?
- 3:守护线程是什么 ?
- 4:多线程有几种实现方式 ?
- 5: Runnable 和 Callable有什么区别 ?
- 6:线程有哪些状态 ?
- 7:sleep() 和 wait() 有什么区别 ?
- 8:notify()和 notifyAll()有什么区别 ?
- 9:线程的 run() 和 start() 有什么区别 ?
- 10:创建线程池有哪几种方式?
- 11:线程池都有哪些状态 ?
- 12:线程池中 submit() 和 execute() 方法有什么区别 ?
- 13:在 Java 程序中怎么保证多线程的运行安全 ?
- 14:多线程中 synchronized 锁升级的原理是什么 ?
- 15:什么是死锁 ?
- 16:怎么防止死锁 ?
- 17:ThreadLocal 是什么?有哪些使用场景 ?
- 18:synchronized 底层实现原理 ?
- 19:synchronized 和 volatile 的区别是什么 ?
- 20:synchronized 和 Lock 有什么区别 ?
- 21:synchronized 和 ReentrantLock 区别是什么 ?
1:并行和并发有什么区别 ?
- 并行
- 多个处理器或多核处理器同时处理多个任务
- 并发
- 多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行
2:线程和进程的区别 ?
- 一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。
- 总结:程序(微信)> 进程(语音,打字)> 线程(语音进程下有多个线程来支持这个功能)
3:守护线程是什么 ?
- 守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
- 在 Java 中垃圾回收线程就是特殊的守护线程。
4:多线程有几种实现方式 ?
- 继承Thread
class ThreadBaseDemo extends Thread {
public static void main(String[] args) {
ThreadBaseDemo thread1 = new ThreadBaseDemo();
ThreadBaseDemo thread2 = new ThreadBaseDemo();
thread1.start();
thread2.start();
}
}
- 实现Runnable接口
class SyncTest1 implements Runnable {
Object object = new Object();
public static void main(String[] args) {
SyncTest1 syncTest1 = new SyncTest1();
Thread thread1 = new Thread(syncTest1);
Thread thread2 = new Thread(syncTest1);
thread1.start();
thread2.start();
}
@Override
public void run() {
synchronized (object) {
try {
System.out.println(Thread.currentThread().getName() + "线程执行了run方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行2秒钟之后完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 实现Callable接口通过FutureTask包装器来创建Thread线程
class ThreadBase1 implements Callable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadBase1 threadBase1 = new ThreadBase1();
// 与实现 Runnable 接口相比多的一步操作
FutureTask<String> ft = new FutureTask<String>(threadBase1);
Thread thread1 = new Thread(ft);
thread1.start();
//这个必须等线程start调用get() 方法才行,否则该线程就会进入到等待状态;
String resultCall = ft.get();
System.out.println("线程返回结果为:" + resultCall);
}
@Override
public String call() throws Exception {
return "当前线程为:" + Thread.currentThread().getName();
}
}
- 通过线程池创建线程,使用线程池接口ExecutorService结合Callable、Future实现有返回结果的多线程。
class ThreadBase2 {
public void test1() throws InterruptedException {
/**
* 实例化线程池
* @param corePoolSize: 核心池大小 (<= maximumPoolSize)
* @param maximumPoolSize: 线程池中能拥有最多线程数
* @param keepAliveTime: 线程没有任务执行时最多保持多久时间会终止
* @param TimeUnit: 参数keepAliveTime的时间单位
* @param workQueue: 一个阻塞队列,用来存储等待执行的任务
*/
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(6, 10,
0L, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<Runnable>());
System.out.println("初始化线程核心池数量:" + threadPool.getCorePoolSize());
for (int i = 0; i < 15; i++) {
ThreadBase2_1 task = new ThreadBase2_1(i);
//线程池中加入一个任务 注意: 并不是一个线程
threadPool.execute(task);
System.out.println("当前线程池中数量:" + threadPool.getPoolSize()
+ "--当前核心池数量:" + threadPool.getCorePoolSize()
+ "--当前队列中等待执行的任务数量:" + threadPool.getQueue().size()
+ "--当前已执行完的任务数量:" + threadPool.getCompletedTaskCount());
}
}
public static void main(String[] args) throws InterruptedException {
ThreadBase2 demo = new ThreadBase2();
demo.test1();
}
}
- 总结:前面两种【无返回值】原因:通过重写run方法,run方法的返回值是void,所以没有办法返回结果。
后面两种【有返回值】原因:通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中。
5: Runnable 和 Callable有什么区别 ?
- Runnable没有返回值,Callable可以拿到有返回值。
- Callable可以看作是 Runnable的补充。
6:线程有哪些状态 ?
- 初始 (new)
- 新创建了一个线程对象,但还没有调用start()方法。
- 运行 (runnable)
- Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
- 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞 (blocked)
- 表示线程阻塞于锁。
- 等待 (waiting)
- 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(timed_waiting)
- 该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止 (terminated)
- 表示该线程已经执行完毕。
7:sleep() 和 wait() 有什么区别 ?
- 类的不同
- sleep() 来自 Thread。
- wait() 来自 Object。
- 释放锁
- sleep() 不释放锁。
- wait() 释放锁。
- 用法不同
- sleep() 时间到会自动恢复。
- wait() 可以使用 notify() / notifyAll()直接唤醒。
- wait() 需要在同步代码块中使用,不然会报 IllegalMonitorStateException ;sleep() 方法不会
class ThreaaBaseDemo_5 implements Runnable {
private static int i = 10;
public static void main(String[] args) throws InterruptedException {
ThreaaBaseDemo_5 demo_5 = new ThreaaBaseDemo_5();
Thread thread1 = new Thread(demo_5);
Thread thread2 = new Thread(demo_5);
//该行如果注释点,就会报 IllegalMonitorStateException
// synchronized (thread1) {
thread1.start();
thread1.wait(1000);
// }
thread2.start();
}
@Override
public void run() {
i--;
System.out.println("线程" + Thread.currentThread().getName() + "运行");
}
}
8:notify()和 notifyAll()有什么区别 ?
- 唤醒数量
- notify() : 唤醒一个线程。
- notifyAll() : 唤醒所有的线程。
- 唤醒过程
- notify() : 只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
- notifyAll() : 会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。
9:线程的 run() 和 start() 有什么区别 ?
- start()
- 方法用于启动线程
- 只能调用一次
- run()
- 用于执行线程的运行时代码
- 可以重复调用
10:创建线程池有哪几种方式?
- newSingleThreadExecutor()
- 它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
- newCachedThreadPool()
- 它是一种用来处理大量短时间工作任务的线程池。
- 具有几个鲜明特点:
- 它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程。
- 如果线程闲置的时间超过 60 秒,则被终止并移出缓存。
- 长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
- newFixedThreadPool(int nThreads)
- 重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现。
- 如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。
- newSingleThreadScheduledExecutor()
- 创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度。
- newScheduledThreadPool(int corePoolSize)
- 和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
- newWorkStealingPool(int parallelism)
- 这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
- ThreadPoolExecutor()
- 是最原始的线程池创建;也是最核心的方式。
11:线程池都有哪些状态 ?
- 运行中(running)
- 这是最正常的状态,接受新的任务,处理等待队列中的任务。
- 关闭(shutdown)
- 不接受新的任务提交,但是会继续处理等待队列中的任务。
- 停止(stop)
- 不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- tidying
- 所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 tidying状态时,会执行钩子方法 terminated()。
- 结束(terminated)
- terminated()方法结束后,线程池的状态就会变成这个。
12:线程池中 submit() 和 execute() 方法有什么区别 ?
- execute()
- 只能执行 Runnable 类型的任务。
- submit()
- 可以执行 Runnable 和 Callable 类型的任务。
- 总结:Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
13:在 Java 程序中怎么保证多线程的运行安全 ?
- 方案一
- 使用安全类,比如 Java. util. concurrent 下的类。
- 方案二
- 使用自动锁 synchronized。
- 方案三
- 使用手动锁 Lock。
- 手动锁代码示例
class ThreadBase4 implements Runnable { private Lock lock = new ReentrantLock(); public void testLock(Thread thread) throws InterruptedException { //获取锁 //lock.lock(); //尝试获取锁,如果不到只等3秒,如果3秒后还获取不到,返回false if (lock.tryLock(3000, TimeUnit.MICROSECONDS)) { try { System.out.println("线程" + thread.getName() + "获取当前锁"); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("线程" + thread.getName() + "发生异常并释放锁"); } finally { System.out.println("线程" + thread.getName() + "执行完毕并释放锁"); lock.unlock();//释放锁 } } } public static void main(String[] args) { ThreadBase4 threadBase4 = new ThreadBase4(); Thread thread1 = new Thread(threadBase4); Thread thread2 = new Thread(threadBase4); thread1.start(); thread2.start(); } @Override public void run() { try { testLock(Thread.currentThread()); } catch (InterruptedException e) { e.printStackTrace(); } } }
14:多线程中 synchronized 锁升级的原理是什么 ?
- 原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
- 锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
15:什么是死锁 ?
- 当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
16:怎么防止死锁 ?
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
17:ThreadLocal 是什么?有哪些使用场景 ?
- ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
- ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
18:synchronized 底层实现原理 ?
- synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。
- 在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。
- 但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
19:synchronized 和 volatile 的区别是什么 ?
- volatile
- 是变量修饰符
- 仅能实现变量的修改可见性,不能保证原子性
- 不会造成线程的阻塞
- synchronized
- 是修饰类、方法、代码段
- 则可以保证变量的修改可见性和原子性
- 可能会造成线程的阻塞
20:synchronized 和 Lock 有什么区别 ?
- synchronized
- 可以给类、方法、代码块加锁
- 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁
- 不能知道有没有获取锁
- Lock
- 只能给代码块加锁
- 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 可以知道有没有成功获取锁
21:synchronized 和 ReentrantLock 区别是什么 ?
- synchronized
- 早期的实现比较低效
- 不需要手动释放和开启锁;
- 可用于修饰方法、代码块等
- 标记的变量可以被编译器优化
- ReentrantLock
- 后来实现的效率较高
- 使用起来比较灵活,但是必须有释放锁的配合动作
- 必须手动获取与释放锁
- 只适用于代码块锁
- 标记的变量不会被编译器优化
22:atomic 的原理 ?
- 主要利用 CAS (Compare And Swap) 和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。