引言
在上一篇文章中,我们提到解决多线程共享变量的数据覆盖问题的方法是使用synchronized,synchronized 是解决线程安全问题最基础、最核心的关键字。很多初学者知道它可以 “加锁”,但锁的是什么、什么时候生效、不同对象之间是否互斥,往往一知半解。
这篇文章结合例子来详细说一下
首先我们要记住:synchronized 修饰普通实例方法 = 锁 当前调用该方法的对象(this)
一、基础原理
- 每个 Java 对象在堆内存中,都自带一把 对象监视器锁(Monitor Lock)。
- 线程要执行
synchronized实例方法,必须先获取该对象的锁。 - 锁被占用时,其他线程想访问该对象的任意
synchronized方法,必须阻塞等待。 - 方法执行完毕 / 抛出异常,锁会自动释放。
sleep()不会释放锁!
二、案例一:一个对象、一个加锁方法、一个普通方法
1.代码
import java.util.concurrent.TimeUnit;
public class duoxiancheng {
public static void main(String[] args) {
Data data = new Data();
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
data.fun1();
}
});
t1.start();
//休眠1秒
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
data.fun2();
}
});
t2.start();
}
}
class Data{
//加锁方法
public synchronized void fun1(){
//休眠 3秒
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("fun1...............");
}
//普通方法
public void fun2(){
System.out.println("fun2..............");
}
}
2.深度分析
Data data = new Data():可以看到主线程里面创建了一个data对象,在堆内存里面创建唯一data对象,对象自带一把专属对象锁;
t1.start() → t1 执行 data.fun1():t1去申请data对象的锁,成功抢到锁,并且持有这把锁,进入synchronized修饰的fun1,休眠3秒(休眠期间锁不会释放)
此时主线程main执行完t1.start()后开始休眠1秒;此时t1依然在睡眠中(t1睡眠和主线程睡眠是同时开始的)
1 秒结束,t2.start() → t2 执行 data.fun2():在代码里面的fun2没有synchronized修饰,不需要申请data对象锁,因此不需要等待t1释放锁,立即执行fun2方法:输出fun2.........
2秒结束,t1的休眠结束:输出fun1.......方法执行完毕,释放data对象锁
关键结论:普通方法不受 synchronized 影响,不需要抢锁,直接执行。
3.运行结果
fun2..............
fun1...............
三、案例二:一个对象、两个加锁方法
1.代码
package com.aaa.tread;
import java.util.concurrent.TimeUnit;
public class duoxiancheng {
public static void main(String[] args) {
Data data = new Data();
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
data.fun1();
}
});
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
data.fun2();
}
});
t2.start();
}
}
class Data{
public synchronized void fun1(){
//休眠 3秒
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("fun1...............");
}
public synchronized void fun2(){
System.out.println("fun2..............");
}
}
2.分析
若fun2也变成加锁方法,那么data类里面的所有方法都被加上锁,因此共用一把锁,执行顺序和第一个例子完全不一样
- t1 抢到 data 锁,休眠 3 秒
- 主线程 1 秒后启动 t2,t2 也要抢同一把 data 对象锁
- 锁被 t1 占用 → t2阻塞等待
- 3 秒到,t1 执行完、释放锁
- t2 拿到锁,执行 fun2
关键结论:同一个对象的所有 synchronized 实例方法,共用一把锁,完全互斥!
3.运行结果
fun1...............
fun2..............
四、案例三:两个对象、一个加锁方法、一个普通方法
1.代码
package com.aaa.tread;
import java.util.concurrent.TimeUnit;
public class duoxiancheng {
public static void main(String[] args) {
Data data1 = new Data();
Data data2=new Data();
//创建线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
data1.fun1();
}
});
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
//创建线程t2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
data2.fun2();
}
});
t2.start();
}
}
class Data{
public synchronized void fun1(){
//休眠 3秒
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("fun1...............");
}
public void fun2(){
System.out.println("fun2..............");
}
}
2.分析
在文章开头就说了,synchronized修饰实例方法就是锁当前调用该方法的对象,因此如果创建了两个data对象,那么他们各自有自己的锁,完全独立,互不干扰
t1 调用 data1.fun1()
- fun1 是
synchronized - 所以 t1 锁住 data1
- 然后 sleep 3 秒
t2 调用 data2.fun2()
- fun2 没有锁
- 而且操作的是 data2,不是 data1
- 所以 根本不需要等任何人
- 直接执行,直接输出!
最终时间线:
- t1 启动 → 锁 data1 → 睡 3 秒
- 主线程睡 1 秒
- t2 启动 → 执行 data2.fun2 () → 立刻输出 fun2
- 再过 2 秒 → t1 睡完 → 输出 fun1
关键结论:不同对象 = 不同锁 = 互不干扰!即使是同一个类的实例,只要对象不同,就不会阻塞。
3.运行结果3
fun2..............
fun1...............
总结
不同对象就意味着不同的锁,不同对象之间互不打扰,如果创建了两个对象但是执行同一个用synchronized修饰的实例方法呢?也是互不干扰!!!

2607

被折叠的 条评论
为什么被折叠?



