必备知识:
- 并发的概述:体现的是在单位时间内可以处理多任务交替处理问题的能力(比如1s内支持处理100个线程,则它的并发数就是100)
- 并发的问题:不同线程同时使用同一个资源(如对象等)并发生修改时会出现数据不一致的问题,强调的是线程的安全概念
synchronized关键字
:内置锁,可修饰方法
(含静态方法和非静态方法)、代码块
对象锁
、类锁
- 互斥访问:即 串行执行,无法同时执行
- 同步访问:即 并行执行,互不影响
延伸知识:
- 内置锁:每个锁都会有开锁和关锁的动作,而
synchronized
并不需要我们去处理开关动作,因为它的开关动作全部在Jvm
内部完成的,即开锁关锁动作已经被内置到 jvm 中去,所以它叫内置锁
- 锁的本质:synchronized 锁的本质是 jvm 的 monitor 机制
- 锁的作用:如果有一个对象拿了一个锁,其他对象也想拿这个锁就拿不到,即
互斥访问
修饰方法
- 修饰 非静态方法:
对象锁
在 java 的设计思想里面,如上方法,我们想使用它的话,就必须基于实例出来的对象进行public synchronized void blockFunc() {}
对象.blockFunc()
调用,在这里,synchronized
的作用范围是对象
,那么它就是一个对象锁
,所以上述方法等同于:public synchronized(this) void blockFunc() {}
- 修饰 静态方法:
类锁
而使用了public static synchronized void blockFunc() {}
static
关键字修饰的如上方法即为 静态方法,在使用的过程中,我们只要能拿得到类,就可以通过类.blockFunc()
直接使用,在这里,synchronized
的作用范围是类
,那么它就是一个类锁
,所以上述方法等同于:public static synchronized(class) void blockFunc() {}
修饰代码块
- 对象锁 代码块
在这里,public void blockCode() { synchronized (object) { //TODO coding } }
object
表示对象
,它可以是this
、全局/局部 的Object
等。当synchronized
后面的 变量 是对象
时,它是一个对象锁
。那么在并发场景中,同一实例对象
被多线程使用时,如果这个 变量 是同一对象,这些代码将会发生互斥访问
,反之这个 变量 如果不是同一个对象,则可以同步访问
;同理,如果是不同实例对象被多线程使用,那么他们之间是不存在互斥关系的,所以必然是同步访问
- 类锁 代码块
因为 类 是 唯一 的,所以只要public void blockCode() { synchronized (class) { //TODO coding } }
synchronized
后面的 变量 使用的是类
,这时候它就是一个类锁
,类 既然是唯一,那么发生的也只能是互斥访问
。
结论
对象锁
的特点:如果不同线程监视的是 同一个实例对象
,那么就会发生 互斥访问
,否则可以 同步访问
。
类锁
的特点:如果不同线程监视的是 同一个类
(包含同一个类实例出来的不同对象),只要是使用了 类锁
的代码,那么 必然
是 互斥访问
,与根据哪个实例出来的对象进行访问无关。
判断是否 互斥的标准
:锁 的 对象 是不是 同一个对象(该对象包括类和实例的对象),如果是 类
,那么就 必然 是 互斥访问
,如果是 实例对象
,那么就需要区分是不是同一个 实例对象,如果是,那么就是 互斥访问
,否则是 同步访问
示例说明
接下来将基于上面的结论,以实际示例的输出结果进行论证:
-
非静态方法的 对象锁
public class DateBean { public synchronized void blockFunc1() { System.out.println("DateBean.blockFunc1 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockFunc1 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } public synchronized void blockFunc2() { System.out.println("DateBean.blockFunc2 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockFunc2 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } }
-
强调使用
相同
对象访问 同一个方法:dateBean.blockFunc1()
,发生互斥访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockFunc1(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockFunc1(); } }, "BThread").start(); }
输出结果:
DateBean.blockFunc1 被线程 AThread 调用的开始时间:1687388503658 —①
DateBean.blockFunc1 被线程 AThread 调用的结束时间:1687388513671 —②
DateBean.blockFunc1 被线程 BThread 调用的开始时间:1687388513671 —③
DateBean.blockFunc1 被线程 BThread 调用的结束时间:1687388523683 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出 -
强调使用
相同
对象访问不同方法:dateBean.blockFunc1()
和dateBean.blockFunc2()
,发生互斥访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockFunc1(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockFunc2(); } }, "BThread").start(); }
输出结果:
DateBean.blockFunc1 被线程 AThread 调用的开始时间:1687414155667 —①
DateBean.blockFunc1 被线程 AThread 调用的结束时间:1687414165668 —②
DateBean.blockFunc2 被线程 BThread 调用的开始时间:1687414165668 —③
DateBean.blockFunc2 被线程 BThread 调用的结束时间:1687414175670 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出
1 和 2 的结论:
被synchronized
修饰的非静态方法,它的锁实质上是对象锁
,所以在不同线程中同时使用同一个对象
访问被synchronized
修饰的非静态方法时(无论是否同一个方法),则发生互斥访问
原理:
该并发场景有且只有一个对象:dateBean
,而public synchronized void blockFunc() {}
中的synchronized
是一个 对象锁,它的作用范围是dateBean
对象,所以当AThread
持有该 对象锁 时,其他并发线程就无法持有,那么就只能等待,直到这个 对象锁 被释放后,线程BThread
持有了它之后才能执行方法内的相关逻辑。
通俗的理解:
比如现在只有一辆自行车,但是想骑车的有 A、B 两人,那么这个时候 A 正使用这辆自行车在骑行,B 就只能等着,直到 A 骑完并归还了自行车,B才能使用这辆自行车进行骑行。-
强调使用
不同
对象的访问同一个方法:dateBeanA.blockFunc1()
和dateBeanB.blockFunc1()
,发生同步访问
(不同对象访问不同方法就留给读者自己去思考和验证吧)public static void main(String[] args) { DateBean dateBeanA = new DateBean(); DateBean dateBeanB = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBeanA.blockFunc1(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBeanB.blockFunc1(); //dateBeanB.blockFunc2(); } }, "BThread").start(); }
输出结果:不同对象访问同一方法
DateBean.blockFunc1 被线程 AThread 调用的开始时间:1687389856884 —①
DateBean.blockFunc1 被线程 BThread 调用的开始时间:1687389856884 —②
DateBean.blockFunc1 被线程 AThread 调用的结束时间:1687389866890 —③
DateBean.blockFunc1 被线程 BThread 调用的结束时间:1687389866890 —④
或者:不同对象访问不同方法
DateBean.blockFunc1 被线程 AThread 调用的开始时间:1687417037316 —①
DateBean.blockFunc2 被线程 BThread 调用的开始时间:1687417037316 —②
DateBean.blockFunc1 被线程 AThread 调用的结束时间:1687417047322 —③
DateBean.blockFunc2 被线程 BThread 调用的结束时间:1687417047322 —④输出现象:
① 和 ② 同时输出,10s 之后 ③ 和 ④ 同时输出
3 的结论:
结合 1 和 2 的结论,在不同线程中同时使用同一个类的不同实例对象
访问被synchronized
修饰的非静态方法时(无论是否同一个方法),发生的是同步访问
,它们各自执行各自的逻辑代码,彼此之间 互不影响。
原理:
该并发场景出现了 两个对象,分别是dateBeanA
和dateBeanB
,线程AThread
持有的是dateBeanA
的 对象锁,线程BThread
持有的是dateBeanB
的对象锁,它们可以同时拥有各自的 锁,所以彼此之间 互不影响
通俗的理解:
好比现在有两辆自行车,分别分配给想骑车的 A、B 两人,那么A、B想完成骑行这件事情时,只需要各自使用自己拥有的自行车就可以完成,两者之间互不影响。 -
-
静态方法的 类锁
public class DateBean { public static synchronized void blockFunc1() { System.out.println("DateBean.blockFunc1 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockFunc1 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } public static synchronized void blockFunc2() { System.out.println("DateBean.blockFunc2 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockFunc2 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } }
输出结果:
DateBean.blockFunc1 被线程 AThread 调用的开始时间:1687414155667 —①
DateBean.blockFunc1 被线程 AThread 调用的结束时间:1687414165668 —②
DateBean.blockFunc1 被线程 BThread 调用的开始时间:1687414165668 —③
DateBean.blockFunc1 被线程 BThread 调用的结束时间:1687414175670 —④
或者:
DateBean.blockFunc1 被线程 AThread 调用的开始时间:1687414155667 —①
DateBean.blockFunc1 被线程 AThread 调用的结束时间:1687414165668 —②
DateBean.blockFunc2 被线程 BThread 调用的开始时间:1687414165668 —③
DateBean.blockFunc2 被线程 BThread 调用的结束时间:1687414175670 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出结论:
以上类的静态方法经验证,无论是一个对象还是多个对象的任何组合访问,最终的输出结果均是一致:互斥访问
。
而出现这样一致的原因即为 synchronized 是一个 类锁,它是唯一的,与具体实例出多少对象是无关的 -
代码块的 对象锁
public static class DateBean { public void blockCode1() { synchronized (this) { System.out.println("DateBean.blockCode1 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockCode1 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } } public void blockCode1_1() { synchronized (this) { System.out.println("DateBean.blockCode1_1 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockCode1_1 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } } private Object object = new Object(); public void blockCode2() { synchronized (object) { System.out.println("DateBean.blockCode2 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockCode2 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } } public void blockCode2_2() { synchronized (object) { System.out.println("DateBean.blockCode2_2 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockCode2_2 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } } public void blockCode3() { Object mObject = new Object(); synchronized (mObject) { System.out.println("DateBean.blockCode3 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockCode3 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } } public void blockCode3_1() { Object mObject = new Object(); synchronized (mObject) { System.out.println("DateBean.blockCode3_1 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockCode3_1 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } } public void blockCode4() { Integer blockCodeObject = 127; // Integer blockCodeObject = 128; synchronized (blockCodeObject) { System.out.println("DateBean.blockCode4 被线程 " + Thread.currentThread().getName() + " 调用的开始时间:" + System.currentTimeMillis()); try { Thread.sleep(10_000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("DateBean.blockCode4 被线程 " + Thread.currentThread().getName() + " 调用的结束时间:" + System.currentTimeMillis()); } } }
-
强调使用
相同
对象的 同一个方法:dateBean.blockCode1()
,发生互斥访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode1(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode1(); } }, "BThread").start(); }
输出结果:
DateBean.blockCode1 被线程 AThread 调用的开始时间:1687519230610 —①
DateBean.blockCode1 被线程 AThread 调用的结束时间:1687519240612 —②
DateBean.blockCode1 被线程 BThread 调用的开始时间:1687519240612 —③
DateBean.blockCode1 被线程 BThread 调用的结束时间:1687519250616 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出 -
强调使用
相同
对象的 同一个方法:dateBean.blockCode2()
,发生互斥访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode2(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode2(); } }, "BThread").start(); }
输出结果:
DateBean.blockCode2 被线程 AThread 调用的开始时间:1687519525480 —①
DateBean.blockCode2 被线程 AThread 调用的结束时间:1687519535481 —②
DateBean.blockCode2 被线程 BThread 调用的开始时间:1687519535481 —③
DateBean.blockCode2 被线程 BThread 调用的结束时间:1687519545491 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出 -
强调使用
相同
对象的 不同方法:dateBean.blockCode1()
和dateBean.blockCode1_1()
,发生互斥访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode1(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode1_1(); } }, "BThread").start(); }
输出结果:
DateBean.blockCode1 被线程 AThread 调用的开始时间:1687519525480 —①
DateBean.blockCode1_1 被线程 AThread 调用的结束时间:1687519535481 —②
DateBean.blockCode1 被线程 BThread 调用的开始时间:1687519535481 —③
DateBean.blockCode1_1 被线程 BThread 调用的结束时间:1687519545491 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出 -
强调使用
相同
对象的 不同方法:dateBean.blockCode2()
和dateBean.blockCode2_2()
,发生互斥访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode2(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode2_2(); } }, "BThread").start(); }
输出结果:
DateBean.blockCode2 被线程 AThread 调用的开始时间:1687519525480 —①
DateBean.blockCode2_2 被线程 AThread 调用的结束时间:1687519535481 —②
DateBean.blockCode2 被线程 BThread 调用的开始时间:1687519535481 —③
DateBean.blockCode2_2 被线程 BThread 调用的结束时间:1687519545491 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出
1、2、3 和 4 的结论:
因为synchronized
的变量是this
和 实例出来的全局变量(对象)
,所以这时候它是一个对象锁:this
代表的是dateBean
对象,object
代表的是dateBean 对象中的对象,在同一个 dateBean 中它也是唯一的
。 所以被修饰的 方法块 的作用范围自然也是 对象,所以它就是对象锁
,那么在不同线程中同时对使用同一个对象
的 同一个代码块 或者 不同代码块 进行访问时,则发生了互斥访问
。
原理:
该并发场景有且只有一个对象,那就是dateBean
,而blockCode()
方法中synchronized (blockCodeObject) {}
方法块 的synchronized
是一个对象锁
,它的作用范围就仅仅只是dateBean
对象,所以当AThread
持有该 对象锁 时,其他并发线程就无法持有,那么就只能等待,直到这个 对象锁 被释放后,线程BThread
持有了它之后才能执行 方法块内 的相关的逻辑。
通俗的理解:
比如现在只有一辆自行车,但是想骑车的有 A、B 两人,那么这个时候 A 正使用这辆自行车在骑行,B 就只能等着,直到 A 骑完并归还了自行车,B才能使用这辆自行车进行骑行。-
强调使用
相同
对象的不同方法:dateBean.blockCode2()
和dateBean.blockCode3()
,发生同步访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode2(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode3(); } }, "BThread").start(); }
输出结果:
DateBean.blockCode2 被线程 AThread 调用的开始时间:1687676937120 —①
DateBean.blockCode3 被线程 BThread 调用的开始时间:1687676937120 —②
DateBean.blockCode2 被线程 AThread 调用的结束时间:1687676947133 —③
DateBean.blockCode3 被线程 BThread 调用的结束时间:1687676947133 —④输出现象:
① 和 ② 同时输出,10s 之后 ③ 和 ④ 同时输出 -
强调使用
相同
对象的不同方法:dateBean.blockCode3()
和dateBean.blockCode3_1()
,发生同步访问
public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode3(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode3_1(); } }, "BThread").start(); }
输出结果:
DateBean.blockCode3 被线程 AThread 调用的开始时间:1687676937120 —①
DateBean.blockCode4 被线程 BThread 调用的开始时间:1687676937120 —②
DateBean.blockCode3 被线程 AThread 调用的结束时间:1687676947133 —③
DateBean.blockCode4 被线程 BThread 调用的结束时间:1687676947133 —④输出现象:
① 和 ② 同时输出,10s 之后 ③ 和 ④ 同时输出5 和 6 的结论:
不管 5(全局对象+局部对象) 还是 6(各自方法内的局部对象) 示例中,synchronized
的 变量 都不是同一个对象,所以它们之间发生的是同步访问
。 -
同理,在强调使用
不同
对象的 同一个方法 或者 不同方法中,因为对象锁已经因为不同对象而不同了,所以发生的同步访问
-
强调使用
相同
对象的 同一个方法:dateBean.blockCode4()
在此,如果小伙伴足够细心的话,则会发现在blockCode4()
中局部 常量对象blockCodeObject
有两种赋值,分别是127
和128
,目前128
是被注释状态,这里并不是我忘记删掉,而是特意留着的一处风景:public static void main(String[] args) { DateBean dateBean = new DateBean(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode4(); } }, "AThread").start(); new Thread(new Runnable() { @Override public void run() { dateBean.blockCode4(); } }, "BThread").start(); }
输出结果:Integer blockCodeObject = 127
异步访问
DateBean.blockCode4 被线程 AThread 调用的开始时间:1687519525480 —①
DateBean.blockCode4 被线程 AThread 调用的结束时间:1687519535481 —②
DateBean.blockCode4 被线程 BThread 调用的开始时间:1687519535481 —③
DateBean.blockCode4 被线程 BThread 调用的结束时间:1687519545491 —④输出现象:
① 输出,10s 之后 ② 和 ③ 同时输出,在过 10s 后 ④ 才输出输出结果:Integer blockCodeObject = 128
同步访问
DateBean.blockCode4 被线程 AThread 调用的开始时间:1687676937120 —①
DateBean.blockCode4 被线程 BThread 调用的开始时间:1687676937120 —②
DateBean.blockCode4 被线程 AThread 调用的结束时间:1687676947133 —③
DateBean.blockCode4 被线程 BThread 调用的结束时间:1687676947133 —④输出现象:
① 和 ② 同时输出,10s 之后 ③ 和 ④ 同时输出结论:
blockCode4() 的并发情况会因为 Integer 的值而发生差异结果,原因在于 Integer 的取值范围是 -128 - 127,在这个范围内,它是一个常量值,它存在于一个常量池中,所有 Integer 值都指向该池中的同一个常量,所以实质上是同一个对象,所以发生的是互斥访问
;当 Integer 值为 128 时,它实际执行的是一个 new 动作申请出来的对象,那么每次执行 new 动作时候,虽然值都是 128,但是它们并不是相同对象,所以发生了同步访问
-
-
代码块的 类锁