先看:http://www.cyc2018.xyz/Java/Java%20%E5%B9%B6%E5%8F%91.html#%E4%B8%80%E3%80%81%E4%BD%BF%E7%94%A8%E7%BA%BF%E7%A8%8B
概念
饥饿状态:某几个进程长久得不到运行
活锁:几个进程同时谦让对方,使得没有进程得到资源
Java 中的 Long 是非原子性的,前 32 和后 32 不同写入,int 操作是原子性的
编译器为了减少中断流水线的次数,所以会进行指令重排,串行中不会发生的问题并行中就出现了
线程的生命周期

1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。
3、运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行 wait()方法,JVM 会把该线程放入等待池中。(wait 会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)
阻塞三种状态:
- BLOCKED: synchronized
- WAITING: 无限等待
- TIMED_WAITING: 有限时间的等待
5、死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。
使用线程
有三种使用线程的方法:
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
实现 Runnable 接口
需要实现 run() 方法。
通过 Thread 调用 start() 方法来启动线程。
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
必须要线程运行,FutureTask.get()才能有结果。 Future不能当做线程运行,必须得是FutureTask.
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
return 3;
});
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
Callable 接口和 Runnable 接口的不同之处:
- Callable 规定的方法是 call,而 Runnable 是 run
- call 方法可以抛出异常,但是 run 方法不行
- Callable 对象执行后可以有返回值,运行 Callable 任务可以得到一个 Future 对象,通过 Future 对象可以了解任务执行情况,可以取消任务的执行,而 Runnable 不可有返回值
get 方法会一直阻塞,直到结束,或者可以规定时间
多线程的实现有以下 4 个步骤:
1.创建一个线程,创建 Callable 的实现类 Race,并且重写 call 方法
ExecutorService ser=Executors.newFixedThreadPool(线程数目);
Race tortoise = new Race();
2.得到 Future 对象
Future
3.获取返回值
int num=result.get();
4.停止服务
ser.shutdown();
继承 Thread 类
同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
实现接口 VS 继承 Thread
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
守护线程
守护线程(后台线程)和非守护线程(前台线程)。 守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。不重要的线程
垃圾回收,内存管理都是后台线程
- 主线程默认是前台线程,前台线程创建的子线程默认是前台线程,守护线程创建的子线程默认是守护线程。当程序中只有守护线程运行时该程序便可以结束运行。
- 当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
- main() 属于非守护线程。
- 在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。setDaemon(true)必须在 start()前设置
线程的加塞运行
t.join()使 t 线程加塞到当前线程之前获得 CPU,当前线程进入等待状态,直到线程 t 结束为止,当前线程恢复为就绪状态,等待线程调度
可以通过传入线程 t2,并 t2.join()使得按照一定顺序的运行 t2->t
thread.Join 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续执行线程 B。
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello");
});
t.start();
new Thread(() -> {
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("done");
}).start();
线程的礼让
yield()让当前线程落选,让出 CPU 回到就绪状态,让线程调试器重新调度一次。让给优先级高或相等的线程获得执行的机会,没有相同优先级的线程是就绪状态,yield()就什么也不做,继续运行。
Thread.yield();
线程的定时执行
Timer 类 定时工具类 cancel(),schedule(TimerTask task,Date time,long period),time 时开始,period 是周期
TimerTask 此类 implements Runable,就是个线程
需要用户 extends 此类。都不是好方式。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 1. 延迟执行一次(只执行一次)
scheduler.schedule(() -> System.out.println("延迟2秒执行一次"), 2, TimeUnit.SECONDS);
// 3. 固定延迟执行(执行完再等指定时间)1s后开始3s执行一次
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("固定延迟任务: " + System.currentTimeMillis());
}, 1, 3, TimeUnit.SECONDS);
线程的中断
Thread.interrupt()通知线程中断,设置中断标志位,但不是立即中断,不会发生作用,需要在 run()中设置中断处理代码。比如 run()中的 while{},增加 if(Thread.currentThread().isInterrupted()),,break,退出线程。线程的正常终止也应是这样的。当在 sleep()期间中断会抛出异常,所以在 catch 块中重新设置中断标志位,来中断线程
while (!Thread.currentThread().isInterrupted()) {
xxx
}
- 对于 阻塞状态(如 sleep/wait/join) 的线程,调用
interrupt()会抛出InterruptedException,可以在 catch 中退出;- 阻塞队列的put, offer也会有类似异常
- 对于 非阻塞状态 的线程,只是设置了
interrupted标志位,不会抛异常,需要手动检查。
线程的终止
- 无限循环中使用退出标志
- stop(),不推荐。 stop()方法回立即释放所持有的锁,会将正在写入的数据写坏,比如写坏一半
- interrupt()
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
Java 中线程优先级是通过 Thread.setPriority(int)来设置的
常见问题
错误的加锁
对于 Integer,给 Integer 加锁是经常出错的,因为 Integer 是不变对象,当对其增加时,是另外创建了一个对象并赋值
线程的挂起和解挂
不推荐使用 suspend()和 resume()函数,会出现状态错误,使用自定义标志位,然后在线程中检查标志,实现挂起和解挂
挂起:while:标志为真,wait();
解挂:设置标志,notify()
线程的同步和互斥
用 synchronized 实现线程的互斥
- 用在语句块前
- 用在方法声明
用 ThreadLocal 实现线程局部变量
就是线程的私有变量,线程之间互不干扰,在线程类中声明定义即可
public ThreadLocal<Integer> sum=new ThreadLocal<Integer>();
Runnable task = () -> {
sum.set(0);
sum.get(); // 每个线程独立的
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
};
用 Object 类的 wait()和 notify()实现线程的同步
Java 中的 wait、notify、notifyAll 是 Object 类提供的线程协作机制,用于在多线程中实现等待与通知。
wait 方法让当前线程挂起,进入该对象的等待队列,并释放对象的锁,直到被其他线程调用 notify 或 notifyAll 唤醒。被唤醒的线程不会立刻执行,必须重新竞争锁,获取到后才能继续执行。
notify 方法唤醒该对象等待队列中的一个线程。被唤醒的线程同样需要重新竞争锁。notify 不释放锁,调用线程要等 synchronized 块执行完才会释放锁。
notifyAll 方法唤醒等待队列中的所有线程。这些线程会一起进入锁的阻塞队列等待竞争锁,先抢到的先执行,其它线程继续等待。
调用 wait、notify、notifyAll 必须在 synchronized 块中,否则抛出 IllegalMonitorStateException。wait 方法会抛出 InterruptedException,调用时需要 try-catch 处理。
为了避免虚假唤醒,使用 wait 时推荐搭配 while 判断条件,不建议使用 if。示例:
synchronized(obj) {
while (!condition) {
obj.wait();
}
// 执行逻辑
}
wait 会释放锁并挂起当前线程,notify/notifyAll 唤醒线程但不释放锁。线程被唤醒后必须重新竞争锁,等持有锁的线程释放后才能继续执行。
总结:
- wait:释放锁,当前线程挂起。
- notify:唤醒一个线程,不释放锁。
- notifyAll:唤醒所有线程,不释放锁。
- 都只能在同步块内使用。
单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对 Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印 ABC 的问题了吧,这是一道比较经典的面试题,题目要求如下:
建立三个线程,A 线程打印 10 次 A,B 线程打印 10 次 B,C 线程打印 10 次 C,要求线程同时运行,交替打印 10 次 ABC。这个问题用 Object 的 wait(),notify()就可以很方便的解决。代码如下:
public class MyThreadPrinter2 implements Runnable {
private String name;
private Object prev;
private Object self;
private MyThreadPrinter2(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();
//释放对象锁,后面的可以拿到锁
}
try {
prev.wait();//阻塞,等待prev的notify,才能继续运行
//也就是释放了这个对象锁,暂停这个线程
//必须有个notify才可以继续运行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);
MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);
MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);
new Thread(pa).start();
Thread.sleep(100); //确保按顺序A、B、C执行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
自己理解来看,就是设置额外变量设置条件,当条件不满足时,this.wait()阻塞,可以 if/while 块中
以下代码是子进程 5 次,主线程 3,然后循环 10 次的,通过一个 boolean 设置条件,每当条件不满足时,就阻塞下。
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
T t=new T();
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
t.subthread();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
t.mainthread();
}
}
}).start();
}
}
class T{
public boolean shouldSub=true;
public synchronized void subthread(){
if(!shouldSub){
try{
this.wait();
}catch(Exception e){
e.printStackTrace();
}
}
for(int i=0;i<5;i++){
System.out.println("sub : ");
}
shouldSub=false;
this.notify();
}
public synchronized void mainthread(){
if(shouldSub){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int i=0;i<3;i++){
System.out.println("main : ");
}
shouldSub=true;
this.notify();
}
}
内存屏障 和 volatile
内存屏障是硬件之上、操作系统或 JVM 之下,对并发作出的最后一层支持。再向下是是硬件提供的支持;向上是操作系统或 JVM 对内存屏障作出的各种封装。内存屏障是一种标准,各厂商可能采用不同的实现。
内存屏障的实现涉及大量硬件架构层面的知识,又需要操作系统或 JVM 的配合才能发挥威力,单纯从任何一个层面都无法理解。
volatile 变量规则:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。
volatile 变量规则只是一种标准,要求 JVM 实现保证 volatile 变量的偏序语义。结合程序顺序规则、传递性,该偏序语义通常表现为两个作用:
- 保持可见性
- 禁用重排序(读操作禁止重排序之后的操作,写操作禁止重排序之前的操作). 指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
通过volatile 标记,可以解决编译器层面的可见性与重排序问题。而内存屏障则解决了硬件层面的可见性与重排序问题。
volatile 并不能代替锁,只能让其他线程看到线程的修改,也无法保证一些复合操作的原子性。
另外,不能真正的保证线程安全,当两个线程同时修改一个数据时,依然会发生冲突,比如两个 i++,其实只加了一次
volatile 两大作用
1、保证内存可见性 2、防止指令重排
此外需注意 volatile 并不保证操作的原子性。保证的是 load,assign,store. 本身cpu和mem之间还有个cpu缓存。
volatile 关键字可以保证变量的可见性,因为对 volatile 的操作都在 Main Memory 中,而 Main Memory 是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或 Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。
volatile 还有一个作用就是局部阻止重排序的发生,对 volatile 变量的操作指令都不会被重排序,因为如果重排序,又可能产生可见性问题。 在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写都可以确保变量的可见性。但是实现方式略有不同,例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而 volatile 变量干脆都是读写内存。
注意点:
- 不能当计数器,因为计数器要保证读取-修改-存取是一个原子性操作
- 可以当标志位 boolean
volatile用处
禁止重排序
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为
uniqueInstance分配内存空间 - 初始化
uniqueInstance - 将
uniqueInstance指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
可见性
volatile 关键字可以保证变量的可见性,因为对 volatile 的操作都在 Main Memory 中,而 Main Memory 是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或 Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。
synchronized 和 volatile 有什么区别?
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好 。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块 。volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
Synchronized原理
synchronized 是基于对象头(Object Monitor)的 Monitor Lock 实现的。
每个对象都有一个 Monitor(监视器):
Monitor 中包含
Owner:当前持有锁的线程。EntryList:所有请求这个锁、但尚未获得的线程,存放在一个双向链表(或队列)中。WaitSet:主动调用wait()的线程,即已获得锁后进入等待状态的线程。WaitSet 是 JVM Monitor 结构的一部分,用于管理那些已获取锁但因调用wait()方法而挂起的线程; 被唤醒的线程会从 WaitSet 移入 EntryList,等待再次竞争锁;recursions:记录重入次数。
EntryList(同步队列)和 WaitSet(等待队列)。
获取锁 重入锁
| 步骤 | 行为 |
|---|---|
| 1️⃣ | 线程尝试进入同步块,JVM 检查对象 Monitor 的 Owner |
| 2️⃣ | 若无锁,则设置当前线程为 Owner,锁获取成功 |
| 3️⃣ | 若 Owner 是当前线程,说明是可重入,再次进入,recursion++ |
| 4️⃣ | 若已被其他线程持有,则线程进入 EntryList,被挂起(park) |
| 5️⃣ | 当锁释放,JVM 会从 EntryList 中唤醒一个线程,唤醒策略是“非公平的”(谁先排队不保证先获取) |
| 6️⃣ | 被唤醒线程尝试重新抢锁,抢到后设置为 Owner,继续执行同步代码块 |