1. 线程的生命周期
- 线程通过调用sleep进入睡眠状态
- 线程调用一个在I/O上被阻塞的操作
- 线程尝试得到一个锁,该锁被其他线程持有
- 线程正在等待某个触发条件
- run方法正常退出而导致死亡
- 一个未捕获的异常终止了run方法而使线程猝死
- Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
- 实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回结果
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
- 加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法
- 注:Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取返回结果,当不调用此方法时,主线程不会阻塞
2.线程和进程的关系
-
进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
-
线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
-
线程安全在三个方面体现:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized、volatile);
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before 原则)。
-
Java 线程同步的几种方法?
- 使用 Synchronized 关键字;
- wait 和 notify;
- 使用特殊域变量 volatile 实现线程同步;
- 使用可重入锁实现线程同步;
- 使用阻塞队列实现线程同步;
- 使用信号量 Semaphore。
3.进程之间通信方式
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。
- 管道
- 它是半双工的,具有固定的读端和写端;
- 它只能用于父子进程或者兄弟进程之间的进程的通信;
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
- 命名管道
- FIFO 可以在无关的进程之间交换数据,与无名管道不同;
- FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
- 消息队列
- 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 ID 来标识;
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
- 信号量
- 信号量(semaphore)是一个计数器。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
- 支持信号量组。
- 共享内存
- 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区;
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
4.直接调用run()方法和调用start()方法的区别
- 每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程;
- start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run() 方法体代码执行完毕,可以直接继续执行下面的代码;这时此线程是处于就绪状态,并没有运行。然后通过此 Thread 类调用方法 run() 来完成其运行状态,这里方法 run() 称为线程体,它包含了要执行的这个线程的内容,run() 方法运行结束,此线程终止。然后 cpu 再调度其它线程;
- run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接待用 run() 方法必须等待 run() 方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start() 方法而不是 run() 方法。
5.synchronize和volatile比较
- volatile是线程同步的轻量级实现,性能比synchronize较好,volatile只能修饰变量 synchronize可以修饰方法,代码块
- 多线程访问volatile不会发生阻塞
synchronize会发生阻塞 - volatile能保证变量在私有内存和主存间的同步,不保证变量的原子性
synchronize可保证变量原子性 - volatile保证变量在多线程之间的可见性
synchronize保证多线程之间访问资源的同步性
6.synchronized和Lock锁的区别
- Lock是一个接口
synchronized是一个关键字 - synchronized会自动释放锁
Lock必须手动释放 - Lock可以让等待锁的线程响应中断
synchronized会让等待锁的线程一直等待下去 - Lock是对块范围上锁的
synchronized能够作用于类,方法和代码块
7.wait和sleep的不同
- sleep方法只是让出了CPU,并不会释放锁 wait会释放锁,调用notify方法,才会解除wait方法
- sleep可以在任何地方使用 wait方法只能在同步方法或同步块中使用
- sleep是Thread的方法,调用会暂停此线程指定的时间,到时间会自动恢复 wait是Object的方法,调用会放弃锁,进入等待队列,调用notify或notifyAll会唤醒线程
8.java可重入锁ReentrantLockReentrantLock实现了Lock
- ReentrantLock来自J.U.C包,是一个可重入的互斥锁
- ReentrantLock等待可中断,当持有锁的线程长时间不释放锁时,正在等待的线程可以选择 放弃等待,可中断特性对处理执行时间较长的同步块很有帮助
- ReentrantLock可以实现公平锁,多个线程等待一个锁时,必须按照申请锁的时间顺序来获得 锁;而非公平锁不能保证这一点,在锁被释放时,任何一个等待线程均有机会获得锁。 synchronized锁是不公平的;ReentrantLock默认也是不公平的,可以通过传参使用公平锁
- 锁绑定多个条件,一个ReentrantLock可以绑定多个Condition对象。
9.Synchronized与ReentrantLock区别
synchronized 是和 for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。
- synchronized竞争锁会一直等待
reentrantLock的等待可中断 - synchronized无法实现公平锁
reentrantLock可以实现公平锁 - synchronized等待和唤醒线程要结合加锁对象的wait,notify或notifyAll
reentrantLock等待和唤醒线程要结合Condition的await,signal,signalAll - synchronized从JMM层面实现的
reentrantLock从代码层面实现 - synchronized出现异常会自动释放锁
reentrantLock不会自动释放锁,需要在finally{ }代码块显示释放
10.AQS—AbstractQueuedSynchronizer类
AQS 核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
内部有一个int类型的state变量,被volatile关键字修饰,保证线程之间可见性还有一个Node内部类(生成同步队列和等待队列),ASQ拥有一个同步队列和多个等待队列AQS定义了俩种资源共享方式:独占模式和共享方式。
11.ThreadLocal
- ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。
它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。 - 每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
在该类中,我觉得最重要的方法就是两个:set()和get()方法。当调用ThreadLocal的get()方法的时候,会先找到当前线程的ThreadLocalMap,然后再找到对应的值。set()方法也是一样。 - ThreadLocal特性:
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是- Synchronized是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
12.volatile的定义和实现原理
volatile 关键字是用来保证有序性和可见性的。这跟 Java 内存模型有关。我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU 也会做重排序的,这样做是为了减少流水线阻塞,提高 CPU 的执行效率。这就需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知道对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作、有序性实现的是通过插入内存屏障来保证的。
被 volatile 修饰的共享变量,就具有了以下两点特性:
- 保证了不同线程对该变量操作的内存可见性;
- 禁止指令重排序。一个字段被声明为volatile,java线程内存模型确保所有的线程看到这个变量的值是一致的
- 对有volatile变量修饰的共享变量写操作的时候,生成的汇编指令会多出一行lock前缀的代码 lock前缀指令会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存中
- 这个写回的操作会使得在其它CPU里缓存了该地址的数据无效
- volatile两条实现原则
- Lock前缀指令会引起处理器缓存回写到内存
- 一个处理器的缓存回写到内存会导致其它的处理器缓存无效
- 对有volatile变量修饰的共享变量写操作的时候,生成的汇编指令会多出一行lock前缀的代码 lock前缀指令会引发两件事情:
13.synchronized的实现原理和应用
- 表现形式
1.对于普通方法,锁是当前实例对象
2.对于静态方法,锁是当前类的Class对象
3.对于同步方法,锁是Synchronized括号内的配置对象
当一个线程试图访问同步代码块时,首先必须得到锁,退出或异常必须释放锁 - 实现原理
JVM基于加入和退出Monitor对象来实现方法同步和代码块同步
代码块同步使用monitorenter和monitorexit指令实现- monitorenter指令是在编译后插入到同步代码块的开始位置
- monitorexit指令是插入到方法结束处或者异常处
- 每个monitorenter必须有对应的monitorexit与之配对
- 一个monitor被持有之后,处于锁定状态, monitor 对象存在于每个 Java 对象的对象头中,线程执行到monitorenter,会尝试获取对象的锁
- 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
- synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
14.Synchronized的俩个用法
-
对象锁:方法锁 同步代码块锁
-
代码块:手动指定锁对象
- 方法锁:synchronized修饰普通方法,锁对象默认this
- 类锁:修饰静态方法 指定锁类Class对象
-
多线程访问同步方法的7种情况
1.俩个线程同时访问一个对象的同步方法 串行执行
2.俩个线程访问的是俩个对象的同步方法 并行执行 锁对象不同
3.俩个线程访问的是synchronized的静态方法 串行访问
4.同时访问同步方法与非同步方法 并行访问 非同步方法不受同步方法影响
5.访问一个对象的不同的普通同步方法 串行访问 默认锁this
6.同时访问静态synchronized和非静态synchronized方法 并行访问 锁不同 .class对象 this对象
7.方法抛异常后,会释放锁 -
总结:
1.一个锁只能被一个线程获取,没有拿到锁的线程必须等待(第1,5种情况)
2.每个实例都对应有自己的一把锁,不同实例之间互不影响;
例外:锁对象是*.class已经synchronized修饰的是static方法的时候,所有对象公用一把类锁 (第2,3,4,6种情况)
3.无论是方法正常执行完毕或者方法抛出异常,都会释放锁(第7种) -
性质
- 可重入
- 什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获得该锁
- 好处:避免死锁,提升封装性
- 粒度:线程而非调用
- 证明:
情况1:同一个方法可重入
情况2:可重入不要求是同一个方法
情况3:可重入不要求是同一个类
- 不可中断
- 一旦该锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁,如果比尔永远不释放锁,我只能一直等下去。
- 原理
- 加锁和释放锁的原理
获取和释放锁的时机:内置锁 - 可重入原理
1.JVM负责跟踪对象被加锁的次数
2.线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时, 计数递增
3.每当任务离开时,计数递减,当计数为0的时候,锁完全被释放 - 可见性原理缺陷
1.效率低:锁的释放情况少,试图获取锁时不能设定超时,不能中断一个正在试图获取锁的线程
2.不够灵活:加锁和释放锁的实际单一,每个锁仅有单一的条件
3.无法知道是否成功获取到锁
- 加锁和释放锁的原理
- 可重入