1. 什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
2. 为什么用 多线程?(多线程的技术背景)
这个问题 大白话就是 多线程有什么用。
一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓”知其然知其所以然”,”会用”只是”知其然”,”为什么用”才是”知其所以然”,只有达到”知其然知其所以然”的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:
- (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.2 并发编程的缺点
1. 频繁的上下文切换
2. 线程安全问题
3. 编程复杂度增加
2.3 容易混淆的概念
同步 VS 异步
同步就是 你喊某个人,一直主动去看对方是不是有回应。异步就是你喊了某个人,然后不管了就干自己的事情去了,被你喊那个人会回来告诉你你喊他了,找他有啥事。
官方一点的说法:
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
- 而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
并发 VS 并行
并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不同。
- 并发 : 同一时间段多个任务交替执行。任务之间可能还是串行的。
- 并行 : 同一时刻多个任务同时执行。
阻塞 VS 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
3. 线程和进程有什么区别?
线程是程序执行的最小单位,程序的执行不仅仅是对CPU的利用,具体占用计算机的那些资源,需要看执行程序的复杂度。进程包含线程,是操作系统进行资源分配和调度的基本单位,可以理解为一个应用程序。进程与进程之间相互独立,需要通过通信协议来通信。线程与线程之间的执行也是相互独立的,但是可以通过共享变量来控制相互的协作。
进程 | 线程 | |
---|---|---|
调度 | 拥有资源的基本单位 | cpu调度的基本单位 |
拥有资源 | 拥有资源的基本单位 | 不拥有资源,但可以访问隶属进程的系统资源 |
并发性 | 不仅进程可以并发执行 | 进程中的线程同样可以并发执行 |
系统开销 | 创建撤销切换带来较大的系统开销(内存空间,IO设备) | 切换:只需保存和设置少点寄存器 通信:可通过共享变量实现 |
通信 | 进程间通信 IPC,需要同步互斥手段的辅助 | 直接读写进程数据断(如全局变量)来进行通信 |
4. 创建线程的方式,不同创建方式之间的区别?
4.1实现多线程的方式
- 继承Thread类,重写run函数
- 实现 Runnable 接口,重写run函数
示例: 模拟并发卖票
public class Test {
static class MyThread implements Runnable{
int tickets = 10;
@Override
public void run(){
while (tickets>0)
{
synchronized (this)
{
if (tickets > 0)
{
System.out.printf("%s 线程正在卖出第 %d 张票\n",
Thread.currentThread().getName(), tickets);
--tickets;
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread myt = new MyThread();
Thread t1 = new Thread(myt);
Thread t2 = new Thread(myt);
t1.start();
t2.start();
}
}
- 实现 Callable 接口,重写call 方法
class MyThread implements Callable{
int tickets = 10;
@Override
public Object call() throws Exception {
while (tickets>0)
{
synchronized (MyThread.class)
{
if (tickets > 0)
{
System.out.printf("%s 线程正在卖出第 %d 张票\n",
Thread.currentThread().getName(), tickets);
--tickets;
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null; // 有返回值
}
}
启动线程
FutureTask<Integer> myt = new FutureTask<>(new MyThread());
new Thread(myt).start();
- 通过线程池 创建
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
public class ExecutorServiceDemo {
public static void main(String[] args) {
// 创建一个线程池对象,控制要创建几个线程对象。
// public static ExecutorService newFixedThreadPool(int nThreads)
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
//结束线程池
pool.shutdown();
}
}
使用接口实现线程的好处:
多个线程可共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
使用线程池的好处:
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
5. 线程间状态的转换
线程是会在不同的状态间进行转换的,java线程线程转换图如上图所示。线程创建之后调用start()方法开始运行,当调用wait(),join(),LockSupport.lock()方法线程会进入到WAITING状态,而同样的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING状态,当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITING和TIMED _WAITING状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到TERMINATED状态,状态转换可以说是线程的生命周期。另外需要注意的是:
当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。
用一个表格将上面六种状态进行一个总结归纳。


5. 什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。
有哪些线程安全的集合类,这些类是怎么做到线程安全的?
常见的线程安全 集合类 有 ConcurrentHashMap, HashTable,Vector,CopyOnWriteArrayList,Stack
6. Java中如何停止一个线程?
Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法,但是由于潜在的死锁威胁。因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。
还有一种方式停止线程,就是通过线程池管理线程,然后调用线程池的shutdown 方法。
7.Java中活锁和死锁有什么区别?
活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
8.Java中什么是竞态条件?
http://ifeve.com/race-conditions-and-critical-sections/
9:什么是ThreadLocal变量?
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
一个故事讲明白线程的私家领地:ThreadLocal
Java面试必问,ThreadLocal终极篇
10:如何检测死锁?怎么预防死锁?
10.1 什么是死锁?
所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁
通俗地讲就是两个或多个进程被无限期地阻塞、相互等待的一种状态
10.2死锁产生的原因?
1.因竞争资源发生死锁 现象:系统中供多个进程共享的资源的数目不足以满足全部进程的需要时,就会引起对诸资源的竞争而发生死锁现象
2.进程推进顺序不当发生死锁
10.3 死锁的四个必要条件
1.互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
2.请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
3.不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
4.环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
10.4 怎么检测死锁?
有两个容器,一个用于保存线程正在请求的锁,一个用于保存线程已经持有的锁。每次加锁之前都会做如下检测:
1.检测当前正在请求的锁是否已经被其它线程持有,如果有,则把那些线程找出来
2.遍历第一步中返回的线程,检查自己持有的锁是否正被其中任何一个线程请求,如果第二步返回真,表示出现了死锁。
10.5 死锁的解除与预防:
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
11:线程的sleep和 Object的 wait方法有什么区别?
1)第一个很重要的区别就是,wait方法必须正在同步环境下使用,比如synchronized方法或者同步代码块。如果你不在同步条件下使用,会抛出IllegalMonitorStateException异常。另外,sleep方法不需要再同步条件下调用,你可以任意正常的使用。
2)第二个区别是,wait方法用于和定义于Object类的,而sleep方法操作于当前线程,定义在java.lang.Thread类里面。
3)第三个区别是,调用wait()的时候方法会释放当前持有的锁,而sleep方法不会释放任何锁。
4)wait方法最好在循环里面调用,是为了处理错误的通告,比如说,即使线程唤醒了,等待状态仍然适用。(看不懂?大概是循环里面再判断一次线程是否真的醒来),然而sleep方法没这样的限制。最好别在循环里面调用sleep方法。
https://www.jianshu.com/p/a67ad7ba89a5
12: 线程池的种类和应用场景?
1.newCachedThreadPool
底层: 返回ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)
通俗: 当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
适用: 执行很多短期异步的小程序或者负载较轻的服务器
2.newFixedThreadPool
底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用: 执行长期的任务,性能好很多
3.newSingleThreadExecutor:
底层: FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用:一个任务一个任务执行的场景
4.NewScheduledThreadPool
底层:创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列
通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
适用:周期性执行任务的场景
https://www.jianshu.com/p/1d30f33cfbde
待补充
参考链接:
https://zhuanlan.zhihu.com/p/32645713
https://zhuanlan.zhihu.com/p/26441926
https://www.zhihu.com/question/264627396/answer/283257314
网友评论