目录:
- 线程简介
- 线程实现
- 线程状态
- 线程同步
- 线程通信问题
- 其他
线程简介
线程实现
- 继承Thread类 -> 重写run() 方法 -> 创建Thread对象 -> 调用Thread对象的Start() 方法
- 实现Runnable接口 -> 重写run() 方法 -> 创建Runnable对象,创建Thread对象,将Runnable作为参数传递到构造方法中 -> 调用创建出来的 Thread 对象的 Start() 方法
- 实现Callable接口,通过泛型定义返回值的类型 -> 重写call() 方法 -> 创建Callable实例 -> 创建service执行服务 -> 调用service的submit() 方法,submit() 的参数为之前创建的Callable实例,返回值Future
中包含call() 方法的返回值 -> 调用Future的get()方法获得最终的返回值 -> service.shutdown() 关闭服务 - Callable接口:可以定义返回值,可以抛出异常
静态代理模式
- 真实对象 和 代理对象 都要实现同一个接口
- 代理对象 用于代理 真实对象
线程状态
- 创建 -> 就绪 -> 运行/阻塞 -> dead
- 停止线程:建议使用一个标志位进行终止变量,当flag = false时,则线程停止运行;在实现了Runnable的类中定义方法改变flag的值
- 在线程类中调用的方法
- Thread.sleep()
- Thread.yield()
- 状态:
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED_WAITING
- TERMINATED
- 关于线程中的阻塞状态
- 线程阻塞具体指的是暂停一个线程的执行以等待某个条件的发生,例如说某些资源的准备就绪;java中设计了大量的方法使得线程进入阻塞状态,例如说比较常见的sleep(), wait()以及join()方法;除此之外,我还了解到还有suspend()和resume()可以使线程阻塞,不过因为这两个方法会有导致死锁的可能性所以就已经被弃用了
线程优先级
- getPriority()
- setPriority()
守护线程
- 线程分为 用户线程 & 守护线程
- 虚拟机会确保用户线程执行完毕,但不会确保守护线程执行完毕
- 调用Thread类的 setDaemon() 方法将该线程设置为守护线程
线程同步
- 基础概念
- 并发:多个线程访问同一个对象
- 并发的同时如果有修改对象的需要,就需要 -> 线程同步
- 线程同步:等待机制,多个需要同时访问对象的线程进入这个对象的等待池,形成队列
- 锁机制,synchronized,排他锁
- 经典的并发不安全问题
- 一票多卖
- 线程不安全的集合
- synchronized 锁
- 修饰普通方法、修饰代码块、修饰静态方法
- 什么是synchronized锁?
- 当线程运行到被锁住的地方,线程回去找到加锁的对象,查看目前有没有其他线程占用该锁
- 如果没有的话,当前线程就会获得锁并继续执行下去
- 但如果该锁被其他线程占用着,当前锁就会等待,直到其他线程释放该锁
- 如果当前线程已经获得了某个对象锁,再去调用其他由该对象锁住的其他代码块或者方法,是可以的 -> 可重入锁
- https://www.zhihu.com/question/57794716/answer/606126905
- https://blog.csdn.net/carson_ho/article/details/82992269
- https://blog.csdn.net/weixin_36759405/article/details/83034386
- synchronized 和 lock 之间的差别
线程之间的通信
- 管程法
- 只有当多个线程都涉及到修改某个对象的时候,才会有线程同步的需求 -> 所以一般就都是用这个将会被修改的对象,作为锁(所以锁涉及到的方法:wait(),notify()/ notifyAll()这些都是Object类的方法)
- 在synchronized代码块中执行了 wait() 的话,就会释放当前锁,让出CPU,进入等待状态
- 在synchronized代码块中执行了 notify() / notifyAll() 的话,会唤醒一个或多个正处于等待状态的线程;不过,当前线程只会在执行完 synchronized 代码块之后才会让出对象锁,又或者是它自己在执行过程中调用了 wait()
- 信号灯法
线程池
- Executors其实是一个工具类,通过调用Executors中的不同方法获得不同的线程池;所有获得线程池方法的底层其实都是创建一个ThreadPoolExecutor
- 创建ThreadPoolExecutor时会用到的几个参数:corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, threadFactory, RejectedExecutorHandler
面试问题
- 什么是上下文切换?
- 当线程被分配到的时间片结束,又或者是线程自己调用了 sleep, wait, yield 等方法自发地交出CPU的使用权,都会导致当前线程切出,另一个线程获得CPU的使用权开始运行
- 那么操作系统要保存和恢复相对应的进度信息,方便下一次线程继续运行
- 这整个过程就是被称为上下文切换,上下文的内容 -> CPU寄存器和程序计数器的内容(CPU正在执行的位置)
- https://www.jianshu.com/p/5549e89133d2#
- 解释一下并发和并行
- 并发是通过CPU执行任务的交替切换,而达到的多个任务交替进行的效果;而并行则是指多个独立互不影响的任务同时进行,所以真正的并行只能出现在多个CPU的系统中
- 线程的生命周期 - 6种状态及切换
- NEW -> 在线程被创建之后,但 start() 方法尚未被调用时
- RUNNABLE -> 包括 READY & RUNNING,当 start() 方法被调用之后,等待获得CPU使用权的情况为READY,获得CPU时间片就变为 RUNNING
- BLOCKED -> 运行到需要获得对象 synchronized 锁
- WAITING -> 等待,调用了 wait() 或者 sleep() 方法的线程
- TIMED_WAITING
- TERMINATED -> 线程运行完成或者抛出了异常
- 解释一下多线程常用的几个方法?wait(), notify(), notifyAll(), sleep(), yield(), join()
- 其中 wait(), notify(), notifyAll() 都是位于 Object 类中,因为这三个方法都是围绕着作为对象锁的对象实例来调用的,例如说 wait() 是挂起当前正在使用同步代码块的该线程,而 notify() 或者 notifyAll() 则是唤醒所有调用了 wait() 方法进入等待状态的线程;因为多线程中任意的对象都可以作为锁,而java中的所有类都继承自Object类,所以自然而然的这三个方法就写在了Object类中
- 而因为 wait(), notify(), notifyAll() 这些都是通过监视器实现的,如果在没有获得对象锁的情况下就调用这几个方法就会抛出异常,IllegalMonitorStateException
- sleep() 和 wait() 的效果都是让当前线程进入 WAITING(等待) 状态,它们的区别就是 sleep() 不会释放锁,而 wait() 会同时释放锁
- yield() 暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态
- join() - 等待这个线程死亡,也就是等待这个线程全部执行完后才继续执行接下来的进程
- join() 方法中的子线程和主线程
- https://www.cnblogs.com/paddix/p/5381958.html
- https://www.cnblogs.com/lyuweigh/p/9568697.html
- sleep() 和 wait() 方法的区别
- 是否释放锁
- 作用对象:sleep()是Thread类的静态方法,作用于当前线程;而wait()是Object类的实例方法,作用于对象本身
- 执行sleep()之后,可以在超时或者调用interrupt()来唤醒休眠中的线程,而调用wait()进入休眠状态的对象则是通过notify() 或者 notifyAll() 来唤醒
- 应该在 if 块还是 while 循环中调用 wait() 方法?
- 如果是while循环中的话,程序每次被唤醒都会对条件进行判断再执行;而如果是在 if 块中的话,程序不会再次判断条件而是直接运行;有些情况下,在线程等待的过程中条件发生了变化,如果不作条件判断就直接运行的话可能会报异常,所以应该在 while 循环中调用 wait() 方法比较妥当
- https://www.cnblogs.com/xy-ouyang/p/10463448.html
- 同步队列 & 等待队列
- 同步队列:存放着竞争同步资源的线程的引用(不是存放线程)
- 等待队列:存放着待唤醒的线程的引用
- 所以 notify() / notifyAll() 是将线程从等待队列移动到同步队列中
- 而同步队列中的线程都是 BLOCKED 的状态(等待锁的释放),而等待队列中的线程都是 WAITING 或者 TIMED_WAITING 的状态
- https://blog.csdn.net/pange1991/article/details/53860651
- synchronized与ReentrantLock的区别
- ReentrantLock 等待可中断,可以设置为公平锁,可以绑定多个条件
- ReentrantLock 一科 CAS + AQS 来实现,而 synchronized 则是由底层的 monitor 来实现
- https://zhuanlan.zhihu.com/p/126085068
- https://www.jianshu.com/p/4358b1466ec9
- 死锁
- 多个线程同时被阻塞,它们中的一个或者多个都在等待某个资源被释放,如果没有外界的介入程序就会一直等待下去,无限期地阻塞
- 避免死锁的方法
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制
- 解释一下多线程开发带来的问题
- 线程安全问题:某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等
- 线程安全问题发生的条件:多线程环境下存在共享资源,而且多线程都会对该共享资源进行非原子性的操作
- 解决方法:避免共享资源;加锁 - synchronized 或者 ReentrantLock;使用ThreadLocal
- 线程性能问题:一个线程的创建到销毁都会占用大量的内存,多线程环境下如果有多个线程闲置,那么大量的内存空间就会被浪费掉
- 解决方法:使用线程池,减少线程的频繁创建和销毁,节省内存开销
- 活跃性问题:死锁,饥饿锁,活锁
- 阻塞:当多个线程共享临界区资源时,有可能发生某一个线程占用了临界区资源,其他线程被挂起等待较长时间
- 解决方法:可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能。
- 临界区资源:临界区是用来表示一种公共的资源(共享数据),它可以被多个线程使用,但是在每次只能有一个线程能够使用它,当临界区资源正在被一个线程使用时,其他的线程就只能等待当前线程执行完之后才能使用该临界区资源
- 线程安全问题:某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等
- volatile关键字
- volatile 关键字用于修饰变量,它能确保变量的可见性,当线程中的一个共享变量被修改时,确保其他线程可以读到该变量的最新修改值
- Java的内存模型,JMM中,线程的共享变量存储在主内存中,而线程将平时运行时的数据存储在一个私有的本地内存,一般情况下当主内存中的数据被其他线程修改了的话,就会造成主内存中的数据和本地私有内存中的数据有出入,也就是我们所说的线程不安全问题
- 而volatile修饰的变量会在被修改之后第一时间将修改后的值更新到主内存中,而且线程本地对该变量的缓存是无效的,这就导致了所以线程在用到该变量的时候都要去主内存中获得最新值
- JMM:https://www.jianshu.com/p/8a58d8335270
- volatile:https://www.cnblogs.com/dolphin0520/p/3920373.html
- Java面试热门内容精讲之——并发编程volatile,https://www.bilibili.com/video/BV1BJ411j7qb?from=search&seid=11701760456153380846
- ThreadLocal
- 为了实现每个线程之间的ThreadLocal数据不互相干扰,Thread类中定义了一个名为 threadLocals,类型为 ThreadLocalMap 的成员变量来存储本线程运行过程有需要的变量副本,而当我们在程序运行的过程中对这些变量进行修改或者初始化的时候,程序就会从ThreadLocalMap获得数据并进行对应的操作
- ThreadLocalMap 中的键值为该ThreadLocal变量,而value值则为变量的值
- ThreadLocal变量时存在于普通对象之中的,定义方法:
- private static ThreadLocal
variableName = new ThreadLocal<>(); - ThreadLocal 类中设定了 get(), set(), remove() 等方法来方便我们对 ThreadLocal 变量进行操作
- ThreadLocal 类的 getMap() 方法:获得当前运行的线程的 ThreadLocalMap,return t.threadLocals
- https://www.cnblogs.com/dolphin0520/p/3920407.html
- https://www.jianshu.com/p/3c5d7f09dfbd
- 线程池
多线程面试问题:https://www.bilibili.com/video/BV16g411u7BW?t=86
多线程数据共享:cnblogs.com/E-star/p/3482170.html