- 何为进程和线程
- 程序计数器为什么是私有的?
- 虚拟机栈和本地方法栈为什么是私有的?
- 堆和方法区
- 并发与并行的区别?
- 线程的生命周期和状态?
- 什么是线程死锁?
- 产生死锁的四个必要条件
- 如何预防和避免死锁?
- 说说 sleep() 方法和 wait() 方法区别和共同点?
- 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- synchronized 和 volatile 的区别?
- synchronized 和 ReentrantLock 的区别
- 请你说一下自己对于 AQS 原理的理解
- 并发编程的重要特性
进程:
进程是程序的运行的基本单位,系统运行一个程序就是从一个进程开始创建的。java中main方法的启动,就是启动了一个进程。
线程:
线程是比进程更小的执行单位。
一个进程中可以包括多个线程。
二者的区别:【从图中可以看出】
- 进程中可以包含多个线程。
- 多个线程之间共享堆和方法区
- 每个线程有自己的虚拟机栈、本地方法栈、程序计数器
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置
虚拟机栈和本地方法栈为什么是私有的?为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区堆:是进程中最大的一块内存,用于存放新创建的对象
方法区:用于存放已被加载的类信息、常量、静态变量、代码等。
并发:两个及其以上的作业在同一时间段内执行。
并行:两个及其以上的作业在同一时刻执行。
生命周期:
首先,创建线程的时候,处于NEW状态。
然后,调用start()方法后,称为,runnable状态
最后,当线程执行完run()方法之后,称为terminated终止状态
其中,当线程执行到wait()方法的时候,线程会进入等待状态,需要其他线程执行通知notify()操作,才可以返回到正常运行状态。
当线程通过sleep()方法可以将线程处于超时等待状态。等到超时时间到达后,自动回归正常运行状态。
当线程调用同步方法的时候,没有获得锁的情况下,线程会进入到blocked阻塞状态
死锁:
多个线程同时阻塞,它们都在相互等待资源的释放,由于线程被无限期阻塞,因此程序不可能正常终止,称为死锁
如果所示,线程A持有资源2,线程B持有资源1,它们都申请访问对方的资源,由于都不进行释放,因此进入死锁状态。
- 互斥条件:资源只由一个线程占用
- 请求与保持条件: 一个线程因为请求资源而阻塞的时候,对自己已经获得到资源不进行释放
- 不剥夺条件: 线程已经获得到资源在没有使用完之前不能被其他线程剥夺,只有自己用完了之后才进行释放。
- 循环等待条件: 多个线程之间形成了头尾相连的循环等待状态。eg:圆桌池饭筷子问题
预防的话,只需要破坏产生死锁的条件之一就可以。
破坏其中条件:
第一个,互斥条件是无法被打破的,因此线程安全就是以互斥的条件保证的。
第二个请求与保持,一次性申请所有的资源
第三个不可剥夺,占用一部分资源的线程进一步申请其他资源,如果申请不到的话,就主动释放它占有的资源
第四个循环等待,只需要按照顺序申请资源,按照某一个顺序申请资源就可以避免循环等待条件。
共同点:
它们都可以暂停线程的运行
区别:
sleep方法没有释放锁,而wait方法释放了锁
wait方法被调用的时候,线程不会自然苏醒,需要别的线程执行notify唤醒。
而sleep方法被调用的时候,超过了所设定的时间段,线程会自动苏醒。
new 一个 Thread,线程进入了新建状态。
调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。
start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
- volatile关键字是线程同步的轻量级实现方法,volatile只能用于变量,而synchronized用于修饰方法和代码块
- volatile关键字可以保证数据的可见性,而不能保证原子性。但是synchronized可以都保证
- volatile关键字用于解决多个线程之间的可见性,synchronized关键字解决多个线程之间访问资源的同步性。
主要有以下几点:
- 用法不同,synchronized可以用于修饰方法和代码块,而reentrantlock只能用于修饰代码块
- 获取锁和释放锁的方式不同,synchronized会自动的加锁和释放锁。进入synchronized的时候会自动加锁,离开其修饰的代码段的时候会自动释放锁,但是reentrantlock需要手动加锁,通过lock()方法和unlock()加锁和释放
- 锁类型不同,synchronized锁为非公平锁,也就是(都等待加锁的时候,进行抢占式加锁)而reentrantlock可以是公平锁也可以是非公平锁
AQS 的全称为(AbstractQueuedSynchronizer)
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出大量应用广泛的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列(虚拟的双向队列)锁实现的,即将暂时获取不到锁的线程加入到队列中。
并发编程的重要特性- 原子性: 一次或者多次操作的时候,要么全部执行,要么都不执行,不会受到干扰打断。
可以通过synchronized锁实现原子性 - 可见性: 当一个线程对共享变量进行修改的时候,另外的线程都是可以立即看到修改的最新内容。
可以通过synchronized锁/volatile锁实现可见性 - 有序性: 由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。
我们上面讲重排序的时候也提到过:
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
在 Java 中,volatile 关键字可以禁止指令进行重排序优化。