多线程&并发篇之上篇
master,这是我的小站 https://blog.study996.cn ,欢迎访问哦~~
1、说说 Java 中实现多线程有几种方法
创建线程的常用三种方式:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口(JDK1.5>=)
- 线程池方式创建
通过继承 Thread 类或者实现 Runnable 接口、Callable 接口都可以实现多线程,不过实现 Runnable 接口与实现 Callable 接口的方式基本相同,只是 Callable 接口里定义的方法返回值,可以声明抛出异常而已。因此将实现 Runnable 接口和实现 Callable 接口归为一种方式。这种方式与继承 Thread 方式之间的主要差别如下。
采用实现 Runnable、Callable 接口的方式创建线程的优缺点
优点:线程类只是实现了 Runnable 或者 Callable 接口,还可以继承其他类。这种方式下,多个线程可以共享一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用Thread.currentThread()
方法
采用继承 Thread 类的方式创建线程的优缺点
优点:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()
方法,直接使用 this 即可获取当前线程
缺点:因为线程类已经继承了 Thread 类,Java 语言是单继承的,所以就不能再继承其他父类了。
2、如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。
2、使用 stop 方法强行终止,但是不推荐这个方法,因为 stop 和 suspend 及 resume 一样都是过期作废的方法。
3、使用 interrupt 方法中断线程。
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true; // 设置共享变量为 true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠 3 秒以便观察线程 m1 的中断情况
System.out.println("Stopping application...");
}
}
3、notify() 和 notifyAll() 有什么区别?
- notify 可能会导致死锁,而 notifyAll 则不会
- 任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码
- 使用 notifyall,可以唤醒 所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。
- wait() 应配合 while 循环使用,不应使用 if,务必在 wait() 调用前后都检查条件,如果不满足,必须调用 notify() 唤醒另外的线程来处理,自己继续 wait() 直至条件满足再往下执行。
- notify() 是对 notifyAll() 的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet 中等待的是相同的条件,唤醒任一个都能正确处理接下来的事 项,如果唤醒的线程无法正确处理,务必确保继续 notify() 下一个线程,并且自身需要重新回到 WaitSet 中
4、sleep() 和 wait() 有什么区别?
- 对于 sleep() 方法,我们首先要知道该方法是属于 Thread 类中的。而 wait() 方法,则是属于 Object 类中的。
- sleep() 方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用 sleep() 方法的过程中,线程不会释放对象锁。
- 当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify() 方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
5、volatile 是什么?可以保证有序性吗?
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile 关键字会强制将修改的值立即写入主存。
2)禁止进行指令重排序。volatile 不是原子性操作 什么叫保证部分有序性?
当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
x = 2; //语句 1
y = 0; //语句 2
flag = true; //语句 3
x = 4; //语句 4
y = -1; //语句 5
由于flag 变量为 volatile 变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1、语句 2 前面,也不会讲语句 3 放到语句 4、语句 5 后面。但是要注意语句 1 和语句 2 的顺序、语句 4 和语句 5 的顺序是不作任何保证的。
使用 volatile 一般用于 状态标记量 和 单例模式的双检锁。
6、Thread 类中的 start() 和 run() 方法有什么区别?
start() 方法被用来启动新创建的线程,而且 start() 内部调用了 run() 方法,这和直接调用 run() 方法的效果不一样。当你调用 run() 方法的时候,只会是在原来的线程中调用,没有新的线程启动,start() 方法才会启动新线程。
7、为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面?
明显的原因是 JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的 wait() 方法就有意义了。如果 wait() 方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。
8、为什么 wait 和 notify 方法要在同步块中调用?
-
只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的 wait(),notify() 和 notifyAll() 方法。
-
如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。
-
还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。
wait() 方法强制当前线程释放对象锁。这意味着在调用某对象的 wait() 方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的 wait() 方法。
在调用对象的 notify() 和 notifyAll() 方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的 notify() 或 notifyAll() 方法。
调用 wait() 方法的原因通常是,调用线程希望某个特殊的状态 (或变量) 被设置之后再继续执行。调用 notify() 或 notifyAll() 方法的原因通常是,调用线程希望告诉其他等待中的线程:"特殊状态已经被设置"。这个状态作为线程间通信的通道,它必须是一个可变的共享状态 (或变量)。
9、Java 中 interrupted 和 isInterruptedd 方法的区别?
interrupted() 和 isInterrupted() 的主要区别是前者会将中断状态清除而后者不会。Java 多线程的中断机制是用内部标识来实现的,调用 Thread.interrupt() 来中断一个线程就会设置中断标识为 true。当中断线程调用静态方法 Thread.interrupted() 来检查中断状态时,中断状态会被清零。而非静态方法 isInterrupted() 用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出 InterruptedException 异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。
10、Java 中 synchronized 和 ReentrantLock 有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等 待,而进行线程阻塞和唤醒的代价是比较高的。
区别:
这两种方式最大区别就是对于 Synchronized 来说,它是 java 语言的关键字,是原生语法层面的互斥,需要 jvm 实现。而 ReentrantLock 它是 JDK 1.5 之后提供的 API 层面的互斥锁,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成。
Synchronized 进过编译,会在同步块的前后分别形成 monitorenter 和 monitorexit 这个两个字节码指令。在执行 monitorenter 指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加 1,相应的,在执行 monitorexit 指令时会将锁计算器就减 1,当计算器为 0 时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
由于 ReentrantLock 是 java.util.concurrent 包下提供的一套互斥锁,相比 Synchronized,
ReentrantLock 类提供了一些高级功能,主要有以下 3 项:
-
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized 来说可以避免出现死锁的情况。
-
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized 锁非公平锁,ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁表现的性能不是很好。
-
锁绑定多个条件,一个 ReentrantLock 对象可以同时绑定对个对象。
11、有三个线程 T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的 join() 方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个 (T3 调用 T2,T2 调用 T1),这样 T1 就会先完成而 T3 最后完成。
实际上先启动三个线程中哪一个都行,因为在每个线程的 run 方法中用 join 方法限定了三个线程的执行顺序
public class JoinTest2 {
// 1.现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用 t1 线程,等待 t1 线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用 t2 线程,等待 t2 线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();//这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}
12、SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap() 和 Hashtable 一样,实现上在调用 map 所有方法时,都对整个 map 进行同步。而 ConcurrentHashMap 的实现却更加精细,它对 map 中的所有桶加了锁。所以,只要有一个线程访问 map,其他线程就无法进入 map,而如果一个线程在访问 ConcurrentHashMap 某个桶时,其他线程,仍然可以对 map 执行某些操作。
所以,ConcurrentHashMap 在性能以及安全性方面,明显比 Collections.synchronizedMap() 更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历 map 时,如果其他线程试图对 map 进行数据修改,也不会抛出 ConcurrentModificationException。
13、什么是线程安全
线程安全就是说多线程访问同一段代码,不会产生不确定的结果。
又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
(1)不可变
像 String、Integer、Long 这些,都是final 类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java 中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的 类,Java 中也有,比方说 CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像 Vector 这种,add、remove 方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个 Vector、有个线程同时在 add 这个 Vector,99% 的情况下都会出现 ConcurrentModificationException,也就是fail-fast 机制。
(4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap 等都是线程非安全的类
14、Thread 类中的 yield 方法有什么作用?
Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield() 的线程有可能在进入到暂停状态后马上又被执行。
15、Java 线程池中 submit() 和 execute() 方法有什么区别?
两个方法都可以向线程池提交任务,execute() 方法的返回类型是 void,它定义在 Executor 接口中,而 submit() 方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。
16、说一说自己对于 synchronized 关键字的了解
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。另外,在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
17、说说自己对线程池的理解,有哪些优势?
线程池的优势主要体现在以下几个方面: - 降低资源消耗:通过重复利用已存在的线程,避免了频繁创建和销毁线程带来的开销。比如,在 Web 应用中,线程被创建后保持存活状态并等待任务,这样每当有新请求时,就可以立即使用已有线程处理。 - 提高响应速度:当任务到达时,如果线程池中有空闲的线程,则可以立即执行该任务。而无需花费时间去创建一个新线程或者重建一些资源。 - 方便管理资源:通过限制线程数量,能够防止因为大量线程被创建而导致系统资源耗尽,这样有助于保护系统的稳定性和可预测性,从而避免出现崩溃等严重问题。
总结一下,synchronized 的实现机制是通过对象的内部锁(monitor)来实现同步,确保同一时间只有一个线程可以执行特定代码块。而随着技术的进步和优化,现在使用 synchronized 并不会带来太大的性能损失,并且它还具有简单易用的优点,因此在 Java 多线程开发中仍然是一个非常常用的方法。
18、什么是守护线程?
守护线程就是用来服务其他线程的线程。当所有的用户线程死亡之后,守护线程也会随之死亡。 守护线程与普通线程的区别在于: - 用户线程:必须等到程序中的所有线程都终止执行才能结束整个程序。 - 守护线程:在后台运行,并且不会阻止 JVM 的终止。也就是说,当没有存活的用户线程时,JVM 将会自动关闭并终止守护线程。 所以守护线程通常用于一些支持性的、服务型的任务,比如垃圾回收、后台日志记录等。
19、什么是内存屏障?
-
内存屏障(Memory Barriers)是处理多处理器系统中内存一致性问题的一种方式。在现代计算机体系结构中,每个处理器都有自己的缓存,而这些缓存并不一定总是保持一致的。当多个线程在不同的处理器上执行时,可能会出现缓存不一致的问题,导致某些线程看到的数据不是最新的。synchronized 关键字确保在任何时刻只有一个线程可以执行被它保护的代码块或者方法,从而避免了多个线程同时修改共享资源而引起的竞态条件(race condition)和内存一致性问题。通过同步机制,Java 能够为应用程序提供一致且可预测的行为,尤其是在多处理器环境下。此外,synchronized 还有一些变种,比如在方法级别使用 synchronized 修饰符或者在代码块中使用 synchronized(this) 来实现不同的同步粒度。需要注意的是,过度的使用同步可能导致线程之间的竞争变得激烈,从而引发性能问题。
-
内存屏障是一种同步机制,用于保证数据的一致性。例如,当一个线程执行到一个内存屏障时,这个线程之前的所有操作的结果都会被刷写到主存中,并且其他线程的缓存会被清空或者标记为无效,这样其他线程可以读取到最新的数据。 在 Java 中,除了 synchronized 外,还有一些其他的同步机制,比如 ReentrantLock、Semaphore 等,这些机制提供了更高的灵活性和更好的性能,在某些场景下比 synchronized 更适合使用。比如,ReentrantLock 提供了公平锁和非公平锁的选项,并且可以实现多个条件变量的绑定。总的来说,synchronized 是 Java 中最基础也是最简单的一种同步方式,适用于大多数的线程安全问题。理解它的原理和优化机制对于编写高效的并发程序非常重要
20、为什么说 Java 的 volatile 关键字不能保证变量完全有序?
volatile 的作用主要是确保可见性,并且可以防止指令重排,它能确保线程间的内存一致性。但是,由于在多处理器系统中,每个处理器都有自己的本地缓存,而 volatile 变量会被保存到主内存中,当一个线程修改了 volatile 变量的值时,其它线程能够立即看到这个变化。然而,这并不意味着 volatile 能够完全保证代码执行的有序性。 比如,假设有一个由两个线程同时操作的共享变量 x 和 y。如果线程 A 修改了 x,并且另一个线程 B 修改了 y,那么即使这两个操作都是对 volatile 变量进行的操作,也不能保证线程 C 在读取时会看到线程 A 的修改在前还是线程 B 的修改在前。也就是说,volatile 关键字并不能保证操作的顺序性,它仅仅能保证变量值的可见性。
21、如何避免线程安全问题?
- 方式一:同步机制 使用 synchronized 或 ReentrantLock 这样的锁机制,来对共享资源进行同步访问,确保同一时间只能有一个线程在执行关键代码区域。这样可以有效防止数据被多个线程同时修改,造成的数据不一致。
- 方式二:不可变对象 尽可能多地使用不可变对象,比如 String、Integer 等类型,它们一旦被创建就不能再改变其值,这样的对象在多线程环境下访问时就无需考虑同步的问题,因为这些对象的状态不会被任何线程所改变。这种方式不仅能提高程序的效率,而且能提升代码的可读性。
22、什么是自旋锁?
当一个线程试图获取一个已经被其他线程占用的同步资源的时候,它会进入自旋状态,即在原地循环等待,直到资源被释放为止。这可能会减少系统开销,因为在这个过程中,线程不会被阻塞,而是一直处于运行态,只是不断地尝试获取锁。 但是,在某些情况下,比如当锁竞争非常激烈,且每次持有锁的时间非常短的时候,使用自旋锁可能并不划算,甚至会导致性能下降。因此,选择是否使用自旋锁需要根据具体场景来判断。
23、线程池的作用是什么?自己如何理解?
线程池的主要作用就是复用线程和控制最大线程数,从而避免频繁创建和销毁线程带来的资源消耗和性能问题。当我们需要处理大量的任务时,如果能控制在一定数量的线程内进行排队处理,可以提高系统的响应速度和吞吐量,同时避免过多的线程竞争导致系统崩溃。