Java基础知识-线程
1、在 Java 中要想实现多线程代码有几种手段?
1. 一种是继承 Thread 类
2. 另一种就是实现 Runnable 接口
3. 最后一种就是实现 Callable 接口
4. 第四种也是实现 callable 接口,只不过有返回值而已
2、Thread 类中的 start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且 start()内部调用了 run()方法,这和直接调用 run()方法的效果不一样。当你调用 run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
3、Java 中 notify 和 notifyAll 有什么区别?
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它 才有用武之地。而 notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一 个线程能继续运行。
4、Java 多线程中调用 wait() 和 sleep()方法有什么不同?
Java 程序中 wait 和 sleep 都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep()方法仅仅释放 CPU 资源或者让当前线程停止执行一段时间,但不会释放锁。
在java中,sleep和wait方法都用于线程的暂停,但它们在原理、使用场景、对锁的处理机制等方面有所不同。以下是详细介绍:
● 原理不同。sleep是Thread类中的静态方法,用于让线程暂停执行一段时间,但不释放持有的锁;wait是Object类的方法,用于线程间的通信,当线程执行wait时会释放它持有的锁,并进入等待池。
● 使用区域不同。sleep可以在任何地方使用,并且必须捕获可能出现的异常;wait必须放在同步控制方法和同步代码块中使用。
● 唤醒方式不同。sleep必须传递一个超时时间参数,超过指定时间后线程会自动唤醒;wait可以有两种形式,一种是无需传递参数,线程会进入无限期的等待状态,直到被其他线程的notify或notifyAll唤醒;另一种是传递参数,线程会在指定时间后醒来。
● 释放锁资源不同。sleep不会释放锁;wait会释放锁,使得其他线程可以访问被锁定的资源。
5、什么是线程安全
多个线程同时运行一段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。同一个实例对象在被多个线程使用的情况下也不会出现计算失误,也是线程安全的,反之则是线程不安全的。
7、线程的状态?
实线程一般具有五种状态,即创建、就绪、运行、阻塞、终止。
1. 在这里插入图片描述新建( new ):新创建了一个线程对象。
2. 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象的start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取 cpu 的使用权 。
3. 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ),执行程序代码。
4. 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:
● a.等待阻塞:运行( running )的线程执行 o.wait ()方法,JVM 会把该线程放 入等待队列( waitting queue )中。
● b.同步阻塞:运行( running )的线程在获取对象的同步锁时, 若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
● c.其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t.join ()方法,或者发出了 I / O 请求时,JVM 会把该线程置为阻塞状态。当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
1. 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
8、线程间通信的几种实现方式?
1. 使用 volatile 关键字。基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式。
2. 使用Object类的wait() 和 notify() 方法。Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁
9、线程属性
编号(ID):用于标识线程的唯一编号,只读属性。
名称 (Name):用于定义线程名称,可读可写。
线程类别(Daemon):通过线程的 setDaemon(boolean on) 方法进行设置,为 true 表示设置为守护线程,否则为用户线程。用户线程会阻止 Java 虚拟机正常停止,守护线程则不会。通常可以把一些不重要的线程设置为守护线程,比如监控其他线程状态的监控线程,当其他工作线程停止后,虚拟机就可以正常退出。
优先级 (Priority):Java 线程支持 1 到 10 十个优先级,默认值为 5 。Java 线程的优先级本质上只是给线程调度器一个提示信息,它并不能保证线程一定按照优先级的高低顺序运行,所以它是不可靠的,需要谨慎使用。在 Java 平台中,子线程的优先级默认与其父线程相同。
10、Java 线程的生命周期分为以下五类状态:
RUNABLE:该状态包括两个子状态:READY 和 RUNING 。处于 READY 状态的线程被称为活跃线程,被线程调度器选中后才开始运行,转化为 RUNING 状态。
BLOCKED:一个线程发起一个阻塞式 IO 操作后(如文件读写或者阻塞式 Socket 读写),或者申请一个由其他线程持有的独占资源(比如锁)时,相应的线程就会处于该状态。
WAITING:线程处于无时间限制的等待状态。
TIMED_WAITING:有时间限制的等待状态,如果在指定时间内并没有执行的特定的操作,则该线程自动转换为 RUNABLE。
TERMINATED:Thread.run()正常返回或者由于抛出异常而提前终止,则对应的线程都会处于该终止状态。
11、线程的三大特性
线程有原子性、可见性和有序性三大特性。
原子性
对于涉及共享变量的操作,若该操作从其执行线程以外的任意线程来看都是不可分割的,那么我们就说该操作具有原子性。它包含以下两层含义:
访问(读、写)某个共享变量的操作从其执行线程以外的其他任何线程来看,该操作要么已经执行结束要么尚未发生,即其他线程不会看到该操作的中间部分的结果。
可见性
如果一个线程对某个共享变量进行更新之后,后续访问该变量的其他线程可以读取到这个更新结果,那么我们就称该更新对其他线程可见,反之则是不可见,这种特性就是可见性。出现可见性问题,往往意味着线程读取到了旧数据,这会导致更新丢失,从而导致运行结果与预期结果存在差异。可见性问题与计算机的存储结构和 Java 的内存模型都有着密切的关系。
有序性
源代码顺序:程序员编写的代码的执行顺序;
程序顺序:编译后的代码的执行顺序;
执行顺序:给定代码在处理器上的实际执行顺;
感知顺序:处理器感知到的其他处理器上代码的执行顺序。
在 Java 语言中,volatile 和 synchronized 都能够保证有序性:
volatile:通过内存屏障来禁止指令重排序,通过加载屏障和存储屏障来冲刷写缓冲器和清空无效化队列,从而可以避免内存重排序的现象;
synchronized :使用 synchronized 修饰的变量在同一时刻只允许一个线程对其进行 lock 操作,这种限制决定了持有同一个锁的两个同步块只能串行执行,也就避免了乱序问题。
12、ThreadLocal的理解
ThreadLocal 主要功能有两个:
● 第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题
● 第二个是实现了线程内的资源共享
面试官:好的,那你知道ThreadLocal的底层原理实现吗?
在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
面试官:好的,那关于ThreadLocal会导致内存溢出这个事情,了解吗?
是应为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。
在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。