线程间的共享和协作
线程间的共享
JVM 会为每一个线程独立
分配虚拟机栈空间
,本地方法栈空间
以及程序计数器
,而对于共享内存
中的变量
,是对每一个线程而言是共享
的,因此多线程并发
访问共享内存
中的变量
时就会出现线程安全
问题。具体可以参考JVM 内存模型这篇博客。
synchronized 内置锁
在前面提到共享资源在多个线程并发访问时会出现线程安全问题
,而解决线程安全问题就是要解决以下两个问题,一是要保证共享资源的在多个线程之间是互斥访问
的,二是要保证共享资源在多个线程之间的数据同步
的。
我们用一张图来描述 synchronized 保证线程安全的本质原因:
从上图中,我们可以看出:
原子性:
互斥访问
保证了共享变量同一时刻
只有一个线程能够访问,体现了操作共享资源的原子性。
可见性:
数据同步
在线程获取锁时从主存中读取共享变量的值到线程工作内存,在释放锁之前将工作内存的共享变量值刷新到主存中,这就体现了共享变量在多线程之间的可见性
。
对象锁
synchronized 作用于对象实例方法上,对象锁是当前 this 对象。
public class SyncTest {
private int count;
//作用于实例方法上,对象锁是当前 this 对象
public synchronized void increase() {
count++;
}
}
synchronized 作用于对象实例方法内部的同步代码块上,对象锁是当前 this 对象/或者 monitor。
public class SyncTest {
private int count;
private Object monitor = new Object();
public void increase() {
// 对象锁是当前对象 this
synchronized (this) {
count++;
}
//对象锁是 monitor
//synchronized (monitor) {
//count++;
//}
}
}
类锁
其实类锁也是一个对象锁,为什么这样说呢?因为类锁使用的是一个类的 Class 对象作为锁, Class 是用来描述所有的类,因此使用 Class 对象也是一种对象锁,只是一般情况将其称为类锁而已。
//类锁:使用在类静态方法上
public synchronized static void change() {
//do sth
}
//类锁:SyncTest.class对象作为对象实例方法代码块锁
public static void change2() {
synchronized (SyncTest.class) {
//do sth
}
}
synchronized 注意点
synchronized 能够保证
线程安全
的前提是操作共享资源
的多个线程
必须持有的是同一把锁
。
线程的协作
等待/通知机制
是指一个线程A
调用了对象O
的wait()
方法进入等待状态
,而另一个线程B
调用了对象O
的notify()
或者notifyAll()
方法,线程A
收到通知
后从对象O的wait()
方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互。
JDK 提供实现等待/通知的 API
注意:以下方法不是 Thread 提供的,而是 Object 的。
- wait()
如果正在执行的线程内部调用执行了该方法,那么线程将进入
WAITING
状态(线程状态可以参考(ps:劣实基础–Java 并发编程基础知识),等待其他线程通知或者线程被中断才会返回,注意: wait() 会释放当前对象锁和释放 CPU 执行权,具体可以看下面介绍的锁池
和等待池
- wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。指定时间到了不会抛出异常,而是继续往下执行。除非在 wait 期间发生了中断,那么 wait 将出异常。
- wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒
- notify()
通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
- notifyAll()
通知所有等待在该对象上的线程
等待和通知的标准范式
等待方遵循如下原则:
- 获取对象锁。
- 如果条件不符合,那么调用该对象的 wait()方法,被其他 notify() 之后仍要检查条件。
- 条件满足则执行对应的逻辑。
伪代码如下:
synchronized(锁对象){
while(条件不满足){
锁对象.wait();
}
//满足条件处理对应的逻辑
}
通知方遵循如下原则:
- 获取对象锁。
- 改变条件。
- 通知正在等待对象锁的线程。
伪代码如下:
synchronized(锁对象){
改变条件
锁对象.notifyAll();
}
一个对象拥有两个池:
- 锁池
假设 A 线程持有对象 Object 的锁,此时其他线程想要执行该对象的某一个同步方法或者同步块,这些线程就会进入该对象的锁池中。
- 等待池
假设 A 线程正在同步方法或者同步块中执行中调用了object.wait() ,那么线程 A 就会进入对象 object 的等待池中,等待其他线程调用该对象的 notify() 或者 notifyAll() 方法。如果其他线程调用的 object.notity() 方法,那么 CPU 会从等待池中随机取出一个线程放入锁池中,如果其他线程调用 object.notifyAll() 那么 CPU 会将等待池中所有的线程到放入到锁池中,准备争夺锁的持有权。
看了上面的等待池和锁池的作用后,这里有一个疑问:notify 和 notifyAll 应该用谁?
如果多个线程都调用了 对象锁.wait() 方法,那么如果只是调用 对象锁.notify() 方法,那么不一定会唤醒你想要的那个线程,CPU 只是随机地都等待池种去取出一个线程放入锁池中,所以说最好是使用
notifyAll()
;
下面举一个老王和老张买小米9手机的栗子:
等待/通知 范式的应用
public class XimaoShop implements Runnable {
//锁
private Object lock = new Object();
private int xiaomi9Discount = 10;
/*
通知方:折扣改变的通知方法
*/
public void depreciateXiaomi9(int discount) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "收到总部通知,现在进行小米9打" + discount + "折活动,通知米粉们来买吧");
xiaomi9Discount = discount;
//通知客户:小米9打折了哦,赶紧去看看价格吧。
//notify() 随机通知一个等待线程
// lock.notify();
//notifyAll() 通知所有等待的线程
lock.notifyAll();
}
}
/*
等待方:查询小米9价格
*/
public void getXiaomi9Price() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "正在查询小米9价格");
//小米9的折扣还没低于8折,不要给我推销
while (xiaomi9Discount > 8) {
try {
System.out.println(Thread.currentThread().getName() + "发现小米9价格折扣为" + xiaomi9Discount + "太少,我要开始等待降价,老板,降价了,就通知我哦,开始等待...");
//等待:等待小米9降价
lock.wait();
System.out.println(Thread.currentThread().getName() + "收到通知:小米9搞活动,打折了哦,目前折扣为:" + xiaomi9Discount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "剁手买顶配小米9:" + xiaomi9Discount + "折购入");
}
}
@Override
public void run() {
getXiaomi9Price();
}
public static void main(String[] args) throws InterruptedException {
XimaoShop shop = new XimaoShop();
//老王想要买手机
Thread getXiaomiPriceThread = new Thread(shop);
//老张也要买手机
Thread getXiaomiPriceThread2 = new Thread(shop);
getXiaomiPriceThread.start();
getXiaomiPriceThread2.start();
Thread.sleep(1000);
//降价了
shop.depreciateXiaomi9(9);
Thread.sleep(1000);
//又降价了
shop.depreciateXiaomi9(8);
}
}
- lock.notify()
根据输出结果可以看出,当降价到满足条件时,只有 Thread-1 收到通知。
- lock.notifyAll() 的输出结果
根据输出结果可以看出,当降价到满足条件时,只有 Thread-1 和 Thread-2 都收到通知。
线程隔离ThreadLocal
ThreadLocal 即线程变量,是一个以ThreadLocal
对象为键、任意对象
为值的存储结构。这个结构 ThreadLocal.ThreadLocalMap
被附带在线程
上,也就是说一个线程
可以根据一个ThreadLocal
对象查询到绑定
在这个线程
上的一个值
, ThreadLocal
往往用来实现变量
在线程之间
的隔离
。
- 定义一个 ThreadLocal,存储的是 String 类型,默认存储 subject 的值为"我是默认值"。
public class ThreadLocalTools {
public static String subject = "我是默认值";
public static ThreadLocal<String> sThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return subject;
}
};
}
- 使用 ThreadLocal
package com.example.threadlocal;
public class ThreadLocalDemo {
public static void main(String[] args) {
Thread thread1 = new Thread("线程1") {
@Override
public void run() {
super.run();
ThreadLocalTools.sThreadLocal.set("Flutter");
String result = ThreadLocalTools.sThreadLocal.get();
System.out.println(Thread.currentThread().getName() + "-" + result);
//线程执行完,要清除
ThreadLocalTools.sThreadLocal.remove();
}
};
Thread thread2 = new Thread("线程2") {
@Override
public void run() {
super.run();
ThreadLocalTools.sThreadLocal.set("Android");
String result = ThreadLocalTools.sThreadLocal.get();
System.out.println(Thread.currentThread().getName() + "-" + result);
//线程执行完,要清除
ThreadLocalTools.sThreadLocal.remove();
}
};
thread1.start();
thread2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String result = ThreadLocalTools.sThreadLocal.get();
System.out.println(Thread.currentThread().getName() + "-" + result);
//线程执行完,要清除
ThreadLocalTools.sThreadLocal.remove();
}
}
运行结果:
线程2-Android
线程1-Flutter
main-我是默认值
我是默认值
从上面的运行结果可以看出,不同线程都拥有一个独有的 subject 的副本变量,不同线程对这个副本的修改都是针对当前线程的,对其他线程的 subject 副本变量不会造成影响。
记录于2019年4月12日