Java——10个关于Java中多线程并发的面试题
Posted 张起灵-小哥
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——10个关于Java中多线程并发的面试题相关的知识,希望对你有一定的参考价值。
1.多线程的创建方式有几种?
答案:应该是3种。
1.1 继承Thread类,重写run方法
/**
* 实现线程的第一种方式: 直接继承java.lang.Thread,重写run方法
* 怎么创建线程? new就可以了
* 怎么启动线程? start就可以了
*/
class MyThread extends Thread {
@Override
public void run() {
//这段程序运行在分支线程中(分支栈)
for (int i=0;i<10;i++) {
System.out.println("分支线程---> " + i);
}
}
}
public class ThreadTest02 {
public static void main(String[] args) {
//新建一个分支线程对象
MyThread myThread=new MyThread();
//启动线程
//start方法的作用是:启动一个分支线程,在JVW中开辟一个新的栈空间
//只要栈空间开辟出来,start方法就结束了,线程就启动成功了,启动成功的线程会自动调用run方法
//run方法在分支线程的栈底部,main方法在主线程的栈底部,run和main是平级的
myThread.start();
//下面的代码运行在主线程中
for (int i=0; i<10; i++) {
System.out.println("主线程---> " + i);
}
}
}
1.2 实现Runnable接口,重写run方法
/**
* 实现线程的第二种方式: 实现java.lang.Runnable接口,实现run方法
*/
class MyRunnable implements Runnable {
@Override
public void run() {
//分支线程
for (int i=0;i<10;i++) {
System.out.println("分支线程---> " + i);
}
}
}
public class ThreadTest03 {
public static void main(String[] args) {
//将一个可运行对象封装成一个线程对象
Thread thread=new Thread(new MyRunnable());
//启动线程
thread.start();
//主线程
for (int i=0; i<10; i++) {
System.out.println("主线程---> " + i);
}
}
}
1.3 实现Callable接口,重写call方法,其中采用线程池
package com.szh.thread;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
/**
* 实现线程的第三种方式:实现Callable接口
*/
class MyCallable implements Callable<Object> {
private String taskNum;
public MyCallable(String taskNum) {
this.taskNum=taskNum;
}
@Override
public Object call() throws Exception {
System.out.println(">>> " + taskNum + "任务启动");
Date dateTest1=new Date();
Thread.sleep(1000);
Date dateTest2=new Date();
long time=dateTest2.getTime() - dateTest1.getTime();
System.out.println(">>> " + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}
public class ThreadTest16 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("-----程序开始运行-----");
Date date1=new Date();
int taskSize=5;
//创建一个线程池
ExecutorService pool= Executors.newFixedThreadPool(taskSize);
//创建具有多个返回值的任务
List<Future> list=new ArrayList<>();
for (int i=0;i<taskSize;i++) {
Callable callable=new MyCallable(i + " ");
//执行任务并获取Future对象
Future future=pool.submit(callable);
//添加到list集合中
list.add(future);
}
//关闭线程池
pool.shutdown();
//获取所有并发任务的运行结果
for (Future f : list) {
//从Future对象上获取到任务的返回值,打印输出到控制台
System.out.println(">>> " + f.get().toString());
}
Date date2=new Date();
System.out.println("-----程序结束运行-----,程序运行时间【" + (date2.getTime()-date1.getTime()) + "毫秒】");
}
}
2.说说Runnable和Callable的区别?
- Callable能够抛出checked exception(受检异常),而Runnable不可以。
- Callable可以返回一个泛型V,而Runnable不可以。
- Callable可以获取线程的执行结果,Runnabl不可以。
3.说说wait和sleep方法的不同?
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。
4.synchronized和volatile关键字的作用、区别?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
● 保证了不同线程对这个变量进行操作时的可见性,不保证原子性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
● 禁止进行指令重排序。
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法级别的。
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
5.什么是线程池,如何使用?
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。
5.1 创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
5.2 创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
5.3 创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
5.4 创建一个固定大小的线程池,支持定时及周期性任务执行。
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(10);
6.线程池的启动策略是什么?
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用execute()方法添加一个任务时,线程池会做如下判断:
(1)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
(2)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
(3)如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程运行这个任务;
(4)如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
(5)当一个线程完成任务时,它会从队列中取下一个任务来执行。
(6)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
7.说说synchronized和Lock的区别?
- synchronized是关键字,Lock是类。
- synchronized无法获取锁的状态,Lock可以。
- synchronized会自动释放锁,Lock需要手动。
- synchronized没有Lock锁灵活(Lock锁可以自己定制)。
8.请写一段简单的死锁代码
class MyThread1 extends Thread {
Object o1;
Object o2;
public MyThread1(Object o1,Object o2) {
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1,Object o2) {
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
}
}
}
}
public class DeadLock {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
//这两个线程共享o1、o2对象
Thread thread1=new MyThread1(o1,o2);
Thread thread2=new MyThread2(o1,o2);
//启动线程
thread1.start();
thread2.start();
}
}
9.请说出同步线程及线程调度相关的方法?
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
注意:java 5通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调。
10.非阻塞算法CAS
首先我们需要了解悲观锁和乐观锁
悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。有一个经典比喻,“如果你不锁门,那么捣蛋鬼就回闯入并搞得一团糟”,所以“你只能一次打开门放进一个人,才能时刻盯紧他”。
乐观锁:假定并发环境是乐观的,即虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,“如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道”,所以“你大可以放进所有人,等发现他们想破坏的时候再做决定”。通常认为乐观锁的性能比悲观所更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争则只发生在最小的并发冲突处,如果用悲观锁来理解,就是“锁的粒度最小”。但乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。首先保证正确性,有必要的话,再去追求性能。
乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个CAS指令,实现“Compare And Swap”的语义(这里的swap是“换入”,也就是set),构成了基本的乐观锁。CAS包含3个操作数:
需要读写的内存位置V
进行比较的值A
拟写入的新值B
当且仅当位置V的值等于A时,CAS才会通过原子方式用新值B来更新位置V的值;否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。一个有意思的事实是,“使用CAS控制并发”与“使用乐观锁”并不等价。CAS只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。
以上是关于Java——10个关于Java中多线程并发的面试题的主要内容,如果未能解决你的问题,请参考以下文章