Java并发编程:线程安全与多线程实战指南【个人八股】

这个部分强调的是Java并发编程,那么就需要强调一下多线程环境下的数据安全性问题!
首先是并行和并发 [针对多个任务],需要明确这两个概念!
接着往下去分就是线程和进程!进程代表的是一个个的应用,线程代表的是进程下一个个执行任务的执行单元
既然提到了线程,就需要考虑到如何保证线程安全?原子性,有序性,可见性【三大特性!】
有没有比线程更轻量的执行单元->携程【单线程支持并发!】
并行和并发的区别!
并行就是同一个时间段内多个任务同时并行执行!
并发就是在一个时间段内,在单核CPU当中,多个任务交替的执行过程!通过时间片轮转的机制来轮流使用CPU
你是如何理解线程安全的?
线程安全简单来讲就是一份数据怎么保证在多线程的环境下保证数据的正确读取和修改。
如果一段代码块或者一个方法被多个线程同时执行,还能够正确地处理共享数据,那么这段代码块或者这个方法就是线程安全的。
我们其实可以考虑通过原子性,可见性这2个特性来保证线程的安全性。【漏掉了有序性!!!】
原子性:一组操作要么完全成功,要么完全不操作!不会出现中间的状态!
原子性其实我们可以考虑用同步代码块synchronized的方式来进行实现,也可以考虑用线程安全的类来进行实现!
可见性:变量的修改,在多个线程要是可见的!在这个部分的话可以考虑用volatile来修饰变量,保证变量每次修改之后,都要及时的将变量刷新到主存当中,同时,每次读取变量,都从主存里面拉取最新的数据!
有序性:加锁的顺序是有序的!目的是为了保证在加锁的过程中不会有死锁的发生!【要确保线程不会因为死锁、饥饿、活锁等问题导致无法继续执行。】
线程和进程的区别?
进程说简单点就是我们在电脑上启动的一个个应用。它是操作系统分配资源的最小单位。
线程是进程中的独立执行单元。多个线程可以共享同一个进程的资源,如内存;每个线程都有自己独立的栈和寄存器。
【进程是系统分配资源的最小单位!】
如何理解协程?
一句话讲
协程比线程更轻量并且是支持在单线程中并发执行的!
协程被视为比线程更轻量级的并发单元,可以在单线程中实现并发执行,由我们开发者显式调度。
协程是在用户态进行调度的,避免了线程切换时的内核态开销。
Java 自身是不支持携程的,我们可以使用 Quasar、Kotlin 等框架来实现协程。
线程间是如何进行通信的?
一句话,有消息传递和共享内存这两种机制,但我们一般采用的是共享内存的方式来进行实现!
原则上可以通过消息传递和共享内存两种方法来实现。Java 采用的是共享内存的并发模型。
首先需要了解到的是,线程在工作的过程,都会保存一份本地的副本,每次修改都会先更新线程副本里面的变量值,然后定期讲线程副本里面的值刷新到内存当中!
那么至于这个机制,就需要考虑到JMM内存模型了,如果这个变量加上了volatile关键字,每次更新的时候都需要及时的刷新到内存当中,每次读取都要从内存里面去进行拉取!
这个模型被称为 Java 内存模型,简写为 JMM,它决定了一个线程对共享变量的写入,何时对另外一个线程可见。当然了,本地内存是 JMM 的一个抽象概念,并不真实存在。
用一句话来概括就是:共享变量存储在主内存中,每个线程的私有本地内存,存储的是这个共享变量的副本。
线程 A 与线程 B 之间如要通信,需要要经历 2 个步骤:
-
线程 A 把本地内存 A 中的共享变量副本刷新到主内存中。
-
线程 B 到主内存中读取线程 A 刷新过的共享变量,再同步到自己的共享变量副本中。
介绍一下下面这个部分
首先是从线程的创建这个角度来进行引入!有三种不同的方法来创建线程
接着进一步深入,刻意的提到执行不同的方法,如start()和run()方法会有不同的效果,顺带引出下面线程执行不同的方法也会进入不同的状态
那么不同的方法执行后线程会进入到不同的状态,那线程一共有多少种状态呢?
需要掌握的是通过执行不同的方法,会让线程进入到不同的状态当中!这是后续很多多线程的基础!
既然你会了很多的方法调用,那么 一个8G大小的内存可以启动多少个线程呢?同时线程的启动过程需要同步的启动那些其他的线程呢?
创建线程的方式有哪些?实际项目中推荐哪种?
一句话讲就是有三种创建
第一种是继承thread类,这种方式一般不推荐!因为在Java当中只能是单继承的!
第二种是实现接口,推荐!
第三种是如果需要返回的结果,可以考虑进行使用!
有三种,分别是继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。
第一种需要重写父类 Thread 的 run() 方法,并且调用 start() 方法启动线程。
这种方法的缺点是,如果 ThreadTask 已经继承了另外一个类,就不能再继承 Thread 类了,因为 Java 不支持多重继承。
第二种需要重写 Runnable 接口的 run() 方法,并将实现类的对象作为参数传递给 Thread 对象的构造方法,最后调用 start() 方法启动线程。
这种方法的优点是可以避免 Java 的单继承限制,并且更符合面向对象的编程思想,因为 Runnable 接口将任务代码和线程控制的代码解耦了。
【更推荐的是第二种方法,一方面是摆脱了单继承的限制,同时更加符合面向对象的编程思想】
第三种需要重写 Callable 接口的 call() 方法,然后创建 FutureTask 对象,参数为 Callable 实现类的对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,最后调用 start() 方法启动线程。
那这里就需要引出一个问题了,为什么不用run()方法来启动线程,而是用start()方法呢?
调用start()会新建一个线程,异步地执行run()方法来运行对应的代码块里面的内容!
如果是直接调用run()方法,相当于是在当前的主线程里面来运行任务,没有实现异步的效果!
也就是说,调用 start() 方法会通知 JVM,去调用底层的线程调度机制来启动新线程。
除了start()那你知道那些有关于线程的调度方法?
wait()让当前的线程进入等待的状态,notify()可以指定的唤醒一个等待中的线程!sleep()让当前线程休眠一段时间,yield()代表的是当前线程主动的让出执行权,但是不一定会被采纳!interrupted()可以打断正在等待中的线程!
interrupt() 方法用于通知线程停止,但不会直接终止线程,需要线程自行处理中断标志。
常与 isInterrupted() 或 Thread.interrupted() 配合使用。
目前的stop()方法已经废弃了,因为stop方法采用的是强制停止线程的机制,可能会导致线程出现数据的不一致性,导致后续的操作出错!
那既然提到了stop()强制停止线程的机制不可用了,如果业务当中有这个需要,你会怎么做强制停止?
第一步,调用线程的 interrupt() 方法,请求终止线程。
第二步,在线程的 run() 方法中检查中断状态,如果线程被中断,就退出线程。
既然你提到了sleep和wait,我想听听你对于这两个关键字的理解!他们之间的异同点
补充:这个部分的话,没有明确哪个类来进行负责
sleep 会让当前线程休眠,不需要获取对象锁,属于 Thread 类的方法;wait 会让获得对象锁的线程等待,要提前获得对象锁,属于 Object 类的方法。【不同点肯定不止以下两点】
-
sleep()方法可以在任何地方被调用。 -
wait()方法必须在同步代码块或同步方法中被调用,这是因为调用wait()方法的前提是当前线程必须持有对象的锁。否则会抛出IllegalMonitorStateException异常。
那你提到了线程的不同调度方法,可能会使其陷入不同的状态,那么线程一共有多少种不同的状态呢?
-
首先是线程刚刚创建的初始状态,new;还没有进行初始化的操作
-
接着是线程的就绪状态,runnable状态,代表当前的线程可以运行了,需要等待时间片
-
接着是线程的运行状态,running,线程正在运行当中的状态!
-
考虑到当前线程在运行的过程,可能会因为各种原因,执行了wait(),进入了等待的状态!
-
如果这个时候有一个线程尝试执行一段代码,但是这段程序正在运行并且加上了同步代码块进行修饰,那么线程就会进入到blocked阻塞状态!
-
当前的线程可能因为执行了sleep(),进入了休眠的状态,当休眠时间一到就可以接着去执行后续的代码!
-
线程运行结束,声明周期完毕,进入terminated 状态!

一个 8G 内存的系统最多能创建多少个线程?
理论上大约 8000 个。
创建线程的时候,至少需要分配一个虚拟机栈,在 64 位操作系统中,默认大小为 1M,因此一个线程大约需要 1M 的内存。
但 JVM、操作系统本身的运行就要占一定的内存空间,所以实际上可以创建的线程数远比 8000 少。
启动一个 Java 程序,你能说说里面有哪些线程吗?
一句话讲
main线程+垃圾回收线程+编译器线程!
首先是 main 线程,这是程序执行的入口。
然后是垃圾回收线程,它是一个后台线程,负责回收不再使用的对象。
还有编译器线程,比如 JIT,负责把一部分热点代码编译后放到 codeCache 中。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)