本文主要是介绍《小码哥带你圆梦大厂》多线程之《真的不菜,只是缺了30题》,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
宝剑锋从磨砺出,梅花香自苦寒来,大家好,我是 小码哥 今天来整理了30道java线程的基础面试题,希望看完对大家面试有所收货!
1.多线程有什么用/基本方法
一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回 答更扯淡。所谓"知其然知其所以然","会用"只是"知其然","为什么用"才是"知其所以然",只有达到"知 其然知其所以然"的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:
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,分别 建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
基本方法
线程相关的基本方法有 wait,sleep,yield,interrupt,join, notify 等。
1、线程等待(wait)
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的 是调
用 wait()方法后, 会释放对象的锁。因此, wait 方法一般用在同步方法或同步代码块中。
2、线程睡眠(sleep)
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程 进入
TIMED-WATING状态,而 wait()方***导致当前线程进入 WATING 状态
3、线程让步(yield)
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下, 优先
级高的线程有更大的可能性成功竞争得到 CPU 时间片, 但这又不是绝对的,有的操作系统对线程 优先级并
不敏感。
4、线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。 这 个线
程本身并不会因此而改变状态(如阻塞,终止等)。
- 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会 因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
- 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方***抛出InterruptedException,从而使线程提前结束 TIMED-WATING状态。
- 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都 会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
- 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据thread.isInterrupted()的值来优雅的终止线程。
5、Join 等待其他线程终止
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞 状态
,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
为什么要用 join()方法?
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线 程结
束后再结束,这时候就要用到 join() 方法 。
System.out.println(Thread.currentThread().getName() + "线程运行开始!");Thread6 thread1 = new Thread6();thread1.setName("线程 B");thread1.join();System.out.println("这时 thread1 执行完毕之后才能执行主线程");
6、线程唤醒(notify)
Object 类中的 notify() 方法, 唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象 上等
待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中 一个
wait() 方法,在对象的监视器上等待, 直到当前的线程放弃此对象上的锁定,才能继续执行被唤 醒的线程,
被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还 有 notifyAll() ,
唤醒再次监视器上等待的所有线程。
2.多线程和单线程的区别和联系
1、在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流 占用 CPU 的机制。
2、多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需 要的时间比一个线程的进程执行两次所需要的时间要多一些。
结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的 响应时间。
3.简述线程、程序、进程之间的联系和关系
线程
与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与 进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在 各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序
是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程
是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个 进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令 接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出 设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更 小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程 中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可 以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
4.创建线程的几种方式
方式一、继承Thread类,作为线程对象存在(继承Thread)
让线程等待的方法
Thread.sleep(200); //线程休息2ms
Object.wait() ; //让线程进入等待,直到调用Object的notify或者notifyAll时,线程停止休眠
方式二、实现Runnable接口,作为线程任务存在
Runnable 只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable 对象,必须将它放 到一个线程对象里。
方式三、匿名内部内创建线程对象
创建带线程任务并且重写run方法的线程对象中,为什么只运行了Thread的run方法。我们看看Thread 类的源码,
我们可以看到Thread实现了Runnable接口,而Runnable 接口里有一个run方法。
所以,我们最终调用的重写的方法应该是Thread类的run方法。而不是Runnable接口的run方法。
方式四、创造带返回值的线程
Callable源码详情
返回指定泛型的call方法。然后调用FutureTask对象的get方法得道call方法的返回值。
方式五、定时器Timer实现
方式六、线程池创造线程
现在不太支持这种写法但是也是一种方式
方式七、利用java8新特性Stream开发
5、线程有哪些基本状态(生命周期)
Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态 变迁如下图所示:
操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态 ,所以 Java 系统一般将这两个 状态统称为 RUNNABLE(运行中) 状态 。
当线程执行 wait() 方法之后,线程进入 WAITING等待状态。进入等待状态的线程需要依靠其他 线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加 了超时限制,比如通过 sleep(long millis) 方法或 wait(long millis) 方法可以将 Java 线程置 于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步 方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的 run() 方法之后将会进入到 TERMINATED(终止) 状态。
6、如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的 方法。
3、使用interrupt方法中断线程。
7、start()方法和run方法的区别
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。 如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行
完毕之后,另外一个线程才可以执行其run()方法里面的代码。
8、为什么我们调用Start()方法时会自动调用run方法,为什么我们不能直接调用run方法
感兴趣的朋友可以看看Thread源码中start()方法就会有更加详细的了解
JVM执行start方***另起一条线程执行thread 的run方法,这才起到多线程的效果~为什么我们不能直接调用run方法,
如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线 程效果。
9、Runnable接口和Callable接口的区别
有点深的问题了,也看出一个Java程序员学习知识的广度。
1、Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
2、Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取 异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满 着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经 赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而 Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据 的情况下取消该线程的任务,真的是非常有用。
10、什么是线程安全?
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。 但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized , 就是一段代码同时只能有一个线程来操作 不然中间过程可能会 产生不可预制的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运 行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
11、线程的状态转换
1、新建状态
新创建了一个线程对象。
2、就绪状态
线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位 于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态
就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进 入就绪状态,才有机会转到运行状态。
阻塞的情况又分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻 塞状态。当sleep()状态超时、join()等待线程终止 或者超时、或者I/O处理完毕时,线程重新转入就绪状 态。(注意,sleep是不会释放持有的锁)
5、死亡状态
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
12、在多线程中什么是上下文切换
单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU 分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程 时同时执行的,时间片一般是几十毫秒(ms)。
操作系统中,CPU时间分片切换到另一个就绪的线程,则需要保存当前线程的运行的位置,同时需要加 载需要恢复线程的环境信息。
13、java中对堆和栈有什么不同
栈:在函数中定义的基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
堆:堆内存用于存放由new创建的对象和数组。
从通俗化的角度来说,堆是用来存放对象的,栈是用来存放执行程序的
当一个人开始学习Java或者其他编程语言的时候,会接触到堆和栈,由于一开始没有明确清晰的说明解释,很多人会产生很多疑问,什么是堆,什么是栈,堆和栈有什么区别?更糟糕的是,Java中存在栈这样一个后进先出(Last In First Out)的顺序的数据结构,这就是java.util.Stack。这种情况下,不免让很多人更加费解前面的问题。事实上,堆和栈都是内存中的一部分,有着不同的作用,而且一个程序需要在这片区域上分配内存。众所周知,所有的Java程序都运行在JVM虚拟机内部,我们这里介绍的是JVM(虚拟)内存中的堆和栈。
1、区别
java中堆和栈的区别自然是面试中的常见问题,下面几点就是其具体的区别
2、各司其职
最主要的区别就是栈内存用来存储局部变量和方法调用。 而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
3、独有还是共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。 而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
5、异常错误
如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出
java.lang.StackOverFlowError。 而如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。
6、空间大小
栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满。如果递归没有及时跳出,很可能发生StackOverFlowError问题。 你可以通过-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值。
这就是Java中堆和栈的区别。理解好这个问题的话,可以对你解决开发中的问题,分析堆内存和栈内存使用,甚至性能调优都有帮助。
7、查看默认值(Updated)
查看堆的默认值,使用下面的代码,其中InitialHeapSize为最开始的堆的大小,MaxHeapSize为堆的最大值。
14、如何确保线程安全?
一、线程安全等级
之前的博客中已有所提及"线程安全"问题,一般我们常说某某类是线程安全的,某某是非线程安全的。其实线程安全并不是一个"非黑即白"单项选择题。按照"线程安全"的安全程度由强到弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1、不可变
在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。
2、绝对线程安全
绝对的线程安全完全满足Brian GoetZ给出的线程安全的定义,这个定义其实是很严格的,一个类要达到"不管运行时环境如何,调用者都不需要任何额外的同步措施"通常需要付出很大的代价。
3、相对线程安全
相对线程安全就是我们通常意义上所讲的一个类是"线程安全"的。
它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在java语言中,大部分的线程安全类都属于相对线程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保证的集合。
4、线程兼容
线程兼容就是我们通常意义上所讲的一个类不是线程安全的。
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下可以安全地使用。Java API中大部分的类都是属于线程兼容的。如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
5、线程对立
线程对立是指无论调用端是否采取了同步错误,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。
一个线程对立的例子是Thread类的supend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都有死锁风险。正因此如此,这两个方法已经被废弃啦。
二、线程安全的实现方法
保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。
1、互斥同步
互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。
2、非阻塞同步
随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。
CAS缺点:
ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3、无需同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
1)可重入代码
可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。
(类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)
2)线程本地存储
如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如"生产者-消费者"模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的"一个请求对应一个服务器线程(Thread-per-Request)"的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。
15、什么是竞态条件?你怎么发现和解决竞争?
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
在临界区中使用适当的同步就可以避免竞态条件。
界区实现方法有两种,一种是用synchronized ,一种是用Lock显式锁实现。
16、用户线程和守护线程有什么区别
如果JVM中所有的线程都是守护线程,那么JVM就会退出,进而守护线程也会退出。
如果JVM中还存在用户线程,那么JVM就会一直存活,不会退出。
由此可以得到:
守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。
用户线程是独立存在的,不会因为其他用户线程退出而退出。
默认情况下启动的线程是用户线程,通过setDaemon(true)将线程设置成守护线程,这个函数务必在线程启动前进行调用,否则会报
java.lang.IllegalThreadStateException异常,启动的线程无法变成守护线程,而是用户线程。
现在我们开始进行试验:
在主线程中启动守护线程
得出的结果是:
发现主线程一旦退出,守护线程也就不再运行,直接退出了。
那如果主线程启动了两个线程,一个是守护线程,一个是用户线程,主线程退出,守护线程会不会退出呢?我们来测试一下
得出结果
主线程退出后,守护线程依然在运行!由此得到只要任何非守护线程还在运行,守护线程就不会终止
17、如何创建守护线程以及在什么情况下使用它
任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程 设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行 时会抛出异常。
守护线程相当于后台管理者 比如 : 进行内存回收,垃圾清理等工作。
18、notify()和notifyAll()有什么区别?
notify可能会导致死锁,而notifyAll则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码 使用
notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤 醒一个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须 调用
notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致 死锁。
正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如 果唤醒的
线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中。
19、sleep()和wait() 有什么区别?
- 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,
- 当指定的时间到了又会自动恢复运行状态
- 在调用 sleep()方法的过程中, 线程不会释放对象锁。
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,
- 只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
20、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 一般用于 状态标记量 和 单例模式的双检锁。
21、Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的 效果
不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法 才会启动
新线程 。
22、为什么wait, notify 和 notifyAll这些方法不在thread类里面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。
如果线 程需要等待某些锁那么调用对象中的wait()方法就有意义了。
如果wait()方法定义在Thread类中,线程 正在等待的是哪个锁就不明显了。
简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把 他们定义在Object类中因为锁属于对象 。
23、为什么wait和notify方法要在同步块中调用?
- 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法
- 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
- 还有一个原因是为了避免wait和notify之间产生竞态条件。
- wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经 获得该对象的锁。
- 因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
- 在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。
- 因此,必须在某 个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
- 调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。
- 调用notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:"特殊状态已经被设置"。
- 这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
24、Java中synchronized 和 ReentrantLock 有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如 果一
个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而 进行线程
阻塞和唤醒的代价是比较高的。
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互 斥,
需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock() 方法配合
try/fifinally语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码 指令。
在执行monitorenter指令时,首先要尝试获取对象锁。
如果这个对象没被锁定,或者当前线程已 经拥有了那个对象锁,把锁的计算器加1,
相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,
锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止 。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁。
相比Synchronized,ReentrantLock
类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,
这相当于Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,
ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现 的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象 。
25、SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap()
和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。 而
ConcurrentHashMap
的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问 map,
其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程, 仍然可以对map执行某些操作。
评估:
ConcurrentHashMap在性能以及安全性方面,明显比
Collections.synchronizedMap()更加有优势。
同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修 改,
也不会抛出
ConcurrentModifificationException 。
26、Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,
- execute()方法的返回类型是void,它定义在Executor接口中,
- submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,
它扩展了 Executor接口,其它线程池类像ThreadPoolExecutor和
ScheduledThreadPoolExecutor都有这些方法 。
27、结合实战
1、简述一下你对线程池的理解如果问到了这样的问题,可以展开的说一下(线程池如何用、线程池的好处、线程池的启动策略)合理利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,
还会降 低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
28、JAVA 后台线程
1、定义:
守护线程--也称"服务线程", 他是后台线程, 它有一个特性,即为用户线程 提供 公共服 务,
在没有用户线程可服务时会自动离开。
2、优先级:
守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
3、设置:
通过 setDaemon(true)来设置线程为"守护线程";将一个用户线程设置为守护线程的方式是 在 线
程对象创建 之前 用线程对象的setDaemon 方法。
4、在 Daemon :
线程中产生的新线程也是 Daemon 的。
5、线程则是 JVM 级别的:
以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周 期并不会和 Web 应用程序保持同步。
也就是说,即使你停止了 Web 应用,这个线程依旧是活跃 的。
6、example:
垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,
垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。
它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
7、生命周期:
守护进程(Daemon)是运行在后台的一种特殊进程。
它独立于控制终端并且周期性地 执行某种任务或等待处理某些发生的事件。
也就是说守护线程不依赖于终端,但是依赖于系统,与 系统"同生共死"。
当 JVM 中所有的线程都是守护线程的时候, JVM 就可以退出了;如果还有一个 或以上的非守护线程则 JVM不会退出。
29、什么是线程组,为什么在 Java 中不推荐使用?
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为 了管
理线程的生命周期,复用线程,减少创建销毁线程的开销。
30、线程的部分方法使用
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()// 获得一个线程的优先级。
感谢大家的阅读!希望本文对大家有所帮助。
这篇关于《小码哥带你圆梦大厂》多线程之《真的不菜,只是缺了30题》的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!