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,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单多了。
问题1:多线程和单线程的区别和联系?
1、在单核CPU中,将CPU分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用CPU的机制。
2、多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。
结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。
问题2:简述线程、程序、进程的基本概念。以及他们之间关系是什么?
线程:
与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被成为轻量级进程。
程序:
是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程:
是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程创建、运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占用某些系统资源如CPU时间、内存时间、文件、输入输出设备的使用权等等。换句话说,在程序执行时,将会被操作系统载入内存中。线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一个进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
问题3:线程的创建方式有哪些?
1、继承Thread类,作为线程对象存在;
2、实现Runnable接口,作为线程任务存在;
3、匿名内部类创建线程对象;
4、创建带返回值的线程;
5、定时器Timer;
6、线程池创建线程;
7、利用java8新特性stream实现并发。
问题4:线程有哪些基本状态
1、初始状态;
2、运行状态;
3、阻塞状态;
4、等待状态;
5、超时等待状态;
6、终止状态。
问题5: 如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止;
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法;
3、使用interrupt方法中断线程。
问题6:start()方法和run()方法的区别
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。
如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
问题7:为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
首先看一下源码说明:
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果。如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。
问题8:Runnable接口和Callable接口的区别
1、Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
2、Callable接口中的call()方法是具有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,只能等待多线程任务执行完毕。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,这一点非常有用。
问题9:什么是线程安全?
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有成员,那么一定是线程安全的。但是,多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized,就是一段代码同时只能有一个线程来操作,不然中间过程可能会产生不可预知的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量值也和预期的是一样的,就是线程安全的。
问题10:线程的状态转换?
1、新建状态:新创建一个线程对象;
2、就绪状态:线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权;
3、运行状态:就绪状态的线程获取了CPU,执行程序代码;
4、阻塞状态:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态,阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中;
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;
- 其他阻塞: 运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态:线程执行完毕或者因异常退出了run()方法,该线程结束生命周期。
问题11:在多线程中,什么是上下文切换?
操作系统中,CPU时间分片切换到另一个就绪线程需要保存当前线程的运行位置,同时加载需要恢复线程的环境信息,这一过程称为上下文切换。
问题12:如何确保线程安全?
- 对非安全的代码进行加锁控制
- 使用线程安全的类
- 多线程并发情况下,线程共享的变量改为方法级的局部变量
问题13:线程安全级别
- 不可变:不可变的对象一定是线程安全的,并且永远也不需要额外同步;如:Integer、String和BigInteger。
- 无条件的线程安全:由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要额外的同步;如:Random、ConcurrentHashMap、Concurrent集合、atomic。
- 有条件的线程安全:有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。如:Hashtable、Vector、返回的迭代器。
- 非线程安全(线程兼容):线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用;如:ArrayList、HashMap。
- 线程对立:线程对立是那些不管是否采用了同步措施,都不能在多线程环境中使用的代码;如:System.setOut()、System.runFinalizersOnExit()。
问题14 volatile关键字的作用
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
- 禁止进行指令重排序
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。
问题15 volatile变量和atomic变量有什么不同
volatile变量和atomic变量看起来很像,但功能却不一样
volatile变量可以确保先行关系,即写操作会发生在后续的操作之前,但它并不能保证原子性。例如用volatile修饰count变量那么count++操作就不是原子性的。
而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加1,其他数据类型和引用变量也可以进行相似的操作。
问题16 sleep方法和wait方法有什么区别
对于sleep方法属于Thread类,wait方法属于Object类
sleep方法导致了程序暂停执行指定的时间,让出CPU给其他线程,但是它的监控状态依然保持着,当指定时间到了又会自动恢复运行状态。在调用sleep方法的过程中,线程不会释放对象锁。
当调用wait方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify方法后本线程才进入对象锁定池,准备获取对象锁进入运行状态。
问题17 Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也获取到CPU控制权,可以使用Thread.sleep(0)手动出发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
问题18 线程类的构造方法、静态块是被那个线程调用的
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
问题19 在线程中如何处理不可控异常?
在Java中有两种异常:
非运行时异常(Checked Exception),这种异常必须在方法声明的throws语句指定,或者在方法体内捕获,例如:IOException和ClassNotFoundException。
运行时异常(Unchecked Exception):这种异常不必在方法声明中指定,也不需要在方法体中捕获,例如:NumberFormatException。
因为run()方法不支持throws语句,所以当线程对象的run()方法抛出非运行时异常,必须捕获并处理它们,如果非运行时异常从run()方法中抛出,默认行为是在控制台输出堆栈记录并且退出程序。
此外,Java提供了一种在线程对象中捕获和处理运行时异常的一种机制。在程序中实现UncaughtExceptionHandler接口,重写uncaughtException()方法即可,示例如下所示。
package concurrency;
import java.lang.Thread.UncaughtExceptionHandler;
public class Main2 {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
class Task implements Runnable{
@Override
public void run() {
int numero = Integer.parseInt("TTT");
}
}
class ExceptionHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("An exception has been captured\n");
System.out.printf("Thread: %s\n", t.getId());
System.out.printf("Exception: %s: %s\n",
e.getClass().getName(),e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n",t.getState());
}
}
当一个线程抛出了异常并且没有被捕获时(这种情况只可能是运行时异常),JVM检查这个线程是否被预置了未捕获异常处理器。如果找到,JVM将调用线程对象的这个方法,并将线程对象和异常作为接入参数。
Thread类还有另一个方法可以处理未捕获的异常,即静态方法setDefaultUncaughtExceptionHandler()。这个方法在应用程序中为所有的线程对象创建一个异常处理器。
当线程抛出一个未捕获的异常时,JVM将为异常寻找以下三种可能的处理器。
- 首先,它查找线程对象的未捕获异常处理器;
- 如果找不到,JVM继续查找线程对象所在的线程组(ThreadGroup)的未捕获异常处理器;
- 如果还是找不到,JVM将继续查找默认的未捕获异常处理器;
- 如果没有一个处理器存在,JVM则将堆栈异常记录打印到控制台,并退出程序。
问题20 什么是CAS?
CAS,全称为Compare and Swap,即比较-替换。
假设有三个操作数:内存值V,旧的预期值A,要替换的值B,当且仅当A=V时,才会将内存值修改为B并返回true,否则什么也不做并返回false。当然CAS一定要volatile变量配合,这样才
能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会
变的值A,只要某次CAS操作失败,永远都不可能成功。
CAS的缺陷:
- ABA问题;
- 循环时间长开销;
- 只能保证一个变量的原子操作。
问题21 线程池的作用
- 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度,当任务到达时,任务可以不需要等待线程创建就能立即执行;
- 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
补充:
1 线程阻塞的方法有哪些?
- 线程睡眠sleep()方法
sleep()方法允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU时间,指定时间已一过,线程重新进入可执行状态。
- 线程让步 yield() 方法
yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用
yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
yield()方法调用会考虑线程的优先级。如果一个线程调用了yield()方法,它会放弃CPU一段时间,以便优先级相同的或更高的线程可以获取CPU。
- 线程融合 join()方法
在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
线程挂起 suspend() 和 resume() 方法
方法已废弃
- 线程等待 wait() 和 notify() 方法
wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。这一对方法会释放占用的锁。
2 锁的实现方式有哪些?
- synchronized关键字: synchronized关键字是Java中最基本的锁机制。通过在方法或代码块前添加synchronized关键字,可以保证同一时间只有一个线程能够执行该方法或代码块。synchronized关键字使用起来简单方便,但它的粒度较大,只能对整个方法或代码块进行加锁;
- ReentrantLock类: ReentrantLock是java.util.concurrent包中提供的锁的实现类,它提供了灵活的锁机制,支持可重入、公平锁和非公平锁;
ReentrantLock是Java.util.concurrent包中提供的一个可重入锁实现类。与synchronized关键字相比,ReentrantLock提供了更多的灵活性和功能。可以使用lock()方法获取锁,使用unlock()方法释放锁。与synchronized关键字不同的是,ReentrantLock可以实现公平锁和非公平锁,并且可以通过tryLock()方法尝试获取锁,避免线程长时间等待。
- ReadWriteLock接口: ReadWriteLock是java.util.concurrent包中提供的读写锁接口,它提供了读锁和写锁两种不同的锁机制,可以更好的提高并发读取的性能;
ReadWriteLock接口是Java.util.concurrent包中提供的读写锁机制。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。通过使用读写锁,可以提高多线程读取操作的并发性能。ReadWriteLock接口提供了读锁和写锁的分离,读锁可以同时被多个线程获取,写锁只能被一个线程获取。
- StampedLock类: StampedLock是java.util.concurrent包中提供的锁实现类,它提供了乐观读锁、悲观读锁和写锁三种模式,可以根据具体需求选择不同的锁模式;
StampedLock是Java 8中新增的一种锁机制,它提供了一种乐观读锁的实现方式。与传统的读写锁相比,StampedLock在读操作较多的情况下,可以提供更好的性能。StampedLock使用乐观读锁时,不会阻塞写锁的获取,而是在获取乐观读锁后,通过验证数据版本号是否发生变化来判断读操作是否有效。
- LockSupport类: LockSupport是java.util.concurrent包中提供的线程阻塞工具类,它可以实现线程的阻塞和唤醒操作,可以配合自定义的标志位实现简单的锁机制;
Java提供了多种实现锁的方式,每种方式都有其适用的场景和特点。synchronized关键字简单易用,适合在单线程或少量线程并发的情况下使用;ReentrantLock类提供了更多的功能和灵活性,适用于复杂的多线程并发场景;ReadWriteLock接口适用于读多写少的场景,可以提高读操作的并发性能;StampedLock类提供了乐观读锁的实现方式,适用于读操作较多的场景。 根据具体的需求和场景,选择合适的锁机制可以提高多线程程序的性能和可靠性。
3 线程池的实现方式有哪些?
- Executors.newFixedThreadPool: 创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
- Executors.newCachedThreadPool: 创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
- Executors.newSingleThreadExecutor: 创建单个线程数的线程池,它可以保证先进先出的执行顺序。
- Executors.newScheduledThreadPool: 创建一个可以执行延迟任务的线程池。
- Executors.newSingleThreadScheduledExecutor: 创建一个单线程的可以执行延迟任务的线程池。
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。