Java高并发之魂:synchronized深度解析 —— 学习笔记
课程链接 https://www.imooc.com/learn/1086
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。
- 修饰方法和对象后保证同一时刻只能有一个线程能访问到
- 具有可重入和不可中断性
- 无法知道是否已经获取到锁,无法设置等待超时,只有持有的线程释放或者出现异常自动释放。不够灵活无法选择性释放锁
对于悟空老师的 对象锁和类锁
这种分类,在按老师的课程学习时便于理解。但学完后,自己还是应该明白。synchronized
就两种:
- 一是同步方法
方法没什么异议。静态方法锁的是类.class
,普通方法锁的是this
- 二是同步代码块
同步代码块时锁加在参数对象上使用相同对象为参数的代码都会同步执行
。
这里有一个需要特殊注意的情况,就是当参数对象是类.class
。此时锁的是整个类对象。
下面的内容都是对这段原理的解释。
归根结底就是看 Synchronized 的参数(锁)是否同一个东西。用同一把锁的所有代码都得排队走。
第2章 Synchronized简介
让同学们对Synchronized关键字有整理概念,从官网解释引出通俗解释,便于理解。从Synchronized关键字的地位说明该关键字的重要性。代码演示不用并发手段会带来的问题,吸引同学们带着疑问继续学习。分享IDEA的调试技巧,便于同学们实际操作。
- 2-1 synchronized的作用 (03:11)
能够保证在同一时刻
最多只有一个
线程执行该段代码,以达到保证并发安全的效果。
当有一个线程正在执行这段代码时,其它线程就得排队等。可见这也是要时间成本的。 - 2-2 synchronized的地位 (00:51)
大佬级别 Synchronized 是关键字
由Java
原生支持。 - 2-3 不用并发手段的后果预警 (01:06)
代码实战:两线程同时a++,最后结果会比预计的少。 - 2-4 后果的代码演示和原因分析 (07:23)
代码演示
public class DisappearRequest1 implements Runnable {
static DisappearRequest1 instance = new DisappearRequest1();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();// 父线程等待子线程执行完再执行
t2.join();// 父线程等待子线程执行完再执行
System.out.println(i);
}
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
原因分析
上面的代码我们预期的是输出200000
但三次执行结果如下,均少于预期:
135797
108613
106960
原因在于i++
看上去是一个操作,但实际上包含了三个动作:
- 从内存读取
i
- 将
i
加一 - 将
i
的值写入到内存中
这三个动作应该属于一个原子操作但上面的代码并没有。。。所以我们需要用synchronized
来实现这一点。
第3章 Synchronized的两种用法(对象锁和类锁)
- 3-1 Synchronized的两种用法介绍(02:11)
- 对象锁
包括方法锁
(默认锁对象为this当前实例对象)和同步代码块锁
(自己指定锁对象) - 类锁
指synchronized修饰静态
的方法或指定锁为Class对象
- 3-2 对象锁的形式1-同步代码块(13:07)
同步代码块 synchronized (this)
public class SynchronizedObjectCodeBlock2 implements Runnable {
static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();
// Object lock = new Object();
private byte[] lock = new byte[0]; // 比 new Object() 便宜
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {}
}
@Override
public void run() {
// synchronized (lock) {
System.out.println("我是对象锁的代码块形式。我叫 " + Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束。");
// }
}
}
输出结果,两个线程同时执行,这也就是前面 i++
出坑的原因。
我是对象锁的代码块形式。我叫 Thread-1
我是对象锁的代码块形式。我叫 Thread-0
Thread-1 运行结束。
Thread-0 运行结束。
接下来我们去掉代码中 synchronized
的注释。输出结果如下,两个线程按顺序执行。Bug解决了。
我是对象锁的代码块形式。我叫 Thread-0
Thread-0 运行结束。
我是对象锁的代码块形式。我叫 Thread-1
Thread-1 运行结束。
synchronized 的参数
synchronized
的参数可以是任意对象实例
。如果我们只需要控制一把锁,那可以省掉lock
对象,直接使用this
。这也是大家常用的方式。
- 3-3 调试技巧-看线程生命周期(03:28)
在run()
内打断点,开启调试,然后按需要切换同线程放行。
@Override
public void run() {
// 在这里面打断点
}
- 3-4 对象锁的形式2-普通方法锁(03:58)
synchronized 修饰普通方法 (锁对象默认为 this)
public class SynchronizedObjectMethod3 implements Runnable {
static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("飞泥洗的");
}
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println("我是对象锁的方法修饰符形式,我叫 " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束。");
}
}
- 3-5 类锁的概念(04:54)
概念(重要)
Java类可能有很多个实例对象,但只有1个Class对象
形式1
:synchronized 加在static方法上。
形式2
:synchronized(*.class) 代码块.
本质
:所谓类锁,不过是 Class对象的锁而已。
用法效果
:类锁只能在同一时刻被一个对象拥有。
- 3-6 类锁的形式1:synchronized 加到静态方法上
synchronized 修饰静态方法 (锁对象默认为 *.class)
这里和前面不同的是,两个线程两个不同的实例。但是因为锁是加在类上的,所以对类的所有实例都有效。
public class SynchronizedClassStatic4 implements Runnable {
static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("飞泥洗的");
}
@Override
public void run() {
method();
}
public static synchronized void method() {
System.out.println("我是类锁的第一种形式:修饰 static 方法。我叫: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束。");
}
}
按顺序输出,正常。
我是类锁的第一种形式:修饰 static 方法。我叫: Thread-0
Thread-0 运行结束。
我是类锁的第一种形式:修饰 static 方法。我叫: Thread-1
Thread-1 运行结束。
飞泥洗的
我们尝试去掉 static
变成 public synchronized void method() {...}
输出如下。
因为普通方法锁的是this
但现在两个线程是两个不同的实例,所以它们不相干了。
我是类锁的第一种形式:修饰 static 方法。我叫: Thread-1
我是类锁的第一种形式:修饰 static 方法。我叫: Thread-0
Thread-1 运行结束。
Thread-0 运行结束。
飞泥洗的
- 3-7 类锁的形式2:synchronized(*.class)
同步代码块 synchronized (*.class)
public class SynchronizedClassClass5 implements Runnable {
static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();
@Override
public void run() {
method();
}
public void method() {
synchronized (SynchronizedClassClass5.class) {
System.out.println("我是类锁的第二种形式: synchronized (*.class)。我叫: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束。");
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("飞泥洗的");
}
}
输出如下,成功按顺序执行。
我是类锁的第二种形式: synchronized (*.class)。我叫: Thread-1
Thread-1 运行结束。
我是类锁的第二种形式: synchronized (*.class)。我叫: Thread-0
Thread-0 运行结束。
飞泥洗的
- 3-8 消失的请求解决方案(04:25)
总结:学完了,回来收拾一下那个 i++
。
对象锁
synchronized 修饰普通方法
默认锁 this
public class DisappearRequest1 implements Runnable {
static DisappearRequest1 instance = new DisappearRequest1();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();// 父线程等待子线程执行完再执行
t2.join();// 父线程等待子线程执行完再执行
System.out.println(i);
}
@Override
public synchronized void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
synchronized (this) 同步代码块
直接锁this
的话等同于写在方法前。
//重复代码略
@Override
public void run() {
synchronized (this) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
如果同一个实例中要加几个不同的锁 this
就不够用了。可以如下创建一个特殊的对象用来上锁。
//重复代码略
private byte[] lock = new byte[0]; // 比 new Object() 便宜
@Override
public void run() {
synchronized (lock) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
类锁
synchronized 修饰静态方法
public class DisappearRequest1 implements Runnable {
static DisappearRequest1 instance1= new DisappearRequest1();
static DisappearRequest1 instance2 = new DisappearRequest1();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
t1.join();// 父线程等待子线程执行完再执行
t2.join();// 父线程等待子线程执行完再执行
System.out.println(i);
}
@Override
public void run() {
methon();
}
private static synchronized void methon(){
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
synchronized (*.class) 同步代码块
//重复代码略
private void methon() {
synchronized (DisappearRequest1.class) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}