
一句話總結synchronized:
JVM會自動通過使用monitor來加鎖和解鎖,保證了同時只有一個線程可以執行指定代碼,從而保證了線程安全,同時具有可重入和不可中斷的性質。
一.synchronized的作用
使用synchronized修飾方法或者代碼塊時,它能夠保證在同一時刻最多只有一個線程執行該段代碼,以達到保證并發安全的效果。
二.synchronized的地位
1.Synchronized是Java的關鍵字,被Java語言原生支持
2.是最基本的互斥同步手段
3.是并發編程中的元老級角色,是并發編程的主要內容
三.synchronized的性質
synchronized的具有可重入性和不可中斷性
1.可重入性(synchronized區別于其他鎖的一個很重要的性質)
什么是可重入:指的是同一線程的外層函數獲得鎖之后,內層函數可以直接再次獲取該鎖。也叫做遞歸鎖。Java中兩大遞歸鎖:Synchronized和ReentrantLock。
鎖的不可重入性:線程拿到一把鎖了,如果想再次使用這把鎖,必須先將鎖釋放,與其他線程再次進行競爭。
鎖的可重入性:如果線程已經拿到了鎖,試圖去請求這個已經獲得到的鎖,而無需提前釋放,直接就可以使用手里的這把鎖,這就叫做可重入性
synchronized是可重入鎖
可重入鎖的好處:
1 避免死鎖
2 提升封裝性
粒度:
可重入的特性是線程級別的,不是調用級別的。
情況1:證明同一個方法是可重入的
實例代碼:
public class SynchronizedRecursion {
int a = 0;
public static void main(String[] args) {
SynchronizedRecursion sr = new SynchronizedRecursion();
sr.method1();
}
private synchronized void method1(){
System.out.println(synchronized修飾的方法,a = + a);
if(a==0){
a++;
method1();
}
}
}
情況2:證明可重入不要求是同一個方法
實例代碼:
public class SynchronizedOtherMethod {
public synchronized void method1(){
System.out.println(我是method1);
method2();
}
public synchronized void method2(){
System.out.println(我是method2);
}
public static void main(String[] args) {
SynchronizedOtherMethod som = new SynchronizedOtherMethod();
som.method1();
}
}
情況3:證明可重入不要求是同一個類中的
實例代碼:
public class SynchronizedSuperClass {
public synchronized void doSomething(){
System.out.println(我是父類方法);
}
}
class TestClass extends SynchronizedSuperClass{
public synchronized void doSomething(){
System.out.println(我是子類方法);
super.doSomething();
}
public static void main(String[] args) {
TestClass tc = new TestClass();
tc.doSomething();
}
}
2.不可中斷(相比于其他有的鎖可以中斷,這個性質是synchronized的一個劣勢所在)
一旦這個鎖已經被別的線程獲得了,如果當前線程還想獲得,只能選擇等待或者阻塞,直到別的線程釋放這個鎖。如果別的線程 永遠不釋放鎖,那么線程只能永遠地等下去。
相比之下,Lock類,可以擁有中斷的能力。
第一點,如果我覺得我等的時候太長了,有權中斷現在已經獲取到鎖的線程的執行; 第二點,如果我覺得我等待的時間太長了不想再等了,也可以退出。
Lock比synchronized靈活很多,但是編碼易出錯。
四.synchronized的原理
1.加解鎖原理:現象,時機,深入JVM看字節碼
現象:每一個類的實例對應一把鎖,而每一個synchronized方法都必須先獲得調用該方法的類的實例的鎖方能執行,否則線程會阻塞。而方法一旦執行,它就獨占了這把鎖,直到該方法返回或者是拋出異常,才將鎖釋放。一旦鎖釋放之后,之前被阻塞的線程才能獲得這把鎖,從被阻塞的狀態重新進入到可執行的狀態。當一個對象中有synchronized修飾的方法或者代碼塊的時候,要想執行這段代碼,就必須先獲得這個對象鎖,如果此對象的對象鎖已經被其他調用者所占用,就必須等待它被釋放。所有的Java對象都含有一個互斥鎖,這個鎖由JVM自動去獲取和釋放,我們只需要指定這個對象就行了,至于鎖的釋放和獲取不需要我們操心。
獲取和釋放鎖的時機:內置鎖
我們知道每一個Java對象都可以用作一個同步的鎖,這個鎖叫做內部鎖,或者叫做監視器鎖--monitor lock。線程在進入到同步代碼塊之前,會自動獲得這個鎖,并且在退出同步代碼塊的時候會自動的釋放這個鎖,無論是正常途徑退出還是拋出異常退出。獲得這個內置鎖的唯一途徑就是進入這個鎖保護的同步代碼塊或者同步方法中。
實例代碼:
public class SynchronizedToLock {
Lock lock = new ReentrantLock();
public synchronized void method1(){
System.out.println(我是synchronized鎖);
}
public void method2(){
lock.lock(); //加鎖
try{
System.out.println(我是lock鎖);
}finally {
lock.unlock(); //釋放鎖
}
}
public static void main(String[] args) {
SynchronizedToLock stl = new SynchronizedToLock();
stl.method1();
stl.method2();
}
}
method1()和method2()方法等價
深入JVM看字節碼:
實例代碼:
public class Decompilation {
private Object object = new Object();
//同步代碼塊
public void insert(Thread thread){
synchronized (object){
}
}
}
1、將上面的Java代碼編譯為 .class文件,命令窗口執行命令:javac xxx.java 進行編譯。
2、通過反編譯查看字節碼,執行命令:javap -verbose xxx.class 進行反編譯
3、synchronized如何實現的,有個加鎖monitorenter和解鎖monitorexit
讀到該指令,會讓monitor計數器+1或-1
具體如圖:


