一、线程和进程的区别?
1、进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
多实例进程就是可以打开多个
单实例进程就是只能打开一个
2、线程
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
一个进程之内可以分为一到多个线程。
3、进程和线程对比
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
二、并行和并发有什么区别?
1、单核CPU
- 单核CPU下线程实际还是串行执行的。
- 操作系统中有一个组件叫做任务调度器,将pu的时间片(windows下时间片最小约为15 毫秒)分给不同的程房使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。
- 总结为一句话就是: 微观串行,宏观并行。
- 一般会将这种线程轮流使用CPU的做法称为并发 (concurrent)。
对于单核CPU而言,一次只能执行一个线程,不过每个线程执行特别快,并且存在CPU调度,所以每个CPU执行一个线程,但是多个线程轮回切换。
2、多核CPU
每个核(core)都可以调度运行线程,这时候线程可以是并行的。
第一个时间片
第二个时间片
第三个时间片
第四个时间片
3、并发和并行的区别
- 如果是在单核CPU上,多个任务交替执行,那么就是并发。
- 如果是在多核CPU上同时执行多个任务,那么就是并行。
三、线程创建的方式
1、继承Thread类
- 继承Thread类
- 重写run方法
- 创建继承Thread类的实例
- 调用start()方法启动线程
2、实现Runnable接口
- 实现Runnable接口
- 重写run方法
- 创建实现Runnable接口的对象的实例
- 创建Thread类对象,并将实例包装在Thread类中
- 运行start方法启动线程
3、实现Callable接口
- 实现Callable接口
- 重写call方法
- 创建实现Callable的类的实例
- 创建Futuretask方法,将实例包装进去
- 创建Thread类,将Futuretask实例包装进去
- 执行start方法,开启线程
- 如果需要获得返回值,调用futuretask实例的get()方法获取返回值
4、线程池创建线程
- 实现Runnable接口
- 重写run()方法
- 创建线程池对象
- 提交任务
- 关闭线程池
5、Runnable 和 Callable 有什么区别?
- Runnable 接口run方法没有返回值。
- Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
- Callable接口的()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。
6、在启动线程的时候,可以使用run方法吗? run()和 start()有什么区别?
- 线程启动时候,只能使用start方法开启线程
- run方法执行只是使用当前主线程去执行这个方法
- start方法是创建一个线程,然后通过子线程去执行这个方法
- 同一个run方法可以执行多次,同一个start方法执行执行一次
四、线程包括哪些状态,状态之间是如何变化的?
1、线程的状态
- 新建
- 就绪
- 运行
- 阻塞
- 死亡
- 当创建一个线程但未执行start()方法,此时是新建状态
- 当执行start()方法就是就绪状态,等待CPU的调度
- 当获取CPU的调度的时候就是运行状态
- 当运行中线程无法获取锁、执行了wait()方法、执行sleep()方法,就进入阻塞状态
- 当阻塞中的线程获取锁,执行了notify()方法,等待时间过期,就进入就绪状态
- 当线程执行完毕,就进入死亡状态
2、概括
线程包括哪些状态
- 新建(NEW)
- 可运行(RUNNABLE)
- 阻塞(BLOCKED)
- 等待 (WAITING)
- 时间等待(TIMEDWALTING)
- 终止(TERMINATED)
线程状态之间是如何变化的
- 创建线程对象是新建状态
- 调用了start()方法转变为就绪状态
- 线程获取到了CPU的执行权,执行结束是终止状态在就绪状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized或lock) 进入阻塞状态,获得锁再切换为可执行状态。
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态。
- 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态。
五、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
使用join()方法解决
代码例子:
- 首先创建线程t1
- 然后创建线程t2,在线程t2中加入t1.join()方法,就会阻塞当前线程让t1线程先执行完,然后再执行当前线程。
- 然后在线程t3加入t2.join()方法,就会阻塞当前线程让t2线程先执行完,再执行当前线程。
六、notify()和 notifyAlI()有什么区别?
- notifyAll:唤醒所有wait的线程
- notify: 只随机唤醒一个 wait 线程
七、java中wait和sleep方法的区别?
1、共同点
wait0,wait(long)和 sleep(long)的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
2、不同点
方法归属不同
- sleep(long)是Thread 的静态方法。
- 而wait(),wait(long)都是Object 的成员方法,每个对象都有。
醒来时机不同
- 执行 sleep(long)和 wait(long)的线程都会在等待相应毫秒后醒来。
- wait(long)和 wait0 还可以被 notify 唤醒,wait0 如果不唤醒就一直等下去。
- 它们都可以被打断唤醒
锁特性不同(重点)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制。
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)。
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁 (我放弃 cpu,你们也用不了)。
八、如何停止一个正在运行的线程?
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止(不推荐,方法已作废)。
- 使用interrupt方法中断线程:
- 打断阻塞的线程 ( sleep,wait,join )的线程,线程会抛出InterruptedException异常。
- 打断正常的线程,可以根据打断状态来标记是否退出线程。
九、Sychronized底层原理
1、Sychronized基本使用
如上图,对于抢票,如果不加锁,就会出现超卖或者同一张票多次出售的情况。为了避免这种情况就需要使用Sychronized。如下图结果:
Synchronized(对象锁)采用互斥的方式让同一时刻至多只有一个线程能持有(对象锁),其它线程再想获取这个(对象锁)时就会阻塞住。
2、synchronized关键字的底层原理-基础
- Monitor:翻译为监视器,由JVM提供,C++语言实现
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList: 关联没有抢到锁的线程,处于Blocked状态的线程
- WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
当一个线程尝试获取锁的时候,首先让该线程的对象和Monitor进行关联,就会判断Monitor结构中的Owner是否为null,如果为null,则持有Owner并获取锁
当后续的线程如果获取锁的时候,就会判断Owner是否为null,如果不为null,则加入EntryList集合中阻塞并进行等待Owner为null。注意EntryList不是队列,里面的线程谁先抢到Owner谁获取锁。
对于WaitSet集合,则是线程调用wait()方法就会加入该集合中
3、 基础回答Sychronized底层原理概括
- synchronized关键字的底层原理
- Synchronized(对象锁)采用互斥的方式让同一时刻至多只有一个线程能持有(对象锁)
- 它的底层由monitor实现的,monitor是jvm级别的对象 (c++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner、 entrylist、 waitset:
- 其中owner是关联的获得锁的线程,并且只能关联一个线程
- entrylist关联的是处于阻塞状态的线程
- waitset关联的是处于Waiting状态的线程
十、synchronized关键字的底层原理-进阶
1、Monitor实现的锁属于重量级锁,你了解过锁升级吗?
Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题
前面已经讲到过了重量级锁,那么lock对象是如何和Monitor进行关联的呢?下面进行讲解
2、对象的内存结构
3、MarkWord
- hashcode:25位的对象标识Hash码
- age:对象分代年龄占4位
- biased lock: 偏向锁标识,占1位,0表示没有开始偏向锁,1表示开启了偏向锁
- thread: 持有偏向锁的线程ID,占23位
- epoch: 偏向时间戳,占2位
- ptr_to lock _record: 轻量级锁状态下,指向栈中锁记录的指针,占30位
- ptr to heavyweight monitor: 重量级锁状态下,指向对象监视器Monitor的指针,占30位
因此,对象头Mark Word里面由指向重量级锁的Monitor的指针ptr to heavyweight monitor。这个指针会指向Monitor的地址。因此lock对象根据对象头的重量级锁指针保存到Monitor地址来进行关联的。
4、Monitor重量级锁
每个Java 对象都可以关联一个Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor对象的指针 。
当多个线程发生竞争就会升级为重量级锁。一旦发生竞争就会升级为重量级锁。
5、轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
锁重入情况下使用轻量级锁
如上代码,当前对象由两个方法,分别为method1和method2。其中method1方法加锁,代码块内执行了method2方法。这就会出现同一个线程对同一个锁获取了两次,即锁重入。这种情况下可以使用轻量级锁,否则对性能会产生较大的影响。
根据以上代码,可以得知对象的内存结构。第一部分为MarkWord,第二部分为klass Word,第三部分为object body。其中MarkWord保存的是无锁状态下信息。
如果此刻来了一个线程,要执行method1的方法,在线程执行的时候就会创建一个锁记录,叫做Lock Record。
Lock Record由两个结构,分别为:
- 锁记录:用来保存当前线程的轻量级锁的地址
- Object reference:用来指向获取该锁的对象。
每个线程的栈帧包含着轻量级锁的结构,里面的锁记录会存储锁对象的MarkWord
首先会让线程栈帧里面的Lock Record中的Object reference指向Object对象,是为了记录当前线程正在获取的锁对象,表示这个线程锁获取的锁是这个对象,因为Sychronized里面的对象是Object,所以线程锁获取的锁就是这个Object对象。
上面还有个Lock record地址 00。当前线程持有锁的时候,就会去修改Object对象的MarkWord。这个交换根据CAS算法来进行交换Lock record地址和Object对象的MarkWord进行交换,用CAS为了保证修改交换的过程是原子操作。
如果交换成功了,Object对象的MarkWord就会改称为Lock record的地址,表示这个对象现在拥有轻量级锁。
如果CAS失败了,第一个原因是多个线程竞争锁,这个时候不能使用轻量级锁,会直接升级为重量级锁。第二个原因是当前锁重入。
比如method1中调用了method2方法,在已经加锁的前提下再加一层锁,那么就会在栈帧中添加一个Lock Record,作为重入的计数,比如Lock Record是几个就会算是几重锁。
加入两次锁,至少要进行两次CAS操作。每加一个锁记录都要加入一个CAS操作。因为第一次已经将轻量级锁地址记录到Object对象的MarkWord中,因此第二次Lock Record就不用真正的去修改了。并且第二个LockRecord的Object reference也会指向Object。
当解锁的时候,就会从栈顶开始向下遍历,查看Lock Record中的MarkWord是否为null。如果为null,说明该锁是重入锁,那么从栈中删除,并且锁计数减1并重置。
当解锁最后一个锁的时候,LockRecord中的锁记录就会和Object对象的MarkWord进行交换。就会变回Object中MarkWord初始状态,表示当前对象未上锁。
6、轻量级锁流程的概括
加锁流程
- 在线程栈中创建一个Lock Record,将其obj字段指向锁对象
- 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
- 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁流程
- 遍历线程栈找到所有obj字段等于当前锁对象的Lock Record。
- 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
- 如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
7、偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS操作
Java 6中引入了偏向锁来做进一步优化: 只有第一次使用CAS 将线程ID设置到对象的Mark Word 头中,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
以以上代码为例子如下:
对于Object对象MarkWord存储的数据,当线程1执行m1方法的时候,就会加锁。
因为当前线程是同一个线程,那么就会将当前线程Id写入Object中的MarkWord中,并且将偏向锁的标志改为1。
当执行m2方法的时候,就不会进行CAS操作,而是判断Object的MarkWord中的线程Id是否为当前线程Id,如果是那么就会将Lock Record加入栈帧中,并且设置锁记录为null,Object reference指向Object
8、Monitor实现的锁属于重量级锁,你了解过锁升级吗?
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
一旦锁发生了竞争,都会升级为重量级锁
当一个线程访问同步块时,它首先尝试获取偏向锁,如果偏向锁被占用,那么它会尝试获取轻量级锁,如果轻量级锁获取失败,那么它会尝试获取重量级锁
十一、谈谈JMM(JAVA内存模型)
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存