内容回顾:
且听下回分解..........
目录
一、synchronized 关键字-监视器锁monitor lock
1) 直接修饰普通方法: 锁的 SynchronizedDemo 对象
2)修饰静态方法: 锁的 SynchronizedDemo 类的对象
前言:
本文章内容介绍synchronized关键字和volatile关键字,只有也包括wait和notify两个线程方法的介绍。
一、synchronized 关键字-监视器锁monitor lock
1.synchronized 的特性
1)互斥
synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到同一个对象 synchronized 就会 阻塞等待
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
注意:
- 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的一部分工作.
- 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
- synchronized的底层是使用操作系统的mutex lock实现的.
2) 刷新内存
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.
3) 可重入
理解 "把自己锁死"
一个线程没有释放锁, 然后又尝试再次加锁 .// 第一次加锁, 加锁成功 lock(); // 第二次加锁, 锁已经被占用, 阻塞等待. lock();
按照之前对于锁的设定, 第二次加锁的时候 , 就会阻塞等待 . 直到第一次的锁被释放 , 才能获取到第二个锁,但是由于第二个锁导致这个线程处于阻塞状态,而阻塞状态就导致第一个锁无法解开,从而使B锁也无法解开,都得互相开锁才能进行下一步,就和狗和猫打架谁也不让谁一样,这就是 死锁 .
这样的锁被称为 不可重入锁(比较经典的死锁问题:哲学家就餐问题,可以去了解一下)
死锁的四个必要条件:(前三个为锁本身特点)
- 互斥使用:一个锁被一个线程占用了之后,其他线程占用不了(锁本质,保证原子性)
- 不可抢占:一个锁被一个线程占用了之后,其他线程不能把这个锁抢走
- 请求和保持:当一个线程占据了多把锁之后,除非显示的释放锁,否则这些锁始终都是被该线程保持有
- 环路等待:等待关系成环 A等B,B等C,C等A.......
幸好我们的synchronized是一个可重入锁,所以不会出现以上问题(但是不代表不会出现其他死锁问题)
可重入锁;
代码示例在下面的代码中 ,
- increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 前对象加锁的.
- 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁没释放, 相当于连续加两次锁)
这个代码是完全没问题的 . 因为 synchronized 是可重入锁 .
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
可重入锁带来的问题:
- 可重入锁的意义在于降低了程序猿的负担(提高开发效率)
- 代价,程序中需要又更高的开销(维护属于哪个线程,并且加减计数,降低了运行效率)
在实际开发中,如果需要使用嵌套锁,一定要约定好加锁的顺序。即所有锁都是按照a->b->c顺序加锁,别有的abc,有的cba.......
2.synchronized使用示例
synchronized 本质上要修改指定对象的 " 对象头 ". 从使用角度来看 , synchronized 也势必要搭配一个具体的对象来使用
1) 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
2)修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
3)修饰代码块: 明确指定锁哪个对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
我们重点要理解,synchronized 锁的是什么 . 两个线程竞争同一把锁 , 才会产生阻塞等待, 两个线程分别尝试获取两把不同的锁 , 不会产生竞争 .
3.补充说明
String是一个线程安全的类,但是不是因为他有加锁操作,事实上它并没有加锁,安全的原因在于,String本身就是不可修改的,也就是不怕多个线程修改字符串的问题
其他支持线程安全的类
- Vector (不推荐使用,无脑加锁,效率低)
- HashTable (同上)
- ConcurrentHashMap
- StringBuffer
线程不安全的类
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
- ..........
二、volatile 关键字
1.volatile 能保证内存可见性
volatile 修饰的变量 , 能够保证 " 内存可见性 ".

代码在写入 volatile 修饰的变量的时候 ,
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候 ,
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况. 加上 volatile , 强制读写内存. 防止出现因Java编译器的代码优化而导致的主内存和工作内存信息不一致的问题,虽然这样速度是慢了, 但是数据变的更准确了。
代码示例:
在这个代码中
- 创建两个线程 t1 和 t2
- t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
- t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
- 预期当用户输入非 0 的值的时候, t1 线程结束
static class Counter {
public int flag = 0; }
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
上面代码t1 读的是自己工作内存中的内容. 当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.(这是由于代码优化的问题)
解决办法是给flag加上volatile
static class Counter {
public volatile int flag = 0; }
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.
2.补充说明:
1)volatile不保证原子性
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
这段代码的结构无法保证结果为10w,所以只能加锁
2)synchronized保证原子性与内存可见
static class Counter {
public int flag = 0; }
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
三、 wait 和 notify
为了控制线程顺序,可以使用wait和notify,这两种方法多用在锁的代码块中,前者是等待释放当前锁,后者是通知当前对象等待的线程并使其获得对象锁(wait和notify是应在在同一个对象上的)
1.wait方法
wait 做的事情 :
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 结束等待的条件 :
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
这样在执行到object.wait() 之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()
2.notify方法
notify 方法是唤醒等待的线程 .
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
- 创建 WaitTask 类, 对应一个线程, run 内部循环调用 wait.
- 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次 notify
- 注意, WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合
- 就需要搭配同一个 Object.
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
3.notifyAll
notify方法只是唤醒某一个等待线程 . 使用 notifyAll 方法可以一次唤醒所有的等待线程.
static class WaitTask implements Runnable {
// 代码不变
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notifyAll();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t3 = new Thread(new WaitTask(locker));
Thread t4 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
注意: 虽然是同时唤醒 3 个线程 , 但是这 3 个线程需要竞争锁 . 所以并不是同时执行 , 而仍然是有先有后的 执行.
notify方法与notifyAll方法区别
二者共同点在于都是将wait唤醒,不同在于前者一次只唤醒一个,多个线程的时候,由线程调度按照一定规则调度一个,后者是一次性唤醒全部,让那些线程去争一个锁。前者是被安排,后者是主动抢
4.wait方法与sleep方法对比(面试题)
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间
总结:
- wait 需要搭配 synchronized 使用. sleep 不需要.
- wait 是 Object 的方法 sleep 是 Thread 的静态方法
个人笔记使用