目錄
-
- 前言
- 八鎖現象
- 一、synchronized鎖的對象是方法的調用者
- 二、static在類加載就有Class模闆,鎖Class
前言
我們都知道,隻要有并發,就必然要鎖來保證線程安全,那什麼是鎖呢?這裡将舉8個例子,對應鎖的8個問題,讓你徹底搞懂鎖的原理與使用
八鎖現象
一、synchronized鎖的對象是方法的調用者
上面标題是先抛出的結論,我們一步步來得到這個結論
案例:電話類,有打電話和發短信兩個功能,兩個線程分别執行發短信和打電話,我們看下面代碼,是先列印發短信還是先列印打電話?
注:代碼中TimeUnit.SECONDS.sleep(1),這是JUC包中的睡眠方法,類似于多線程中的Thread.sleep(1000)
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//1、标準情況下,兩個線程先列印發短信,還是打電話?
public class test1 {
public static void main(String[] args) {
Phone1 phone = new Phone1();
new Thread(()->{
phone.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone1{
//發短信
public synchronized void sendMsg(){
System.out.println("發短信");
}
//打電話
public synchronized void call(){
System.out.println("打電話");
}
}

這裡運作結果是先發短信,為什麼呢?可能有人會覺得線程A先調用,是以先執行發短信,這是錯誤的!!!我們可以再看一個例子對比一下,再得出原因
我們在發短信加一個3秒睡眠,那列印結果是什麼?
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//2、發短信加一個3秒睡眠,兩個線程先列印發短信,還是打電話?
public class test2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone2{
//發短信
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
//打電話
public synchronized void call(){
System.out.println("打電話");
}
}
結果還是發短信,打電話。并且發短信等待了3秒才列印。現在我們可以回答上面留下的問題了,為什麼總是先發短信,再打電話。因為這是有鎖的存在!!!因為synchronized鎖的對象是方法的調用者,這兩個方法調用者都是phone這個對象,是以這兩個方法用的是同一把鎖,誰先拿到鎖誰就先執行,而phone.sendMsg()先加載執行,就先拿到鎖,是以call()方法隻能等待發短信用完鎖(釋放鎖),才輪到call()用鎖。就好像小餐館排隊如廁,隻有一個坑位(一把鎖),先進去關門,鎖上門。後面的必須等前面如廁完,開鎖并開門,才能用廁所。
我們再來,如果加一個普通方法hello,沒有synchronized關鍵字修飾,然後線程B調用呢
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//3、加一個普通方法hello,列印結果是?
public class test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
new Thread(()->{
phone.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.hello();
},"B").start();
}
}
class Phone3{
//發短信
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
//打電話
public synchronized void call(){
System.out.println("打電話");
}
public void hello(){
System.out.println("hello");
}
}
結果是先列印hello,為什麼呢?因為根據synchronized鎖的對象是方法的調用者這句話,這兩個方法就不是一個鎖,hello方法就沒有鎖,是以不會受到鎖的影響,自然因為發短信方法休眠3秒,就提前線上程B調用時就列印了。是不是用到同一把鎖才是關鍵。
我們再舉第4個案例,兩個對象,一個發短信,一個打電話,這樣結果是什麼呢?
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//2、兩個對象,分别調用,怎麼列印呢?
public class test4 {
public static void main(String[] args) {
//兩個對象
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{
phone1.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone4{
//發短信
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
//打電話
public synchronized void call(){
System.out.println("打電話");
}
}
這我們應該就能脫口而出了,先打電話後發短信,根據synchronized鎖的對象是方法的調用者來看,兩個方法屬于兩個不同對象,自然就是兩把鎖,就相當于可以進兩個不同的廁所,互不影響呀。是以才會先打電話,然後3秒後列印發短信。
二、static在類加載就有Class模闆,鎖Class
同樣我們先說出結論,後面舉例一步步論證
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//5、兩方法都加static,列印結果是?
public class test5{
public static void main(String[] args) {
Phone5 phone = new Phone5();
new Thread(()->{
phone.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
//Phone5的class全局唯一
class Phone5{
//發短信
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
//打電話
public static synchronized void call(){
System.out.println("打電話");
}
}
先列印發短信,為什麼呢?這是因為synchronized鎖的對象是方法的調用者這個原因嗎?不!!!這裡是因為涉及到反射,static在類加載時加載了反射得到對象的class,也就是Phone5對象的class,Phone5.class。那麼兩個方法都用static修飾,說明鎖的class相同。自然用的是同一把鎖,是以senMsg先調用的,就先上鎖執行,是以後面的線程B隻能等待,是以執行時等待3秒出現發短信,再出現打電話
我們再舉一個案例,如果在staic修飾基礎上,再執行個體化兩個對象分别調用兩個方法呢?
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//6、兩方法都加static,并且執行個體化兩個對象調用對應方法
public class test6{
public static void main(String[] args) {
Phone6 phone1 = new Phone6();
Phone6 phone2 = new Phone6();
new Thread(()->{
phone1.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone6{
//發短信
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
//打電話
public static synchronized void call(){
System.out.println("打電話");
}
}
結果還是先列印發短信,後列印打電話,因為static在類加載時加載了反射得到對象的class,雖然執行個體化兩個對象,但是用的都是Phone6.class,是以用的是同一把鎖,自然線程A調用發短信先調用執行,線程B必須等待線程A釋放鎖。這裡就可以和之前執行個體化兩個對象但沒有加static做對比,執行個體化兩個對象屬于兩把鎖,這裡由于是鎖class,是以還是用同一把鎖。
我們再舉出一個例子,下面是第7個例子,一個普通不加staic方法,一個加staic方法,列印結果是?
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//7、一個不加staic方法,一個加staic方法,列印結果是?
public class test7{
public static void main(String[] args) {
Phone7 phone = new Phone7();
new Thread(()->{
phone.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone7{
//發短信
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
//打電話
public synchronized void call(){
System.out.println("打電話");
}
}
相信大家都能知道結果了,沒錯,就是先打電話後發短信,為什麼呢?我相信基本都能說出來了吧,因為staic修飾鎖的是Class,沒有staic修飾鎖的是phone對象,這是兩把鎖,是以互不影響,不需要等待,是以先打電話,發短信sleep3秒才列印。
最後一個案例,大家直接猜猜結果
一個不加staic方法,一個加staic方法,兩個對象
package com.yx.lock8;
import java.util.concurrent.TimeUnit;
//8、一個普通不加staic方法,一個加staic方法,兩個對象,列印結果是?
public class test8{
public static void main(String[] args) {
Phone8 phone1 = new Phone8();
Phone8 phone2 = new Phone8();
new Thread(()->{
phone1.sendMsg();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone8{
//發短信
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
//打電話
public synchronized void call(){
System.out.println("打電話");
}
}
結果是先打電話,因為雖然是兩個對象,但一個是鎖Class,一個是phone2對象,是兩把鎖,是以不用等待,互不影響。讀到這裡相信你對鎖有了更為深刻的了解。
我們做個總結:看線程執行調用先後順序,核心是看多個線程是否用的是同一把鎖,同一把鎖就要等待,不能插隊;用多個鎖就不用等待,相當于有多個坑位。并且synchronized和staic修飾是不同的,synchronized鎖的對象是方法的調用者,也就是修飾的方法對應調用者,而static鎖的是Class,無論執行個體化多少對象,隻要他們的類.Class相同,那麼就是同一把鎖。