推荐阅读
- 学习笔记 《 深入理解 Java 虚拟机》
- 学习笔记 《 后端架构设计》
- 学习笔记 《 Java 基础知识进阶》
- 学习笔记 《 Nginx 学习笔记》
- 学习笔记 《 前端开发杂记》
- 学习笔记 《 设计模式学习笔记》
- 学习笔记 《 DevOps 最佳实践指南》
- 学习笔记 《 Netty 入门与实战》
- 学习笔记 《 高性能MYSQL》
- 学习笔记 《 JavaEE 常用框架》
- 学习笔记 《 Java 并发编程学习笔记》
- 学习笔记 《 分布式系统》
- 学习笔记 《 数据结构与算法》
各个线程之间相互的独立工作,每个线程都有自己的操作栈以及变量等信息。就像一个团队一样,如果每个人都只做自己的工作,缺少团队成员之间的相互交流,我想这种模式的工作,其价值也是很低的。对于对多线程而言,情况也是类似的,他们同属于某个进程,它们之间应该相互通讯,相互协同,已达到更好的效率实现更复杂的业务功能。比如A线程暂停等待B线程的执行完成后在执行,或者A线程等待某个标记位为真的时候在执行,或者A线程输出数据,B线程接收到数据,然后依次执行等等。
Java中提供了一些API,可以直接或者间接的达到这些需求,下面结合着代码示例来实际操作实现这些功能。
1、Volatile 与 Synchronized 关键字
1.1 Volatile 的应用
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程
修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当 的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。本文将深入分析在硬件层面上Intel处理器是如何实现volatile的,通过深入分析帮助我们正确地 使用volatile变量。
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存 模型确保所有线程看到这个变量的值是一致的。
public class SynchronizedClass {
// 定义标记位
private static boolean on = true;
public static void main(String[] args) {
new Thread(new MyRunnable(), "线程A").start();
SleepUtils.sleep(1);
// 修改标记位为false
on = false;
SleepUtils.sleep(2);
}
static class MyRunnable implements Runnable {
@Override
public void run() {
while (on) {}
System.out.println("程序执行完成,退出线程:" + Thread.currentThread().getName());
}
}
}
- 执行上面的程序,会发现,线程A并没有停止,为什么已经设置为false,仍然没有停止while循环呢?
这是因为虽然两个线程访问的是同一个对象,但是在内存中,各个线程持有的是该对象的拷贝数据,刚开始的值为true,线程A一直持有的拷贝值为true,即使变量值设置为false,线程A读取的依然是原值的拷贝,所以线程一直在执行。
通过使用关键字 volatile 就可以告诉线程,在读取该值的使用不要使用当前线程的拷贝,应该直接读取内存的值,此时程序的运行就达到了预期。即在标记为on的定义修改private static volatile boolean on = true;
即可。
1.2 Volatile实现原理
Lock指令(汇编指令,非JVM指令)是实现volatile的一个关键指令。为了提升处理速度,处理器并不直接和内存进行通讯,而是先将系统内存数据写入到内部多级缓存(L1,L2等)中,然后在进行操作,但是操作完之后,并不明确的知道,到底什么时候回写到内存中。而Lock指令主要有两个作用:
1. 立刻将当前处理缓存的数据写入到内存中
2. 这个写入操作会使得其他CPU缓存的改地址的数据无效。
如果对声明了volatile 的变量进行修改,JVM就会想处理器发出一条LOCK的前缀的指令,将变量所在的缓存写到内存中。其次,就算变量立刻写到内存里,其他处理器缓存的数据仍然是旧的,在执行命令仍然存在问题。所以每个处理器会通过嗅探在总线上传播的数据来检查自己的数据是不是失效了,当前处理器发现自己缓存行的地址发生了改变,就会将自己的缓存行数据设置为无效,当处理器对这个缓存行进行操作的时候,操作系统会从新的从内存中把数据缓存到处理器的缓存中,从而更新了缓存中的值。
1.3 Synchronized的应用与原理
在多线程编程中,synchronized主要用户保证方法或者代码块在同一时刻只能被一个线程独占访问,他保证了线程的可见性和排他性。 synchronized一直被认为是重量级的锁,随着JavaSE6的优化,其性能已经好了,有些情况反而不是那么重了。
synchronized 对于普通方法,锁的对象是当前对象,对于静态方法,锁的是当前对象的class对象,对于同步代码块,锁定的是给定的对象。下面的代码中展示了使用synchronized对一个class对象的同步访问。
public class SynchronizedClass {
public static void main(String[] args) {
synchronized (SynchronizedClass.class) {
}
}
public synchronized void synchronizedMethod() {}
}
在控制台使用 javap -v xxx.class
的方式,可以查看有助记符的解码码编译的内容,方便起见,下面仅做部分摘录
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/company/synchronizerd/SynchronizedClass
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
public synchronized void synchronizedMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
可以看到以下内容:
- 在使用synchronized在关键字的时候,使用字节码指令 monitorenter 以及 monitorexit 来实现锁的获取与释放。
- 在使用synchronized关键字修改方法的时候,使用的方法的标记 ACC_SYNCHRONIZED 来实现对对象的锁的获取。
- 无论哪种方式,其本质就是对对象的监视器进行获取,而这个过程是具有排他性的,有且仅有一个线程能够获取到该对象的监视器。
- 任何一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,必须获取这个对象的监视器,否则进入阻塞状态。
1.4 Synchronized 运行流程图
下图展示了synchronized的简单流程过程:
- 当任意线程对Object对象进行访问时候,首先都需要获取该对象的监视器
- 如果获取失败,则会尝试使用自旋锁获取,如果获取成功则执行代码,否则在指定次数获取失败后,将当前线程进入同步队列中,等待该对象的锁的释放的通知,通过该线程的状态修改为阻塞(BLOCKED)
- 如果获取成功,则进行对象访问(执行方法或者代码块),执行完成之后退出,并释放锁,释放锁的操作会唤醒在同步队列的阻塞线程
- 当持有该对象的监视器的线程释放锁之后,同步队列的线程会被唤醒之后,再次尝试获取该对象的监视器,重复上面的步骤
synorchnized 的锁的信息是放在对象头中的,就32位的JVM而言,对象头由的MarkWord以及 ClassMetadata Address 以及Array Length组成,其中Mark Work 由32位数据组成,这32位数据由不同状态的锁,所表示的含义不同,如下表格所示。
锁状态 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向互斥量的指针 | 10 | |||
GC标记 | 空 | 11 | |||
偏向锁 | 偏向线程ID | Epoch | 对象分代年龄 | 1 | 01 |
2、等待/通知机制原理
线程A修改了数据,线程B要感知到数据的变化,从而响应线程A的操作。数据的产生始于一个线程,数据的操作始于另外一个线程,这种模式类似于生产和消费者,前者生成或修改数据,后者对数据进行消费,那么在多线程的模式中,如何让线程B及时的感知到线程的A修改了数据是一个非常重要的问题。
常见的模式可以使用定时询问的方式,比如下面的伪代码
while(value == 1){
Thread.sleep(1000)
}
doSomething();
使用循环不停地遍历,直到值满足结果,然后执行相应的业务逻辑。 Thread.sleep(1000)
适用于防止过快的进行无效的判断,这个范式编程简单,存在一些问题:
- 难以确保及时性,在睡眠的时候,基本不消耗处理器资源,但是如果睡的太久,就不能及时的发现条件已经修改
- 难以降低开销,如果降低睡眠时间,那么需要消耗更多的资源,造成了无端的浪费。
2.1 使用示例代码
事实上,Object类内部的 wait
和 notify
方法,正好可以完美的解决这种场景。其方法的描述为:
方法名称 | 描 述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从wait()方法中返回,返回的前提是获得该对象的锁 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 调用该方法,线程进入WAITING 状态,只要外部线程调用对象的notify方法,并且是该对象的锁,才会从wait返回 |
wait(long) | 超时等待一段时间,如果没有通知就超时返回 |
wait(long,int) | 超时等待更详细的时间粒度,可精确到纳秒 |
等待/通知机制,就是线程A调用对象O的wait()方法进入等待状态,而另外一个线程B调用对象O的notify()方法并且释放掉对象O的锁之后,线程A接收到通知后从对象O的wait()方法中返回,继续执行后面的方法,所以这种称之为等待/通知机制。
下面的实例代码展示了简单的等待/通知机制。
public class WaitDemo {
private static volatile boolean flag = true;
private static final Object lock = new Object();
public static void main(String[] args) {
Thread wait = new Thread(new Wait(), "Wait");
wait.start();
Thread notify = new Thread(new Notify(), "Notfify");
notify.start();
SleepUtils.sleep(1);
}
static class Wait implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println("Wait.run Start1");
lock.wait();
System.out.println("Wait.run Start2");
} catch (InterruptedException ignored) {
}
}
System.out.println("Wait.run End");
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println("Notify.run Start");
// 通知的时候并不会立刻释放锁,而是等到当前代码块退出的时候才会释放锁,wait() 方法才会继续执行
lock.notify();
flag = false;
SleepUtils.sleep(2);
}
}
}
}
- 首先wait线程先被执行,进入run方法,然后执行了lock.wait() 此时wait线程的状态为WAITING
- 然后main方法中继续启动了Notify线程,同样进入了run方法,执行了lock.notify()方法,此时Wait线程并不会立刻执行,而是等待Notify线程释放lock的锁
- 在Notify线程释放lock的锁之后,也就是执行完synchronized代码块之后,线程Wait才接到通知,继续执行后续的代码
- 读者依据此流程可自行分析下输出的结果
2.2 运行流程图
2.3 总结分析
- 在使用wait(),wait(long),wait(long,int)以及notify()、notifyAll()方法的时候,需要首先获取到对象的锁
- 使用wait()方法之后,线程的状态会有RUNABLE转变为WAITING,并将当前线程放置到等待队列中
- notify()或者notifyAll()方法被执行后,等待线程并不会立刻从wait()方法中返回,而是继续等待调用notify()的线程释放对象的锁
- notify()方法是将线程中一个等待的线程从等待队列中移到同步队列中,notifyAll是将所有等待队列中的线程移动到同步队列中
- 从wait()方法中返回的要求是获得调用对象的锁
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。