Java 线程
简述线程、进程、程序的基本概念?
程序
程序,是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程
进程,是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程
线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。每个线程默认使用 1024KB 的内存,所以一个 Java 进程是无法开启大量线程的。
三者之间的关系
- 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程有什么优缺点?
1)好处
- 使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载。
- 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好。
2)坏处
- 大量的线程降低代码的可读性。
- 更多的线程需要更多的内存空间。
- 当多个线程对同一个资源出现争夺时候要注意线程安全的问题。
你了解守护线程吗?它和非守护线程有什么区别?
Java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
- 任何线程都可以设置为守护线程和用户线程,通过方法
Thread#setDaemon(boolean on)
设置。true
则把该线程设置为守护线程,反之则为用户线程。 -
Thread#setDaemon(boolean on)
方法,必须在Thread#start()
方法之前调用,否则运行时会抛出异常。
唯一的区别是:程序运行完毕,JVM 会等待非守护线程完成后关闭,但是 JVM 不会等待守护线程。比如,JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线程时,Java 虚拟机会自动离开。
什么是多线程上下文切换?
多线程会共同使用一组计算机上的 CPU ,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU 。
不同的线程切换使用 CPU 发生的切换数据等,就是上下文切换。
- 在上下文切换过程中,CPU 会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。
- 上下文切换是存储和恢复 CPU 状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
Java 中用到的线程调度算法是什么?
假设计算机只有一个 CPU ,则在任意时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令。
- 所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。
- 在运行池中,会有多个处于就绪状态的线程在等待 CPU ,Java 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
有两种调度模型:分时调度模型和抢占式调度模型。
- 分时调度模型是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
- Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU ,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU 。处于运行状态的线程会一直运行,直至它不得不放弃 CPU 。 如非特别需要,尽量不要用,防止线程饥饿。
什么是线程饥饿?
饥饿,一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
Java 中导致饥饿的原因:
- 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
- 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
线程的生命周期?
线程一共有五个状态,分别如下:
-
新建(new):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:
Thread t1 = new Thread()
。 -
可运行(runnable):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。例如:
t1.start()
。 -
运行(running):线程获得 CPU 资源正在执行任务(
#run()
方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。 -
死亡(dead):当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
- 自然终止:正常运行完
#run()
方法,终止。 - 异常终止:调用
#stop()
方法,让一个线程终止运行。
- 自然终止:正常运行完
-
堵塞(blocked):由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。直到线程进入可运行(runnable)状态,才有机会再次获得 CPU 资源,转到运行(running)状态。阻塞的情况有三种:
-
正在睡眠:调用
#sleep(long t)
方法,可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入可运行(runnable)状态。
-
正在等待:调用
#wait()
方法。调用
notify()
方法,回到就绪状态。 -
被另一个线程所阻塞:调用
#suspend()
方法。调用
#resume()
方法,就可以恢复。
-
- 中间一行是线程的顺畅的执行过程的四个状态。其上下两侧,是存在对应的情况,达到阻塞状态和恢复执行的过程。
- 有一点要注意,新建(new)和死亡(dead)是单向的状态,不可重复。****
如何结束一个一直运行的线程?
一般来说,有两种方式:
-
方式一,使用退出标志,这个 flag 变量要多线程可见。
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
-
方式二,使用 interrupt 方法,结合 isInterrupted 方法一起使用。
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用
Thread#join()
方法,或者Thread#sleep(...)
方法,在网络中调用ServerSocket#accept()
方法,或者调用了DatagramSocket#receive()
方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时。即使主程序中将该线程的共享变量设置为true
,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这里我们给出的建议是,不要使用
Thread#stop()· 方法,而是使用 Thread 提供的
#interrupt()` 方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
所以,方式一和方式二,并不是冲突的两种方式,而是可能根据实际场景下,进行结合。
一个线程如果出现了运行时异常会怎么样?
如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放。
创建线程的方式及实现?
Java 中创建线程主要有三种方式:
具体的每种方式的代码实现,可以看看 《Java创建线程的四种方式》 。
- 方式一,继承 Thread 类创建线程类。
- 方式二,通过 Runnable 接口创建线程类。
- 方式三,通过 Callable 和 Future 创建线程。
- 方式四,使用线程池创建线程。
start 和 run 方法有什么区别?
- 当你调用 start 方法时,你将创建新的线程,并且执行在 run 方法里的代码。
- 但是如果你直接调用 run 方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run 方法当作普通方法去执行。
如何使用 wait + notify 实现通知机制?
在 Java 发展史上,曾经使用 suspend、resume 方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。
解决方案可以使用以对象为目标的阻塞,即利用 Object 类的 wait 和 notify方法实现线程阻塞。
- 首先,wait、notify 方法是针对对象的,调用任意对象的 wait 方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify 方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行。
- 其次,wait、notify 方法必须在
synchronized
块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
具体的实现,看看 《Wait / Notify通知机制解析》 文章。
Thread类的 sleep 方法和对象的 wait 方法都可以让线程暂停执行,它们有什么区别?
- sleep 方法,是线程类 Thread 的静态方法。调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)
- wait 方法,是 Object 类的方法。调用对象的
#wait()
方法,会导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的#notify()
方法(或#notifyAll()
方法)时,才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
notify 和 notifyAll 有什么区别?
当一个线程进入 wait 之后,就必须等其他线程 notify/notifyAll 。
- 使用 notifyAll,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。
- 如果没把握,建议 notifyAll ,防止 notify 因为信号丢失而造成程序错误。
为什么 wait, notify 和 notifyAll 这三方法不在 Thread 类里面?
一个很明显的原因是 Java 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。
由于 wait,notify 和 notifyAll 方法都是锁级别的操作,所以把它们定义在 Object 类中,因为锁属于对象。
为什么 wait 和 notify 方法要在同步块中调用?
- Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。
- 还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。
为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
所以,我们不能写 if (condition)
而应该是 while (condition)
,特别是 CAS 竞争的时候。示例代码如下:
synchronized (obj) {
while (condition does not hold) {
obj.wait(); // (Releases lock, and reacquires on wakeup)
}
... // Perform action appropriate to condition
}
sleep、join、yield 方法有什么区别?
1)sleep 方法
在指定的毫秒数内,让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有synchronized
同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常。
sleep 方法,可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。
2)yield 方法
yield 方法和 sleep 方法类似,也不会释放“锁标志”,区别在于:
- 它没有参数,即 yield 方法只是使当前线程重新回到可执行状态,所以执行yield 的线程有可能在进入到可执行状态后马上又被执行。
- 另外 yield 方法只能使同优先级或者高优先级的线程得到执行机会,这也和 sleep 方法不同。
3)join 方法
/**
* 存在两个线程:主线程和线程t
Join,这里所说的调用方就是主线程,主线程调用线程t的Join方法,导致主线程阻塞,
直到t线程执行完毕,才返回到主线程中。
简单理解,在主线程中调用t.Join(),也就是在主线程中加入了t线程的代码,
必须让t线程执行完毕之后,主线程(调用方)才能正常执行。
*
*/
public class JoinThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
for(int i =0;i<100;i++){
System.out.println("a:" + i);
}
}
};
t.start();
bmethod();
// 注意观察执行顺序
t.join();
cmethod();
}
public static void bmethod(){
System.out.println(":bmethod:");
}
public static void cmethod(){
System.out.println(":cmethod:");
}
}
线程的 sleep 方法和 yield 方法有什么区别?
- sleep 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。yield 方法只会给相同优先级或更高优先级的线程以运行的机会。
- 线程执行 sleep 方法后转入阻塞(blocked)状态,而执行 yield 方法后转入就绪(ready)状态。
- sleep 方法声明抛出 InterruptedException 异常,而 yield 方法没有声明任何异常。
- sleep 方法比 yield 方法(跟操作系统 CPU 调度相关)具有更好的可移植性。
为什么 Thread 类的 sleep 和 yield 方法是静态的?
Thread 类的 sleep 和 yield 方法,将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
sleep(0) 有什么用途?
Thread#sleep(0)
方法,并非是真的要线程挂起 0 毫秒,意义在于这次调用 Thread#sleep(0)
方法,把当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread#sleep(0)
方法,是你的线程暂时放弃 CPU ,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作。
你如何确保 main 方法所在的线程是 Java 程序最后结束的线程?
可以使用 Thread 类的 #join()
方法,来确保所有程序创建的线程在 main 方法退出前结束。
interrupted 和 isInterrupted 方法的区别?
1)interrupt 方法
Thread#interrupt()
方法,用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出 InterruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
2)interrupted
Thread#interrupted()
静态方法,查询当前线程的中断状态,并且清除原状态。如果一个线程被中断了,第一次调用 #interrupted()
方法则返回 true
,第二次和后面的就返回 false
了。
3)isInterrupted
Thread#isInterrupted()
方法,查询指定线程的中断状态,不会清除原状态。
什么叫线程安全?
线程安全,是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 是线程安全吗?
Servlet 不是线程安全的,Servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
SpringMVC 是线程安全吗?
不是的,和 Servlet 类似的处理流程。
单例模式的线程安全性?
首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
- 饿汉式单例模式的写法:线程安全
//当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调
//用,单例类的唯一实例将被创建。如果使用饿汉式单例来实现负载均衡器
//LoadBalancer类的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。
class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
- 懒汉式单例模式的写法:非线程安全
//懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例
//化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例,为
//了避免多个线程同时调用getInstance()方法,我们可以使用关键字synchronized
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
- 双检锁单例模式的写法:线程安全
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //创建单例实例
}
}
}
return instance;
}
}
多线程同步和互斥有几种实现方法,都是什么?
1)线程同步
线程同步,是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程间的同步方法,大体可分为两类:用户模式和内核模式。顾名思义:
- 内核模式,就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。内核模式下的方法有:
- 事件
- 信号量
- 互斥量
- 用户模式,就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:
- 原子操作(例如一个单一的全局变量)
- 临界区
2)线程互斥
线程互斥,是指对于共享的进程系统资源,在各单个线程访问时的排它性。
- 当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
- 线程互斥可以看成是一种特殊的线程同步。
怎么检测一个线程是否拥有锁?
调用 Thread#holdsLock(Object obj)
静态方法,它返回 true
如果当且仅当当前线程拥有某个具体对象的锁。代码如下:
10 个线程和 2 个线程的同步代码,哪个更容易写?
从写代码的角度来说,两者的复杂度是相同的,因为同步代码与线程数量是相互独立的。
但是同步策略的选择依赖于线程的数量,因为越多的线程意味着更大的竞争,所以你需要利用同步技术,如锁分离,这要求更复杂的代码和专业知识。
在多线程环境下,SimpleDateFormat 是线程安全的吗?
不是,非常不幸,DateFormat 的所有实现,包括 SimpleDateFormat 都不是线程安全的,因此你不应该在多线程序中使用,除非是在对外线程安全的环境中使用,如将 SimpleDateFormat 限制在 ThreadLocal 中。
如果你不这么做,在解析或者格式化日期的时候,可能会获取到一个不正确的结果。因此,从日期、时间处理的所有实践来说,我强力推荐 joda-time 库。
什么是Java Timer 类?
java.util.Timer
,是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer 类可以用安排一次性任务或者周期任务。
java.util.TimerTask
,是一个实现了 Runnable 接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用 Timer 去安排它的执行。
目前有开源的 Qurtz 可以用来创建定时任务。
你有哪些多线程开发良好的实践?
-
1、给线程命名。
这样可以方便找 bug 或追踪。OrderProcessor、QuoteProcessor、TradeProcessor 这种名字比 Thread-1、Thread-2、Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
-
2、最小化同步范围。
锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
-
3、优先使用
volatile
,而不是synchronized
。 -
4、尽可能使用更高层次的并发工具而非 wait 和 notify 方法来实现线程通信。
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。
其次,这些类是由最好的企业编写和维护在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
-
5、优先使用并发容器,而非同步容器。
这是另外一个容易遵循且受益巨大的最佳实践,并发容器比同步容器的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到 Map ,我们应该首先想到用 ConcurrentHashMap 类。
-
6、考虑使用线程池。
并发编程和并行编程有什么区别?
并发(Concurrency)和并行(Parallellism)是:
- 解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 解释二:并行是在不同实体上的多个事件;并发是在同一实体上的多个事件。
- 解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如 Hadoop 分布式集群。
同步和异步有何异同,在什么情况下分别使用他们?
如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
网友评论