我的理解是:多个线程交替执行,本身是没有问题的,但是如果访问共享资源,结果可能会出现问题,于是就出现了线程不安全的问题。
- 访问共享变量或资源
- 依赖时序的操作
- 不同数据之间存在绑定关系
- 对方没有声明自己是线程安全的
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
实际上就是任务从保存到再加载的过程就是一次上下文切换。
- 并行:单位时间内,多个任务同时执行。
- 并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
面试官挺喜欢问这个问题的,他能引出来他想要问你的知识点。
面试官:说一下线程与进程的区别
我:好的,如下:
-
进程是程序的一次执行过程,是系统运行程序的基本单位
-
线程是一个比进程更小的执行单位
-
一个进程在其执行的过程中可以产生多个线程
-
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程
举例子:比如,main函数启动了JVM进程,同时main就是其中的线程,并且启动了JVM进程,那么还有垃圾回收等线程。
或者这样的例子:做个简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
面试官:直接调用Thread的run方法不行吗?
我:肯定不行的,通过run方法启动线程其实就是调用一个类中的方法,当作普通的方法的方式调用。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。如果是start方法,效果就不一样了。
首先看一下start源码:
public synchronized void start() {
// 等于0意味着可以是线程的新建状态
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将该线程加入线程组
group.add(this);
boolean started = false;
try {
start0(); // 核心, 本地方法,新建线程被。
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
当得到CPU的时间片后就会执行其中的run()方法。这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。
@Override
public void run() {
if (target != null) {
target.run(); // target是:private Runnable target; Runnable接口
}
}
// Runnable:
public abstract void run();//抽象方法
面试官:线程的生命周期,讲一下。
我:ok,看图说话
- 线程创建之后它将处于
New
(新建)状态,调用start()
方法后开始运行,线程这时候处于READY
(可运行,也叫做就绪) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于RUNNING
(运行) 状态。 - 当线程执行
wait()
方法之后,线程进入WAITING
(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而TIME_WAITING
(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)
方法或wait(long millis)
方法可以将 Java 线程置于TIMED WAITING
状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。 - 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到
BLOCKED
(阻塞)状态。 - 线程在执行 Runnable 的
run()
方法结束之后将会进入到TERMINATED
(终止) 状态。
面试官:wait/notify 和 sleep 方法的异同?
我:ok
相同点:
- 它们都可以让线程阻塞。
- 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
不同点:
- wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
- 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
- sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
- wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。