Java 多线程 — 线程基础
📋 本文节选自《Java 多线程面试题》,返回 查看全部章节
1. 多线程有什么用?
1、发挥多核CPU 的优势随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了 50%, 在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU 的优势来,达到充分利用CPU 的目的。
2、防止阻塞从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
3、便于建模这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
2. 线程和进程的区别是什么?
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
3. Java 实现线程有哪几种方式?
1、继承 Thread 类实现多线程
2、实现 Runnable 接口方式实现多线程
3、使用 ExecutorService、Callable、Future 实现有返回结果的多线程
4. 启动线程方法 start()和 run()有什么区别?
只有调用了 start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
5. 怎么终止一个线程?如何优雅地终止线程?
stop 终止,不推荐。
6. 一个线程的生命周期有哪几种状态?它们之间如何流转的?
NEW:毫无疑问表示的是刚创建的线程,还没有开始启动。RUNNABLE: 表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中状态。BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行。WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法,通过join()方法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线程就进入了 RUNNABLE 状态继续运行。TIMED_WAITING:表示线程进入了一个有时限的等待,如sleep(3000),等待3秒后线程重新进行 RUNNABLE 状态继续运行。TERMINATED:表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到RUNNABLE 状态
7. 线程中的 wait()和 sleep()方法有什么区别?
这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
8. 多线程同步有哪几种方法?
Synchronized 关键字,Lock 锁实现,分布式锁等。
9. 什么是死锁?如何避免死锁?
死锁就是两个线程相互等待对方释放对象锁。
10. 多线程之间如何进行通信?
wait/notify
11. 线程怎样拿到返回结果?
实现Callable 接口。
12. violatile 关键字的作用?
一个非常重要的问题,是每个学习、应用多线程的 Java 程序员都必须掌握的。理解 volatile关键字的作用的前提是要理解 Java 内存模型,这里就不讲Java 内存模型了,可以参见第31 点,volatile 关键字的作用主要有两个:
1、多线程主要围绕可见性和原子性两个特性而展开,使用volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据
2、代码底层执行不像我们看到的高级语言----Java 程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的 C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能 JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率从实践角度而言,volatile 的一个重要 作 用 就 是 和 CAS 结 合 , 保 证 了 原 子 性,详细的可以参见java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
13. 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
用join 方法。
14. 怎么控制同一时间只有 3 个线程运行?
用Semaphore。
15. 为什么要使用线程池?
我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的CPU和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。