首先来看一个加锁的具体案例 “使用两个线程对同一个变量相加”,大概了解加锁的意义…
代码如下:
public class demo2{
public static int count=0;
public static Object locker1=new Object(); //???需不需要new Object()创建对象
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(()->{
synchronized(locker1){
for(int i=0;i<50000;i++){
count++;
}
}
});
Thread thread2=new Thread(()->{
synchronized (locker1){
for(int i=0;i<50000;i++){
count++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
加锁前:
代码每次运行后的输出结果都不同,这是两个线程修改同一个变量造成的线程安全问题。
加锁后:
代码运行后的输出结果相同且正确
一、锁的使用方式
首先需要明确一点:Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁。
我们通常使用synchronized
关键字来给一段代码或一个方法上锁。它通常有以下三种形式:
1. 修饰普通方法
//1. 锁为当前实例 即SynchronizedDemo对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
2. 修饰静态方法
//2. 锁为当前Class对象 即SynchronizedDemo类对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
3. 修饰代码块
//3. 锁为括号里的对象
//3.1 括号里的this即SynchronizedDemo对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
//3.2 括号里的this即类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
//3.3 括号里的是o对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
}
}
无论是哪种写法,使用synchronized的时候都要明确锁对象(明确是对哪个对象加锁)
由此我们会发现写法1等价于3.1,写法2等价于3.2
文章开头的代码案例,使用了3.2的加锁方式,锁对象是locker1
二、 锁的特性与作用
synchronized用的锁是存在Java对象头里的,synchronized 本质上要修改指定对象的 “对象头”。
1. 锁的互斥性
synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象的 synchronized 就会阻塞等待。
注意:互斥的前提是两个线程拿到同一把锁
当thread1释放锁后,thread2才有可能拿到锁。因为可能是与thread2竞争的线程先拿到锁。
注意:锁的竞争者也必须是尝试加锁失败后,进入BLOCKED阻塞状态的线程
来看一个不同线程对不同对象加锁的情况,这种情况下就不能起到互斥的作用!
代码示例:
public class demo2{
public static int count=0;
public static Object locker1=new Object(); //???需不需要new Object()创建对象
public static Object locker2=new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(()->{
synchronized(locker1){
for(int i=0;i<50000;i++){
count++;
}
}
});
Thread thread2=new Thread(()->{
synchronized (locker2){
for(int i=0;i<50000;i++){
count++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
2. 锁的可重入性
synchronized 同步块对同一个线程来说是可重入的,不会出现自己把自己锁死的问题。
🔊 什么情况会锁死?
一个线程没有释放锁, 然后又尝试再次加锁。如果锁为不可重入锁,就会锁死。
public class demo3 {
public class SynchronizedDemo {
public void method() {
//第一次加锁
synchronized (this) {
}
//第二次加锁
synchronized (this) {
}
}
}
}
按照互斥性:第二次加锁时,进程需要阻塞等待第一次加的锁被释放才能进行第二次加锁。但是第一次的锁需要经过第二次的锁加上并释放之后才能释放…这个时候就进入了无底洞,没办法解锁再加锁,就是死锁状态 — 这样的锁被称为“不可重入锁”
而Java的 synchronized锁是可重入锁,就不会有上述死锁问题
3. 锁的作用 – 解决原子性问题
拿开头的案例分析,为什么加锁前运行结果不正确,加锁后结果就正确了呢?
首先要明确:两个线程修改同一个变量,count++的指令实际有三个步骤:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
多个线程参与修改可能造成指令的步骤执行有多种情况,下面列出4种(上一篇博客也提到过):
只有在可能性1、2情况下,运行结果正确,其他情况都不正确。即只有当:一个线程一次性执行完count指令的三个步骤后,另一个线程才对count进行下一次修改时结果才正确。这种特性也叫做“原子性”
加锁后,两个线程的指令执行情况可能性如下:
使用了synchronized加锁操作之后,可以实现指令的原子性,从而保证“一个线程在修改时另一个线程不能修改”。
即:加锁实际上是保证原子性,一定程度上保证了线程安全
注意:
加锁过程本身是非常消耗资源的。如果加锁过程太频繁,虽然能够保证线程安全,但是效率就会大大降低 ~
三、wait¬ify机制
使用该方法可以在多线程并发执行的环境下,有效控制多线程的执行顺序,使线程之间可以通信。
1. wait()方法
观察wait方法的使用效果,代码示例:
public class demo4 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
}
代码结果:由于wait的存在,线程一直在等待,进程始终没有结束
wait 做的事情:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁
注意:
wait一定要搭配 synchronized 来使用
调用 wait 的对象和 synchronized 里使用的锁对象必须是一个对象
wait结束等待的条件:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常
注意:调用wait方法的线程会一直处于阻塞状态,直到别的线程唤醒它
2. notify方法
使用wait方法后线程一直在等待,进程无法结束。那我们可否唤醒线程,让它从等待队列中出来呢?当然可以,我们使用到的就是notify方法。
notify 方法用来唤醒等待中的线程
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(没有 “先来后到”)
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
代码案例:
public class demo4 {
//创建一个locker作为锁对象
public static Object locker=new Object();
public static Object locker2=new Object();
public static void main(String[] args) {
Thread thread1=new Thread(()->{
System.out.println("线程1 - start");
synchronized (locker){
try {
locker.wait();//记住: wait需要搭配 synchronized 使用,否则会报错
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1 - end");
});
Thread thread2=new Thread(()->{
//让用户控制线程1的结束
Scanner scanner=new Scanner(System.in);
System.out.println("请输入任意内容: ");
// next会阻塞等待用户输入内容
scanner.next();
//用户输入内容后,代码往下执行.此时便可以接触线程1的阻塞了
synchronized (locker){
locker.notify();//记住: 调用notify的对象需要和调用wait的对象是同一个
}
//测试: 调用notify的对象与调用wait的对象不相同时的运行结果
// synchronized (locker2){
// locker.notify();//记住: 调用notify的对象需要和调用wait的对象是同一个
// }
});
thread1.start();
thread2.start();
}
}
注意:
调用 wait 的对象和 synchronized 里使用的锁对象必须是一个对象,且需要和调用 notify 的 对象相同
调用notify的时候会尝试进行通知,在有线程调度器随机挑选出个呈 wait 状态的线程唤醒。
如果当前当前对象中没有正在wait的线程,也不会有副作用
当多个线程等待的时候,notify是随机唤醒一个线程,notifyall是唤醒所有线程
3. 使用wait¬ify机制的好处
① 线程之间能够通信
② 可以一定程度上避免线程饿死
🔊 什么是线程饿死?
在并发执行情况下,各个线程间的执行顺序并不遵循”先来后到”,而是”力争前线”。参与竞争的 线程中某些竞争力大的线程每次都能冲到前线,拿到系统的资源,而有些线程被排挤在外。这 些被排挤的线程一直拿不到系统的资源,就会”饿死”
使用wait后,参与竞争的线程虽然还是那么多,由于部分线程受到wait条件唤醒的限制,需要等待条件被唤醒后才能抢夺到cpu资源,这就给了另一些线程机会。