線程既可以在方法完成之后退出,也可以在拋出異常后退出,因此monitorexit數量多于monitorenter。
monitorenter和monitorexit指令
monitorenter:線程每次進入時,計數器+1。如果重入,繼續加
monitorexit:線程退出時,計數器-1.變為0時候,其他線程可以獲取鎖。
2.可重入原理:加鎖次數計數器
1、JVM負責跟蹤對象被加鎖的次數;
2、有個monitor計數器,線程第一次給對象加鎖的時候,計數變為1.每當這個相同的線程在此對象上再次獲得鎖時,計數會遞增。
3、任務結束離開,則會執行monitorexit,計數遞減,直到完全釋放
3.可見性原理:Java內存模型

五.synchronized的缺陷
1.效率低:
· 鎖的釋放情況少
· 試圖獲得鎖時不能設定超時
· 不能中斷一個正在試圖獲得鎖的線程
2.不夠靈活(讀寫鎖更靈活:讀操作的時候不會加鎖,寫操作的時候才會加鎖):
· 加鎖和釋放的時機單一
· 每個鎖僅有單一的條件(某個對象),可能是不夠的
3.無法知道是否成功獲取到鎖,而使用Lock則不同。
Lock可以,如果嘗試成功了做一些邏輯判斷,如果沒有成功做另外一些邏輯判斷.
Lock類:
lock.lock();lock.unlock();通過這兩個方法,可以手動加鎖和解鎖。
lock.tryLock();lock.tryLock(10, TimeUnit.MINUTES);可以判斷是否加鎖,以及設置超時時間,返回類型為boolean
六.synchronized的用法
1、對象鎖:
a、方法鎖(默認鎖對象為this當前實例對象):synchronized修飾普通方法,鎖對象默認為this
實例代碼:
public synchronized void run(){
for(int j = 0; j 100000; j++){
i++;
}
}
b、同步代碼塊鎖: 自己手動指定鎖對象
實例代碼:
public synchronized void run(){
synchronized (this){
for(int j = 0; j 100000; j++){
i++;
}
}
}
2、類鎖:
Java類可能有很多個對象,但只要一個class對象。類鎖只不過是Class對象的鎖而已。類鎖只能在同一時刻被一個對象擁有。
a、使用關鍵字synchronized修飾靜態static的方法
實例代碼:
public void run(){
method();
}
public static synchronized void method(){
for(int j = 0; j 100000; j++){
i++;
}
}
b、使用關鍵字synchronized指定鎖為Class對象: synchronized(*.class)
實例代碼:
public synchronized void run(){
synchronized (DisappearRequest2.class){
for(int j = 0; j 100000; j++){
i++;
}
}
}
synchronized使用的注意點:
鎖的信息是保存在對象頭中的、作用域不易過大,影響性能、避免死鎖
七.不使用并發情況會帶來的后果以及解決辦法
問題:兩個線程同時a++,最后結果會比預計的少
原因
count++,它看上去只是一個操作,實際上包含了三個動作:
1.讀取count
2.將count加一
3.將count的值寫入到內存中
這三個操作,如果不按照原子去執行,就會帶來并發問題
解決之前:
public class DisappearRequest1 implements Runnable{
static DisappearRequest1 instance = new DisappearRequest1();
static int i = 0;
@Override
public void run(){
for(int j = 0; j 100000; j++){
i++;
}
}
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= + i);
}
}
解決:
public class DisappearRequest2 implements Runnable{
static DisappearRequest2 instance = new DisappearRequest2();
static int i = 0;
//方法1:直接把synchronized加到方法上
/* public synchronized void run(){
for(int j = 0; j 100000; j++){
i++;
}
}*/
//方法2:對象鎖的同步代碼塊
/*public synchronized void run(){
synchronized (this){
for(int j = 0; j 100000; j++){
i++;
}
}
}*/
//第三種:類鎖:synchronized(*.class)
public synchronized void run(){
synchronized (DisappearRequest2.class){
for(int j = 0; j 100000; j++){
i++;
}
}
}
//第四種:類鎖
/*public void run(){
method();
}
public static synchronized void method(){
for(int j = 0; j 100000; j++){
i++;
}
}*/
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= + i);
}
}
八.關于多線程訪問同步方法的七種情況的總結
1.兩個線程同時訪問一個對象的同步方法(對象鎖):串行執行
這種情況就是對象鎖的方法鎖情況。會相互等待,只能有一個線程持有鎖。
2.兩個線程訪問的是兩個對象的同步方法:兩個鎖對象互不干擾,并行執行
不會加鎖,因為訪問的是不同的實例
3.兩個線程訪問的是synchronized的靜態方法:兩個鎖對象是同一個鎖,串行執行
這種情況就是類鎖的靜態方法鎖。
4.同時訪問同步方法與非同步方法:同步(并行)執行
synchronized關鍵字只作用于當前方法,不會影響其他未加關鍵字的方法的并發行為。因此非同步方法不受到影響,還是會并發執行。
5.訪問同一個對象的不同的普通同步方法(對象鎖):串行執行
synchronized關鍵字雖然沒有指定所要的那個鎖對象,但是本質上是指定了this這個對象作為它的鎖。所以對于同一個實例來講,兩個方法拿到的是同一把鎖,因此會出現串行的情況。
6.同時訪問靜態synchronized和非靜態synchronized方法:同步(并行)執行
前者為類鎖,鎖為Class類;后者為對象鎖,鎖為this對象。因此兩者的鎖不同,會并行執行。