2023年java多线程面试基础
阿里云国际版折扣https://www.yundadi.com |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
一、说说Java中实现多线程有几种方法
创建线程的常用三种方式
1. 继承Thread类
2. 实现Runnable接口
3. 实现Callable接口 JDK1.5>=
4. 线程池方式创建
通过继承Thread类或者实现Runnable接口、Callable接口都可以实现多线程不过实现Runnable接口与实现Callable接口的方式基本相同只是Callable接口里定义的方法返回值可以声明抛出异常而已。因此将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下。
采用实现Runnable、Callable接口的方式创建线程的优缺点
优点线程类只是实现了Runnable或者Callable接口还可以继承其他类。这种方式下多个线程可以共享一个target对象所以非常适合多个相同线程来处理同一份资源的情况从而可以将CPU、代码和数据分开形成清晰的模型较好的体现了面向对象的思想。
缺点编程稍微复杂一些如果需要访问当前线程则必须使用 Thread.currentThread() 方法
采用继承Thread类的方式创建线程的优缺点
优点编写简单如果需要访问当前线程则无需使用 Thread.currentThread() 方法直接使用
this即可获取当前线程
缺点因为线程类已经继承了Thread类Java语言是单继承的所以就不能再继承其他父类了。
二、线程与进程的区别
进程是操作系统分配资源的最小单元线程是操作系统调度的最小单元。一个程序至少有一个进程,一个进程至少有一个线程。
三、死锁与活锁的区别死锁与饥饿的区别
死锁是指两个或两个以上的进程或线程在执行过程中因争夺资源而造成的一种互相等待的现象若无外力作用它们都将无法推进下去。
产生死锁的必要条件
1、互斥条件所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件一个进程因请求资源而阻塞时对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源在末使用完之前不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁任务或者执行者没有被阻塞由于某些条件没有满足导致一直重复尝试失败尝试失败。
活锁和死锁的区别在于处于活锁的实体是在不断的改变状态所谓的“活” 而处于死锁的实体表现为等待活锁有可能自行解开死锁则不能。
饥饿一个或者多个线程因为种种原因无法获得所需要的资源导致一直无法执
行的状态。
Java 中导致饥饿的原因
1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
2、线程被永久堵塞在一个等待进入同步块的状态因为其他线程总是能在它之前持续地对该同步块进行访问。
3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法)因为其他线程总是被持续地获得唤醒。
四、如何停止一个正在运行的线程
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...");
}
}
五、notify()和notifyAll()有什么区别
notify可能会导致死锁而notifyAll则不会
任何时候只有一个线程可以获得锁也就是说只有一个线程可以运行synchronized 中的代码使用notifyall可以唤醒 所有处于wait状态的线程使其重新进入锁的争夺队列中而notify只能唤醒一个。
wait() 应配合while循环使用不应使用if务必在wait()调用前后都检查条件如果不满足必须调用notify()唤醒另外的线程来处理自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化但它有很精确的应用场景并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件唤醒任一个都能正确处理接下来的事项如果唤醒的线程无法正确处理务必确保继续notify()下一个线程并且自身需要重新回到WaitSet中。
六、sleep()和wait() 有什么区别
对于sleep()方法我们首先要知道该方法是属于Thread类中的。而wait()方法则是属于Object类中的。sleep()方法导致了程序暂停执行指定的时间让出cpu该其他线程但是他的监控状态依然保持者当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中线程不会释放对象锁。当调用wait()方法的时候线程会放弃对象锁进入等待此对象的等待锁定池只有针对此对象调用
notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
七、Thread 类中的start() 和 run() 方法有什么区别
start()方法被用来启动新创建的线程而且start()内部调用了run()方法这和直接调用run()方法的效果不一样。当你调用run()方法的时候只会是在原来的线程中调用没有新的线程启动start()方法才会启动新线程。
八、为什么wait, notify 和 notifyAll这些方法不在thread类里面
明显的原因是JAVA提供的锁是对象级的而不是线程级的每个对象都有锁通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中线程正在等待的是哪个锁就不明显了。简单的说由于waitnotify和notifyAll都是锁级别的操作所以把他们定义在Object类中因为锁属于对象。
九、SynchronizedMap和ConcurrentHashMap有什么区别
SynchronizedMap()和Hashtable一样实现上在调用map所有方法时都对整个map进行同步。而ConcurrentHashMap的实现却更加精细它对map中的所有桶加了锁。所以只要有一个线程访问map其他线程就无法进入map而如果一个线程在访问ConcurrentHashMap某个桶时其他线程仍然可以对map执行某些操作。
所以ConcurrentHashMap在性能以及安全性方面明显比Collections.synchronizedMap()更加有优势。同时同步操作精确控制到桶这样即使在遍历map时如果其他线程试图对map进行数据修改也不会抛出ConcurrentModificationException。
十、什么是线程安全
线程安全就是说多线程访问同一段代码不会产生不确定的结果。
又是一个理论的问题各式各样的答案有很多我给出一个个人认为解释地最好的如果你的代码
在多线程下执行和在单线程下执行永远都能获得一样的结果那么你的代码就是线程安全的。这个问题有值得一提的地方就是线程安全也是有几个级别的
1、不可变
像String、Integer、Long这些都是final类型的类任何一个线程都改变不了它们的值要改变除
非新创建一个因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。
2、绝对线程安全
不管运行时环境如何调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价Java中标注自己是线程安全的类实际上绝大多数都不是线程安全的不过绝对线程安全的类Java中也有比方说CopyOnWriteArrayList、CopyOnWriteArraySet。
3、相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全像Vector这种add、remove方法都是原子操作不会被打断但也仅限于此如果有个线程在遍历某个Vector、有个线程同时在add这个Vector99%的情况下都会出现ConcurrentModificationException也就是fail-fast机制。
4、线程非安全
这个就没什么好说的了ArrayList、LinkedList、HashMap等都是线程非安全的类。
十一、Thread类中的yield方法有什么作用
Yield方法可以暂停当前正在执行的线程对象让其它有相同优先级的线程执行。它是一个静态方法。而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU执行yield()的线程有可能在进入到暂停状态后马上又被执行。
十二、Java线程池中submit() 和 execute()方法有什么区别
两个方法都可以向线程池提交任务execute()方法的返回类型是void它定义在Executor接口中,而submit()方法可以返回持有计算结果的Future对象它定义在ExecutorService接口中它扩展了Executor接口其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
十三、说一说自己对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 另外在 Java 早期版本中synchronized属于重量级锁效率低下因为监视器锁monitor是依赖于底层的操作系统的Mutex Lock 来实现的Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程都需要操作系统帮忙完成而操作系统实现线程之间的切换时需要从用户态转换到内核态这个状态之间的转换需要相对比较长的时间时间成本相对较高这也是为什么早期的synchronized 效率低的原因。庆幸的是在 Java 6 之后Java 官方对从 JVM 层面对synchronized 较大优化所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
十四、说说自己是怎么使用 synchronized 关键字
修饰实例方法: 作用于当前对象实例加锁进入同步代码前要获得当前对象实例的锁 修饰静态方法也就是给当前类加锁会作用于类的所有对象实例因为静态成员不属于任何一个实例对象是类成员 static 表明这是该类的一个静态资源不管new了多少个对象只有一份。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法而线程B需要调用这个实例对象所属类的静态 synchronized 方法是允许的不会发生互斥现象因为访问静态 synchronized 方法占用的锁是当前类的锁而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 修饰代码块: 指定加锁对象对给定对象加锁进入同步代码库前要获得给定对象的锁。 总结 synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中字符串常量池具有缓存功能
十五、 volatile关键字的作用
一旦一个共享变量类的成员变量、类的静态成员变量被volatile修饰之后那么就具备了两层语义
保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的。禁止进行指令重排序。volatile本质是在告诉jvm当前变量在寄存器工作内存中的值是不确定的需要从主存中读取synchronized则是锁定当前变量只有当前线程可以访问该变量其他线程被阻塞住。
volatile仅能使用在变量级别synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性并不能保证原子性synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化synchronized标记的变量可以被编译器优化。