Java八股文大厂面试宝典——第四期(多线程)

当看到这篇文章,那么我们就有一个共同的目标,进大厂,为了大厂梦,冲
Jvva大厂面试第四期:
主要对Java多线程部分中常见的面试题进行了总结

1.创建线程有哪几种方式?

创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口 。
通过继承Thread类来创建并启动线程的步骤如下:
1.定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
2.创建Thread子类的实例,即创建了线程对象。
3.调用线程对象的start()方法来启动该线程。
通过实现Runnable接口来创建并启动线程的步骤如下:
1.定义Runnable接口的实现类,并实现该接口的run()方法,该run()方 法将作为线程执行体。
2.创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,
Thread对象为线程对象。
3.调用线程对象的start()方法来启动该线程。
通过实现Callable接口来创建并启动线程的步骤如下:
1.创建Callable接口的实现类,并实现call()方法,该cal()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3.使用FutureTask对象作为Thread对象的target创建并启动新线程。
4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

2.run()和start()有什么区别?

run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方 法用来启动线程。
调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一-个普通对象,而run()方法也是- -个普通方法,而不是线程执行体。

3.如何实现线程同步?

1.同步方法
即有synchronized关键字修饰的方法,由于java的每个对象都有一-个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意,
synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
2.同步代码块
即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加,上内置锁,从而实现同步。需值得注意的是,同步是一-种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3. Reentrantl ock
Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantL ock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。
4. volatile
volatile关键字为域变量的访问提供了–种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰.final类型的变量。
5.原子变量
在java的util.concurrent atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器)
但不能用于替换Integer。可扩展
Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

4.说一-说sleep()和wait()的区别.

1.sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
2.sleep()可以在任何地方使用,而wait()只 能在同步方法或同步代码块中使用;
3.sleep()不会释放锁,而wait()会释放锁, 并需要通过notify()/notifyAIl()重新获取锁。

5.说一-说notify()、 notifyAll()的区别

●notify()
用于唤醒-一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
●notifyAll()
用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

6.阻塞线程的方式有哪些?

当发生如下情况时,线程将会进入阻塞状态:
●线程调用sleep()方法主动放弃所占用的处理器资源;
●线程调用了一个阻塞式I0方法,在该方法返回之前,该线程被阻塞;
●线程试图获得一一个同步监视器,但该同步监视器正被其他线程所持有;
●线程在等待某个通知(notify) ;
●程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法。

7.说一-说synchronized与Lock的区别

1.synchronized是Java关键字,在JVM层面实现加锁和解锁; Lock是一 个接口,在代码层面实现加锁和解锁。
2.synchronized可以用在代码块上、方法上; Lock只能写在代码里。
3.synchronized在代码执行完或出现异常时自动释放锁; Lock不会自动释放锁,需要在finally中显示释放锁。
4.synchronized会导致线程拿不到锁一-直等待,Lock可以设置获取锁失败的超时时间。
5.synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
6.synchronized锁可重入、不可中断、非公平; Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。

8.谈谈volatile的实现原理

volatile可以保证线程可见性且提供了一-定的有序性,但是无法保证原子性。在JVM底层
volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前 缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2.它会强制将对缓存的修改操作立即写入主存;
3.如果是写操作,它会导致其他CPU中对应的缓存行无效。

9.说说你对AQS的理解

抽象队列同步器AbstractQueuedSynchronizer(以下都简称AQS),是用来构建锁或者其他同步组件的骨架类,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作的顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是等待。
AQS采用模板方法模式,在内部维护了n多的模板的方法的基础_上,子类只需要实现特定的几个方法(不是抽象方法!不是抽象方法!不是抽象方法! ),就可以实现子类自己的需求。
基于AQS实现的组件,诸如:
●ReentrantLock可重入锁(支持公平和非公平的方式获取锁)
●Semaphore计数信号量;
●ReentrantReadWriteLock读写锁。

10.多线程和单线程的区别和联系

1、在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流 占用 CPU 的机制。
2、多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需 要的时间比一个线程的进程执行两次所需要的时间要多一些。
结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的 响应时间。

11.简述线程、程序、进程之间的联系和关系

线程
与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与 进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在 各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序
是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程
是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个 进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令 接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出 设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更 小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程 中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可 以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

12.什么是线程安全?

线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。 但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized , 就是一段代码同时只能有一个线程来操作 不然中间过程可能会 产生不可预制的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运 行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

13.在多线程中什么是上下文切换

单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU 分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程 时同时执行的,时间片一般是几十毫秒(ms)。
操作系统中,CPU时间分片切换到另一个就绪的线程,则需要保存当前线程的运行的位置,同时需要加 载需要恢复线程的环境信息。

14.什么是竞态条件?你怎么发现和解决竞争?

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
在临界区中使用适当的同步就可以避免竞态条件。
界区实现方法有两种,一种是用synchronized ,一种是用Lock显式锁实现。

15.用户线程和守护线程有什么区别

如果JVM中所有的线程都是守护线程,那么JVM就会退出,进而守护线程也会退出。
如果JVM中还存在用户线程,那么JVM就会一直存活,不会退出。
由此可以得到:
守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。
用户线程是独立存在的,不会因为其他用户线程退出而退出。
默认情况下启动的线程是用户线程,通过setDaemon(true)将线程设置成守护线程,这个函数务必在线程启动前进行调用,否则会报java.lang.IllegalThreadStateException异常,启动的线程无法变成守护线程,而是用户线程。

16sleep()和wait() 有什么区别?

对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,
当指定的时间到了又会自动恢复运行状态
在调用 sleep()方法的过程中, 线程不会释放对象锁。
而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,
只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

17.volatile 是什么?可以保证有序性吗?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层 语义:
1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对 其他
线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
2、禁止进行指令重 排序 。
volatile 不是原子性操作
什么叫保证有序性?
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且 结果
已经对后面的操作可见;在其后面的操作肯定还没有进行;
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5由于flflag变量为volatile变量,那么在进行指令重 排序 的过程的时候,不会将语句3放到语句1、语句 2前
面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺 序是不作
任何保证的。
使用 Volatile 一般用于 状态标记量 和 单例模式的双检锁。

18.Java线程池中submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,
execute()方法的返回类型是void,它定义在Executor接口中,
submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,
它扩展了 Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法

19简述一下你对线程池的理解如果问到了这样的问题,可以展开的说一下(线程池如何用、线程池的好处、线程池的启动策略)合理利用线程池能够带来三个好处。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,
还会降 低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

20.线程的部分方法使用

1.sleep()// 强迫一个线程睡眠N毫秒。
2.isAlive()// 判断一个线程是否存活。
3.join()// 等待线程终止。
4.activeCount()// 程序中活跃的线程数。
5.enumerate()// 枚举程序中的线程。
6.currentThread()//得到当前线程。
7.isDaemon()// 一个线程是否为守护线程。
8.setDaemon()// 设置一个线程为守护线程。 (用户线程和守护线程的区别在于,是否等待主线程依 赖于主线程结束而结束)
9.setName()// 为线程设置一个名称。
10.wait()// 强迫一个线程等待。
11.notify()// 通知一个线程继续运行。
12.setPriority()// 设置一个线程的优先级。
13.getPriority()// 获得一个线程的优先级。

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小尘要自信

不要打赏,我不配。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值