大家好,我是讯享网,很高兴认识大家。
本文主要总结了多线程的问题,所以列出了多线程的40个问题。
这些多线程的问题有的来自各大网站,有的来自我自己的思考。网上可能有一些问题,有一些相应的回答,或者有网友看过。不过这篇文章的重点是,所有问题都会按照自己的理解来回答,不会看网上的答案。所以,有些问题可能是错的。如果你能改正,请随时给我你的意见。
另外,我整理了一份大厂1000多道面试真题的清单。希望对准备春招的同学有帮助。
本文可通过转发+关注+私信获得[0208]
1、多线程有什么用?
一个在很多人看来很扯淡的问题:我希望我能使用多线程。有什么用?在我看来,这个回答更是扯淡。所谓& # 34;知道为什么& # 34;,”会用& # 34;只有& # 34;知道吗& # 34;,”为什么用& # 34;只有& # 34;知道为什么& # 34;,最多只能& # 34;知道为什么& # 34;程度可以说是一个知识点运用自如。好吧,下面是我对这个问题的看法:
1)利用多核CPU
随着产业的进步,至少笔记本、台式机甚至商用应用服务器都是双核的,4核、8核甚至16核的也屡见不鲜。如果是单线程程序,50%浪费在双核CPU上,75%浪费在4核CPU上。所谓& # 34;单核CPU上的多线程& # 34;那是假的多线程。同时,处理器只能处理一块逻辑,但线程切换更快,看起来像多线程& # 34;与此同时& # 34;只是跑步。多核CPU上的多线程才是真正的多线程。可以让你的多段逻辑同时工作,多线程,真正发挥多核CPU的优势,达到充分利用CPU的目的。
2)防止堵塞
从程序运行效率来看,单核CPU不会充分发挥多线程的优势,反而会因为在单核CPU上运行多线程而切换线程上下文,降低整体程序效率。但是我们还是要把多线程应用到单核CPU上,只是为了防止阻塞。试想一下,如果单核CPU使用的是单线程,那么只要这个线程被阻塞了,比如远程读取一些数据,而peer长时间不返回,没有设置超时,那么你的整个程序就会在数据返回之前停止运行。多线程可以防止这个问题。多个线程同时运行,即使一个线程的代码执行阻塞了读取数据,也不会影响其他任务的执行。
3)易于建模。
这是另一个不那么明显的优势。假设有一个很大的任务A,单线程编程,要考虑的事情很多,建立整个程序模型很麻烦。但是如果把这个大任务A分解成几个小任务,任务B,任务C,任务D,分别建立程序模型,这些任务多线程运行,那就简单多了。
2、创建线程的方式
常见的问题,一般有两种:
1)继承线程类
2)实现可运行的接口
至于哪种更好,不言而喻,后者肯定更好,因为实现接口的方式比继承类的方式更灵活,也能降低程序间的耦合度。面向接口编程也是设计模式六大原则的核心。
3、start()方法和run()方法的区别
只有调用start()方法时,才会显示多线程特性,不同线程的run()方法中的代码会交替执行。如果只调用run()方法,那么代码将同步执行。必须等到一个线程的run()方法中的所有代码都执行完之后,另一个线程才能执行其run()方法中的代码。
4、Runnable接口和Callable接口的区别
有点深。它还显示了Java程序员可以学习的知识的广度。
Runnable接口中run()方法的返回值是void,它做的只是执行run()方法中的代码;Callable接口中的call()方法有一个返回值,它是一个泛型类型。当它与Future和FutureTask配合使用时,可以用来获得异步执行的结果。
其实这是一个非常有用的特性,因为多线程比单线程更难更复杂的一个重要原因就是多线程充满了未知。线程被执行了吗?一个线程运行了多长时间?当一个线程执行的时候,我们所期望的数据被分配了吗?我不知道。我们所能做的就是等待这个多线程任务完成。但是Callable+Future/FutureTask可以获得多线程运行的结果,在等待太久没有获得所需数据的时候可以取消这个线程的任务,真的很有用。
5、CyclicBarrier和CountDownLatch的区别
两个类似的类,都在java.util.concurrent下,可以用来指示代码运行到某个点。它们之间的区别是:
1)1)cyclic barrier的一个线程运行到某一点后,该线程停止运行,直到所有线程到达该点,所有线程才会再次运行;CountDownLatch不是。线程运行到某一点后,只是给某个值-1,线程继续运行。
2)CyclicBarrier只能唤醒一个任务,CountDownLatch可以唤醒多个任务。
3) CyclicBarrier可以重用,CountDownLatch不能重用。如果计数值为0,则不能再次使用CountDownLatch。
6、volatile关键字的作用
每个学习和应用多线程的Java程序员必须掌握的一个非常重要的问题。理解volatile关键字的作用的前提是理解Java内存模型,这里就不说Java内存模型了。请参考第31点。volatile关键字有两个主要功能:
1)多线程主要关注两个特性:可见性和原子性。用volatile关键字修饰的变量保证了它们在多线程中的可见性,也就是说,每次读取volatile变量,都必须是最新的数据。
2)代码的底层执行不像我们看到的高级语言Java程序那么简单。它的执行是Java代码->: Code->:根据字节码执行相应的C/C++代码->: C/C++代码编译成汇编语言->:而在现实中,为了获得更好的性能,JVM可能会对指令进行重新排序,多线程下可能会出现一些意想不到的问题。使用volatile会将被禁止的语义重新排序,这当然在一定程度上降低了代码执行的效率。
从实用的角度来看,volatile的一个重要作用就是和CAS结合,保证原子性。具体可以参考java.util.concurrent.atomic包下的类,比如AtomicInteger。
7、什么是线程安全
又是一个理论问题,答案很多。我举一个我个人认为最好解释的:如果你的代码在多线程和单线程下执行时总能得到相同的结果,那么你的代码就是线程安全的。
关于这个问题值得一提的是,线程安全也有几个级别:
1)不可变
String、Integer和Long都是类的最终类型,除非创建一个新的线程,否则任何线程都不能更改它们的值。因此,这些不可变对象可以直接在多线程环境中使用,而无需任何同步手段。
2)绝对线程安全
无论运行时环境如何,调用者都不需要额外的同步措施。做这件事通常要花很多额外的钱。Java将自己标记为线程安全的类,但大多数都不是。但是Java中绝对有线程安全的类,比如CopyOnWriteArrayList和CopyOnWriteArraySet。
3)相对线程安全
相对线程安全就是我们通常所说的线程安全。和Vector一样,add和remove方法都是原子操作,不会中断,但也仅限于此。如果一个线程正在遍历一个向量,同时一个线程正在添加向量,99%的情况下会出现ConcurrentModificationException,这是一种快速失败机制。
4)线程不安全。
这个没什么好说的。ArrayList、LinkedList、HashMap等。都是线程不安全的类。
8、Java中如何获取到线程dump文件
线程转储是解决死循环、死锁、阻塞、页面打开缓慢等问题的最佳方式。所谓线程转储,也就是线程栈。获取线程堆栈有两个步骤:
1)获取线程的pid,可以在Linux环境下使用jps命令或者ps -ef | grep java。
2)打印线程栈,可以使用jstack pid命令或者Linux环境下的kill -3 pid。
此外,Thread类提供了一个getStackTrace()方法,该方法也可用于获取线程堆栈。这是一个实例方法,所以这个方法被绑定到一个特定的线程实例上,每次你得到它的时候,你就得到一个特定线程当前运行的堆栈。
9、一个线程如果出现了运行时异常会怎么样
如果没有捕获到这个异常,线程将停止执行。另外很重要的一点是:如果这个线程持有一个对象的监视器,那么这个对象监视器会被立即释放。
10、如何在两个线程之间共享数据
只是线程之间共享对象,然后通过wait/notify/notifyAll、await/signal/signalAll唤醒等待。例如,BlockingQueue是为线程间共享数据而设计的。
11、sleep方法和wait方法有什么区别
这个问题经常被问到。sleep方法和wait方法都可以用于在一定时间内放弃CPU。不同的是,如果线程持有一个对象的监视器,sleep方法不会放弃该对象的监视器,而wait方法会放弃该对象的监视器。
12、生产者消费者模型的作用是什么
这个问题是理论性的,但很重要:
1)通过平衡生产者的生产能力和消费者的消费能力,提高整个系统的运行效率,是生产者-消费者模型最重要的作用。
2)脱钩,这是生产者-消费者模型的附带功能。脱钩是指生产者和消费者之间的联系越少,联系越少,就越能独立发展,不受相互制约。
13、ThreadLocal有什么用
简单来说,ThreadLocal就是用空改变时间的一种方式。在每个线程中,一个ThreadLocal。保留了open address方法实现的ThreadLocalMap。数据是隔离的,不共享,自然就不会有线程安全问题。
14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
这是JDK强制执行的。wait()方法和notify()/notifyAll()方法在被调用之前必须获得对象的锁。
15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别
wait()方法和notify()/notifyAll()方法在放弃对象监视器时的区别在于,wait()方法立即释放对象监视器,而notify()/notifyAll()方法在放弃对象监视器之前等待其余线程代码的执行。
16、为什么要使用线程池
避免频繁创建和销毁线程,实现线程对象的重用。另外,使用线程池可以根据项目灵活控制并发的数量。单击此处了解有关线程池的更多信息。
17、怎么检测一个线程是否持有对象监视器
我也是在网上看到一个多线程面试的问题才知道有一种方法可以判断一个线程是否持有一个对象监视器:thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被一个线程持有时,这个方法才会返回true。注意,这是一个静态方法,这意味着& # 34;一根线& # 34;引用当前线程。
18、synchronized和ReentrantLock的区别
Synchronized和if,else,for,while是同一个关键字,ReentrantLock是一个类,这是两者的本质区别。由于ReentrantLock是一个类,它提供了比synchronized更多更灵活的特性。它可以被继承,有方法,有各种类变量。ReentrantLock over synchronized的可扩展性体现在几个方面:
(1)ReentrantLock可以设置获取锁的等待时间,从而避免死锁。
(2)ReentrantLock可以获取各种锁的信息。
(3)ReentrantLock可以灵活实现多渠道通知。
另外,两者的锁定机制其实是不一样的。ReentrantLock底层调用Unsafe的park方法进行锁定,同步的操作应该是对象头中的mark word,这个我不太清楚。
19、ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发性是segment的大小,默认为16,也就是说最多可以有16个线程同时操作ConcurrentHashMap,这也是ConcurrentHashMap比Hashtable最大的优势。无论如何,Hashtable可以同时有两个线程来获取Hashtable中的数据吗?
20、ReadWriteLock是什么
首先明确一点,不是说ReentrantLock不好,只是ReentrantLock有时候也有局限性。如果使用ReentrantLock,可能是本身为了防止线程A写数据和线程B读数据造成的数据不一致。但这样一来,如果线程C在读数据,线程D在读数据,读数据并不会改变数据。不需要锁定,但还是锁定了,降低了程序的性能。
正因为如此,读写锁诞生了。Readlock是读写锁接口,ReentrantReadWriteLock是读写锁接口的具体实现,实现了读写分离。读锁是共享的,写锁是独占的。读和读不是互斥的,而是读和写,写和读,写和写是互斥的,提高了读和写的表现。
21、FutureTask是什么
实际上,如前所述,FutureTask的意思是异步操作任务。FutureTask可以传入一个可调用的具体实现类,可以等待这个异步操作任务的结果,判断是否已经完成,取消任务等等。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放到线程池中。
22、Linux环境下如何查找哪个线程使用CPU最长
这是一个比较实际的问题,我觉得挺有意义的。您可以这样做:
(1)获取项目的pid,jps或者ps -ef | grep java,前面已经提到了。
(2)top -H -p pid,顺序不能改变。
这样就可以打印出当前项目和每个线程占用CPU时间的百分比。注意,这里输入的是LWP,也就是操作系统本机线程的线程号。我没有在笔记本山部署Linux环境下的Java项目,所以没有办法截图演示。如果公司把项目部署在Linux环境下,网友朋友们可以试一试。
使用& # 34;顶部高压管道仪表流程图& # 34;+”jps pid & # 34很容易找到占用CPU高的线程的线程栈,从而定位占用CPU高的原因。通常,不正确的代码操作会导致无限循环。
最后一点,& # 34;顶部高压管道仪表流程图& # 34;键入的LWP是十进制的,& # 34;jps pid & # 34键入的本地线程号是十六进制的,一旦转换就可以定位到CPU高的线程的当前线程栈。
23、Java编程写一个会导致死锁的程序
第一次看到这个题目的时候,我觉得这是一个非常好的问题。很多人都知道死锁是怎么回事:线程A和线程B互相等待对方的锁,导致程序无限循环。当然,仅此而已。问怎么写死锁程序,你就不知道了。这种情况,说白了就是你不知道什么是死锁。如果你知道一个理论,你就完成了。实际上,死锁的问题基本上是看不见的。
其实不难理解什么是死锁。有几个步骤:
1)两个线程中有两个Object对象:lock1和lock2。这两个锁被用作同步代码块的锁;
2)在线程1的run()方法中,同步代码块首先获取lock1的对象锁Thread.sleep(xxx),这个过程不需要太多时间。差不多50毫秒,然后获取lock2的对象锁。这主要是为了防止线程1在启动时一次连续获得lock1和lock2对象的对象锁。
3)线程2的运行)(在方法中,同步代码块首先获取lock2的对象锁,然后获取lock1的对象锁。当然,lock1的对象锁已经被线程1持有,线程2必须等待线程1释放lock1的对象锁。
这样,线程1 & # 34;睡眠& # 34;休眠后,线程2已经获取了lock2的对象锁,线程1在试图获取lock2的对象锁时被阻塞,此时形成死锁。我就不写代码了,会多占一点空间。Java多线程7:死锁包含在本文中,是上述步骤的代码实现。
24、怎么唤醒一个阻塞的线程
如果线程被调用wait()、sleep()或join()方法阻塞,可以通过抛出InterruptedException来中断线程并唤醒它;如果线程被IO阻塞了,那就没办法了,因为IO是操作系统实现的,Java代码没有办法直接接触操作系统。
25、不可变对象对多线程有什么帮助
如前所述,不可变对象保证了对象的内存可见性,不可变对象的读取不需要额外的同步手段,提高了代码执行的效率。
26、什么是多线程的上下文切换
多线程上下文切换是指CPU控制权从一个已经运行的线程切换到另一个已经准备好等待CPU执行权的线程的过程。
27、如果你提交任务时,线程池队列已满,这时会发生什么
这里有一个区别:
1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列,那就无所谓了。继续将任务添加到阻塞队列中并等待执行,因为LinkedBlockingQueue几乎可以被认为是一个无限队列,可以无限期地存储任务。
2)如果使用ArrayBlockingQueue之类的有界队列,任务将首先被添加到ArrayBlockingQueue。当ArrayBlockingQueue已满时,线程数将根据maximumPoolSize的值增加。如果增加线程数后还不能处理,ArrayBlockingQueue会继续满。然后拒绝策略RejectedExecutionHandler将用于处理完整的任务,默认为AbortPolicy。
28、Java中用到的线程调度算法是什么
抢先型。一个线程耗尽CPU后,操作系统会根据线程优先级、线程饥饿等数据,计算出一个整体优先级,将下一个时间片分配给某个线程执行。
29、Thread.sleep(0)的作用是什么
这个问题和上面那个有关联,所以我连上了。因为Java采用抢占式线程调度算法,所以可能会出现某个线程经常获得CPU控制权的情况。为了让一些优先级较低的线程获得CPU的控制权,可以使用Thread.sleep(0)手动触发操作系统分配时间片的操作,这也是一种平衡CPU控制权的操作。
30、什么是自旋
很多同步的代码只是简单的代码,执行时间非常快。此时,锁定所有等待的线程可能不值得,因为线程阻塞涉及到用户模式和内核模式的切换。由于synchronized中的代码执行速度非常快,所以最好让等待锁的线程不被阻塞,而是在synchronized的边界处做一个繁忙的循环,这称为spin。如果你做了很多繁忙的循环,发现你还没有得到锁,再次阻塞它可能是一个更好的策略。
31、什么是Java内存模型
Java内存模型定义了多线程访问Java内存的规范。这里的几句话并不能说清楚Java内存模型应该是完整的。让我简要总结一下Java内存模型的几个部分:
1)Java内存模型将内存分为主内存和工作内存。类的状态,即类之间共享的变量,存储在主存中。Java线程每次使用主存中的这些变量时,都会读取一次主存中的变量,并使它们在自己的工作内存中有一个副本。在运行自己的线程代码时,它使用这些变量,所有的操作都在自己的工作内存中。线程代码执行后,最新值将被更新到主存。
2)定义了几个原子操作来操作主存和工作内存中的变量。
3)定义易变变量的使用规则。
4)occurrences-before,即先发生原则,定义了操作A必须先发生在操作b中的一些规则,比如在同一个线程中,控制流前面的代码必须先发生在控制流后面的代码中,解锁锁的动作必须先发生在锁定同一个锁的动作中等等。只要满足这些规则,就不需要额外的同步措施。如果一段代码不满足所有发生之前的规则,那么
32、什么是CAS
CAS,全称Compare and Swap,是比较-替换。假设有三个操作数:内存值V、旧的期望值A和要修改的值B。当且仅当期望值A和记忆值V相同时,记忆值修改为B并返回true否则,将不执行任何操作,并将返回false。当然CAS一定要配合volatile变量,这样才能保证每次得到的变量都是主存中最新的值,否则旧的期望值A对于一个线程来说永远是一个常量值A,只要一次CAS操作失败,就永远不会成功。
33、什么是乐观锁和悲观锁
1)乐观锁:正如其名,他看好并发操作带来的线程安全问题。乐观锁认为竞争并不总是发生的,所以他不需要持有锁。他试图通过比较和替换这两个原子操作来修改内存中的变量。如果失败,就意味着冲突,所以应该有相应的重试逻辑。
2)悲观锁:顾名思义,还是对并发操作带来的线程安全问题持悲观态度。悲观锁认为竞争总是会发生的,所以每次操作一个资源都会持有一个独占锁,就像synchronized一样,不管是什么,直接锁定就可以操作资源。
34、什么是AQS
让我们简单谈谈AQS。AQS被称为抽象队列同步器,应该翻译为抽象队列同步器。
如果说java.util.concurrent的基础是CAS,那么AQS就是整个java的核心,被ReentrantLock、CountDownLatch、Semaphore等等使用。AQ实际上以双向队列的形式连接所有条目,例如ReentrantLock。所有等待的线程都放在一个条目中,并连接成一个双向队列。如果前一个线程使用ReentrantLock,双向队列中的第一个条目实际上开始运行。
AQ定义了双向队列的所有操作,但只开放tryLock和tryRelease方法供开发者使用。开发者可以根据自己的实现重写tryLock和tryRelease方法,实现自己的并发功能。
35、单例模式的线程安全性
这是老生常谈。首先,singleton模式的线程安全意味着一个类的实例在多线程环境中只会被创建一次。有许多方法可以编写单例模式。让我总结一下:
1)饥饿中国单例模式的编写:线程安全
2)懒惰单例模式:非线程安全。
3)双检锁singleton模式的编写方法:线程安全。
36、Semaphore有什么作用
Semhore是一个信号量,它的作用是限制某个代码块的并发。Semaphore有一个构造函数,可以传入一个int整数N,表示某个代码最多只能被N个线程访问。如果超过N,请等到一个线程执行完这个代码块,下一个线程再进入。可以看出,如果信号量构造函数中传入的int整数n=1,就相当于一个synchronized。
37、Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
这是我之前的困惑。不知道大家有没有想过这个问题。如果一个方法中有多个语句,并且都在操作同一个类变量,那么在多线程环境中必然会出现线程安全问题,这一点很好理解。但是size()方法明明只有一条语句,为什么还要锁呢?
关于这个问题,通过慢工出细活来理解主要有两个原因:
1)一个线程只能同时执行一个固定类的同步方法,但是一个类的异步方法可以同时被多个线程访问。所以,有一个问题。可能线程A是通过执行Hashtable的put方法来添加数据,而线程B可以正常调用size()方法来读取Hashtable中当前元素的个数。读取的值可能不是最新的。可能线程A已经加完数据了,但是线程B还没有检查size++就已经读取了大小,所以线程B读取的大小肯定是不准确的。但是在size()方法中加入同步,意味着线程B调用的size()方法只能在线程A调用put方法后才能调用,这样就保证了线程的安全性。
2)CPU执行代码,但不执行Java代码。这很重要,一定要记住。Java代码最后翻译成机器代码执行,机器代码才是真正能和硬件电路交互的代码。即使你只看到一行Java代码,甚至是Java代码编译后生成的一行字节码,也不代表底层只有这个语句的一个操作。1 “返回计数& # 34;假设翻译成三个汇编语句并执行,一个汇编语句对应它的机器码。线程完全有可能在第一句执行完之后切换。
38、线程类的构造方法、静态块是被哪个线程调用的
这是一个非常棘手和狡猾的问题。记住:thread类的构造方法和静态块是由thread类new所在的线程调用的,而run方法中的代码是由线程自己调用的。
如果上面的陈述让你感到困惑,让我给你举个例子。假设在Thread2中新增了Thread1,在main函数中新增了Thread2。然后:
1)Thread2的构造方法和静态块由主线程调用,Thread2的run()方法由thread 2自己调用。
2)Thread1的构造方法和静态块由Thread2调用,Thread1的run()方法由thread 1自己调用。
39、同步方法和同步块,哪个是更好的选择
同步块,这意味着同步块之外的代码异步执行,这比同步整个方法更能提高代码的效率。请知道一个原则:同步的范围越小越好。
有了这篇文章,我想补充一点,虽然同步的范围越小越好,但是在Java虚拟机中还有一种叫做锁粗化的优化方法,就是扩大同步的范围。这很有用,比如StringBuffer,这是一个线程安全的类。自然,最常用的append()方法是同步方法。我们写代码的时候会反复追加字符串,也就是反复加锁->;解锁,对性能不好,因为这意味着Java虚拟机要在这个线程上反复切换内核模式和用户模式。因此,Java虚拟机会对append方法多次调用的代码进行粗化,将append操作多次扩展到append方法的头部和尾部成为一个大的同步块,从而减少锁定->:解锁的次数,有效提高代码执行的效率。
40、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
这是我在并发编程网上看到的一个问题。把这个问题放到最后一个。希望大家能看到,想一想,因为这个问题很好,很实用,很专业。关于这个问题,我个人的意见是:
1)对于高并发、任务执行时间短的业务,可以将线程池中的线程数设置为CPU核数+1,以减少线程上下文的切换。
2)应区分低并发、任务执行时间长的业务:
a)如果业务时间长且集中在IO操作上,即IO密集型任务,因为IO操作不占用CPU,所以不要让所有CPU闲置,可以增加线程池中的线程数量,让CPU处理更多的业务。
b)如果业务时间长,集中于计算操作,即计算密集型任务,就没有出路。就像(1)一样,将线程池中的线程数量设置的少一些,减少线程上下文的切换。
c)高并发和长服务执行时间。解决这类任务的关键不在于线程池,而在于整体架构的设计。第一步是看这些服务中的一些数据是否可以缓存,第二步是添加服务器。关于线程池的设置,参考其他关于线程池的文章。最后,可能还需要分析业务执行时间长的问题,看看是否可以使用中间件来拆分和解耦任务。
100+BAT等大厂Java面试问题汇总
转发本文+关注+私信【0208】即可获取。
本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://51itzy.com/16665.html