Java线程知识考点
盘点了Java线程知识考点
- 进程和线程的区别
- Java进程和线程的关系 -
- Java中Thread的start和run方法的区别 √
- Thread、Runnable以及Callable有什么区别 -
- 如何给run()方法传参
- 线程的状态
- Sleep和wait -
- Notify和notifyAll的区别 √
- Yeild方法 √
- 守护线程 -
- 如何中断线程 √
- 线程状态图
进程和线程的区别
进程是资源分配的最小单位,线程是CPU调度的最小单位
Ø 所有与进程相关的资源,都被记录在PCB中
Ø 进程是抢占处理器的调度单位;线程属于某个进程,共享其资源
Ø 线程只由栈、寄存器,程序计数器和TCB组成
进程和线程的区别 -
根本区别:进程是资源分配的最小单位,而线程CPU调度的最小单位
开销方面:每个进程都有独立的代码和数据空间(程序上下文),进程之间切换开销大;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈、寄存器和程序计数器(PC),线程之间切换的开销小
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
总结
Ø 线程不能看作独立应用,而进程可以看作独立应用
Ø 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
Ø 线程没有独立的地址空间,多进程的程序比多线程程序健壮
Ø 进程切换比线程的切换开销大
进程和线程通信方式 -
进程:
(1)管道(pipe,半双工),流管道(s_pipe,全双工),有名管道(FIFO,全双工)
(2)信号量(sophomore/mutex)
(3)信号(signal)
(4)消息队列
(5)共享内存
(6)套接字(socket)
线程:
(1)volatile变量
(2)使用Object类的wait() 和 notify() 方法
(3)使用JUC工具类 CountDownLatch、CyclicBarrier、Exchanger、Semaphore
(4)使用 ReentrantLock 结合 Condition
(5)LockSupport方法
(6)阻塞队列
Java进程和线程的关系 -
Ø Java对操作系统提供的功能进行封装,包括进程和线程
Ø 运行一个程序会产生一个进程,进程包含至少一个线程
Ø 每个进程对应一个JVM实例,多个线程共享JVM里的堆
Ø Java采用单线程编程模型,程序会自动创建主线程
Ø 主线程可以创建子线程,原则上要后于子线程完成执行
JVM是多线程的
JVM实例在创建的时候同时会创建很多其他的线程,比如GC的线程。
各个JVM实例之间是相互隔离的。
Java中Thread的start和run方法的区别 √
调用线程的start方法是创建了新的线程,在新的线程中执行。
调用线程的run方法是在主线程中执行该方法,和调用普通方法一样
Thread、Runnable以及Callable有什么区别 -
- 由于java不能多继承可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式;
- 实现callable接口,提交给ExecutorService返回的是异步执行的结果,另外,通常也可以利用FutureTask(Callable callable)将callable进行包装然后FutureTask提交给ExecutorService。
· callable的核心是call方法,允许返回值,runnable的核心是run方法,没有返回值
· call方法可以抛出异常,但是run方法不行
Ø Thread是实现了Runnable接口的类,使得run支持多线程
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作
FutureTask类实现了RunnableFuture接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
如何给run()方法传参
实现的方式主要有三种 -
Ø 构造参数传参
Ø 成员变量传参
Ø 回调函数传参
如何获取线程的返回值 √
方法1:主线程等待法
缺点是控制时间不够精准,而且逻辑一多实现起来很麻烦
方法2:使用Thread类的join()阻塞当前线程等待子线程处理完毕
缺点是粒度不够细,比如说无法做到线程1达到某个时刻去执行线程2的方法
方法3:通过Callable接口实现:通过FutureTask Or 线程池获取
ThreadPool
提交多个实现callable的类,让线程池并发的处理结果。统一管理
线程的状态
Ø 新建(New):创建后尚未启动的线程的状态
Ø 运行(Runnable):包含Running和Ready
Ø 无限期等待(waiting):不会被分配CPU执行时间,需要显式被唤醒
进入方法 | 退出方法 |
---|---|
没有设置Timeout参数的Object.wait()方法 | Object.notify() / Object.notfifyAll() |
没有设置Timeout参数的Thread.join()方法 | 被调用的线程执行完毕 |
LockSupport.park()方法 | LockSupport.unpark |
Ø 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒
进入方法 | 退出方法 |
---|---|
Thread.sleep()方法 | 时间结束 |
设置了Timeout参数的Object.wait()方法 | 时间结束/Object.notify()/ Object.notfifyAll() |
设置了Timeout参数的Thread.join()方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos 方法 | LockSupport.unpark |
LockSupport.parkUntil 方法 | LockSupport.unpark |
Ø 阻塞(Blocked):等待获取排他锁
Ø 结束(Terminated):已终止线程的状态,线程已经结束执行
结束后运行join会发生异常
Sleep和wait -
基本的差别
Ø Sleep是Thread类的方法,wait是Object类中定义的方法
Ø Sleep()方法可以在任何地方使用
Ø Wait()方法只能在synchronized方法或者synchronized块中使用
Ø sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
最主要的本质区别
Ø Thread.sleep只会让出CPU,不会导致锁行为的改变
Ø Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
第一个实现wait逻辑,第二个实现sleep逻辑。
在第一个线程start后sleep 10ms再运行第二个线程。
此时由于线程A使用wait方法,所以B可以拿到锁。
Thread A is waiting to get lock
Thread A get lock
Thread B is waiting to get lock
Thread A do wait method
Thread B get lock
Thread B is sleeping 10ms
Thread B is done
Thread A is done
如果A使用Sleep:
Thread A is waiting to get lock
Thread A get lock
Thread B is waiting to get lock
Thread A do wait method
Thread A is done
Thread B get lock
Thread B is sleeping 10ms
Thread B is done
回到线程A wait(不给参数,无限等待),线程B sleep的状态
就会发现A无限等待,然后B已经执行完了,程序还没结束。
此时可以在B中加入notify来唤醒A、或者notifyall
Notify和notifyAll的区别 √
两个概念
锁池EntryList
假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
等待池WaitSet
假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁
Ø NotifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
Ø Notify只会随机选择一个处于等待池中的线程进入锁池去竞争获取锁的机会
1 | package com.interview.javabasic.thread; |
如果改成notifyAll
所有的线程都被唤醒了,但是最后只有一个执行成功。其余线程又回到等待池中。
Yeild方法 √
用了yield方法后,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
Yield对锁的行为没有影响
只会分配给当前线程相同优先级的线程
在Java程序中,通过一个整型成员变量Priority来控制优先级,优先级的范围从1~10.在构建线程的时候可以通过setPriority(int)方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。需要注意的是在不同JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。
Yield对锁的行为没有影响
守护线程 -
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。
这里需要注意的是守护线程在退出的时候并不会执行finnally块中的代码,所以将释放资源等操作不要放在finnally块中执行,这种操作是不安全的
线程可以通过setDaemon(true)的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于start()方法,否则会报
Exception in thread “main” java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1365) at learn.DaemonDemo.main(DaemonDemo.java:19)
这样的异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。
如何中断线程 √
已经被抛弃的方法
Ø 通过调用stop()方法停止线程
Ø 通过调用suspend()和resume()方法
stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的, stop方法会破坏原子逻辑也可能会导致数据不同步。
suspend()在导致线程暂停的同时,并不会去释放任何锁资源
如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。
目前使用的方法
Ø 调用interrupt(),通知线程应该中断了
a) 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
b) 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将会继续正常运行不受影响。
Ø 需要被调用的线程配合中断
a) 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
b) 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用 isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread的静态方法 interrupted()对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。