type
status
date
slug
summary
tags
category
icon
password
AI 摘要
一个 Java 程序的运行是 main 线程和多个其他线程同时运行
Java 线程和操作系统的线程有啥区别?
JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
首先了解下面两个概念:
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。
一句话概括 Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程。
请简要描述线程与进程的关系,区别及优缺点?
首先说下什么是进程什么是线程,进程就是程序运行的基本单位,我们运行一个程序就是进程从创建到运行到消亡的过程。在java中,我们运行main函数就是启动一个jvm进程,其中main函数所在的线程就是主线程。
关系:一个进程可以有多个线程,多个线程共享进程的堆和方法区,但是每个进程都有自己的程序计数器、虚拟机栈和本地方法栈。
区别:进程之间基本上不会互相影响,但是同一进程中的线程有可能互相影响。
拓展:为什么程序计数器、虚拟机栈、本地方法栈是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
- 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的
一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
如何创建线程
Java创建线程有很多种方式,像实现Runnable、Callable接口、继承Thread类、创建线程池等等,不过这些方式并没有真正创建出线程,严格来说,Java就只有一种方式可以创建线程,那就是通过new Thread().start()创建。而所谓的Runnable、Callable……对象,这仅仅只是线程体,也就是提供给线程执行的任务,并不属于真正的Java线程,它们的执行,最终还是需要依赖于new Thread().start()
参考:
线程的生命周期和状态
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- NEW: 初始状态,线程被创建出来但没有被调用
start()。
- RUNNABLE: 运行状态,线程被调用了
start()等待运行的状态。
- BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用
start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。- 当线程执行
wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
- TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过
sleep(long millis)方法或wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
- 当线程进入
synchronized方法/块或者调用wait后(被notify)重新进入synchronized方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。
- 线程在执行完了
run()方法之后将会进入到 TERMINATED(终止) 状态。
什么是线程上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
- 主动让出 CPU,比如调用了
sleep(),wait()等。
- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
Thread#sleep() 方法和 Object#wait() 方法对比
共同点:两者都可以暂停线程的执行。
区别:
sleep()方法没有释放锁,而wait()方法释放了锁 。
wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)超时后线程会自动苏醒。
sleep()是Thread类的静态本地方法,wait()则是Object类的本地方法。为什么这样设计呢?下一个问题就会聊到。
为什么 wait() 方法不定义在 Thread 中?
wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。类似的问题:为什么
sleep() 方法定义在 Thread 中?因为
sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。可以直接调用 Thread 类的 run 方法吗?
这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个
Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。总结:调用
start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
双重校验锁实现对象单例(线程安全):
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:- 为
uniqueInstance分配内存空间
- 初始化
uniqueInstance
- 将
uniqueInstance指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用
getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。volatile关键字的作用
- 保证变量的可见性,它修饰的变量是共享且不稳定的
- 防止jvm指令重排序
- 不能保证变量的操作是原子性的
悲观锁和乐观锁
悲观锁:担心线程被其他线程使用,典型的是synchronized和ReentrantLock;多用于写比较多的情况
乐观锁:不担心共享资源被其他线程修改;多用于写比较少的情况
如何使用 synchronized?
synchronized 关键字的使用方式主要有下面 3 种:- 修饰实例方法
- 修饰静态方法
- 修饰代码块
1、修饰实例方法 (锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
2、修饰静态方法 (锁当前类)
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
静态
synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。3、修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:
synchronized(object)表示进入同步代码库前要获得 给定对象的锁。
synchronized(类.class)表示进入同步代码前要获得 给定 Class 的锁
总结:
synchronized关键字加到static静态方法和synchronized(class)代码块上都是是给 Class 类上锁;
synchronized关键字加到实例方法上是给对象实例上锁;
- 尽量不要使用
synchronized(String a)因为 JVM 中,字符串常量池具有缓存功能。
synchronized 和 volatile 有什么区别?
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好 。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块 。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
介绍一下ReentrantLock
synchronized和ReentrantLock有什么区别?
- 两者都是可重入锁
可重入锁
也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。- ReentrantLock 比 synchronized 增加了一些高级功能
主要是:等待可中断、可实现公平锁、可实现选择性通知
- 作者:阿蒙
- 链接:https://blog.51xyw.xyz//article/javaConcurrentProgram
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。