簡介
synchronized在JDK5.0的早期版本中是重量級鎖,效率很低,但從JDK6.0開始,JDK在關鍵字synchronized上做了大量的優化,如偏向鎖、輕量級鎖等,使它的效率有了很大的提升,
synchronized的作用是實作執行緒間的同步,當多個執行緒都需要訪問共享代碼區域時,對共享代碼區域進行加鎖,使得每一次只能有一個執行緒訪問共享代碼區域,從而保證執行緒間的安全性,
因為沒有顯式的加鎖和解鎖程序,所以稱之為隱式鎖,也叫作內置鎖、監視器鎖,
如下實體,在沒有使用synchronized的情況下,多個執行緒訪問共享代碼區域時,可能會出現與預想中不同的結果,
public class Apple implements Runnable {
private int appleCount = 5;
@Override
public void run() {
eatApple();
}
public void eatApple(){
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
public static void main(String[] args) {
Apple apple = new Apple();
Thread t1 = new Thread(apple, "小強");
Thread t2 = new Thread(apple, "小明");
Thread t3 = new Thread(apple, "小花");
Thread t4 = new Thread(apple, "小紅");
Thread t5 = new Thread(apple, "小黑");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
可能會輸出如下結果:
小強吃了一個蘋果,還剩3個蘋果
小黑吃了一個蘋果,還剩3個蘋果
小明吃了一個蘋果,還剩2個蘋果
小花吃了一個蘋果,還剩1個蘋果
小紅吃了一個蘋果,還剩0個蘋果
輸出結果例外的原因是eatApple方法里操作不是原子的,如當A執行緒完成appleCount的賦值,還沒有輸出,B執行緒獲取到appleCount的最新值,并完成賦值操作,然后A和B同時輸出,(A,B執行緒分別對應小黑、小強)
如果改下eatApple方法如下,還會不會有執行緒安全問題呢?
public void eatApple(){
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + --appleCount + "個蘋果");
}
還是會有的,因為--appleCount不是原子操作,--appleCount可以用另外一種寫法表示:appleCount = appleCount - 1,還是有可能會出現以上的例外輸出結果,
synchronized的使用
synchronized分為同步方法和同步代碼塊兩種用法,當每個執行緒訪問同步方法或同步代碼塊區域時,首先需要獲得物件的鎖,搶到鎖的執行緒可以繼續執行,搶不到鎖的執行緒則阻塞,等待搶到鎖的執行緒執行完成后釋放鎖,
1.同步代碼塊
鎖的物件是object:
public class Apple implements Runnable {
private int appleCount = 5;
private Object object = new Object();
@Override
public void run() {
eatApple();
}
public void eatApple(){
//同步代碼塊,此時鎖的物件是object
synchronized (object) {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
}
//...省略main方法
}
2.同步方法,修飾普通方法
鎖的物件是當前類的實體物件:
public class Apple implements Runnable {
private int appleCount = 5;
@Override
public void run() {
eatApple();
}
public synchronized void eatApple() {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
//...省略main方法
}
等價于以下同步代碼塊的寫法:
public void eatApple() {
synchronized (this) {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
}
3.同步方法,修飾靜態方法
鎖的物件是當前類的class物件:
public class Apple implements Runnable {
private static int appleCount = 5;
@Override
public void run() {
eatApple();
}
public synchronized static void eatApple() {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
//...省略main方法
}
等價于以下同步代碼塊的寫法:
public static void eatApple() {
synchronized (Apple.class) {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
}
4.同步方法和同步代碼塊的區別
a.同步方法鎖的物件是當前類的實體物件或者當前類的class物件,而同步代碼塊鎖的物件可以是任意物件,
b.同步方法是使用synchronized修飾方法,而同步代碼塊是使用synchronized修飾共享代碼區域,同步代碼塊相對于同步方法來說粒度更細,鎖的區域更小,一般鎖范圍越小效率就越高,如下情況顯然同步代碼塊更適用:
public static void eatApple() {
//不需要同步的耗時操作1
//...
synchronized (Apple.class) {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
//不需要同步的耗時操作2
//...
}
內置鎖的可重入性
內置鎖的可重入性是指當某個執行緒試圖獲取一個它已經持有的鎖時,它總是可以獲取成功,如下:
public static void eatApple() {
synchronized (Apple.class) {
synchronized (Apple.class) {
synchronized (Apple.class) {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
}
}
}
如果鎖不是可重入的,那么假如某執行緒持有了該鎖,然后又需要等待持有該鎖的執行緒釋放鎖,這不就造成死鎖了嗎?
synchronized可以被繼承嗎?
synchronized不可以被繼承,如果子類中重寫后的方法需要實作同步,則需要手動添加synchronized關鍵字,
public class AppleParent {
public synchronized void eatApple(){
}
}
public class Apple extends AppleParent implements Runnable {
private int appleCount = 5;
@Override
public void run() {
eatApple();
}
@Override
public void eatApple() {
appleCount--;
System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
}
//...省略main方法
}
基于內置鎖的等待和喚醒
基于內置鎖的等待和喚醒是使用Object類中的wait()和notify()或notifyAll()來實作的,這些方法的呼叫前提是已經持有對應的鎖,所以只能在同步方法或者同步代碼塊里呼叫,如果在沒有獲取到對應鎖的情況下呼叫則會拋出IllegalMonitorStateException例外,下面介紹下相關的幾個方法:
-
wait():使當前執行緒無限期地等待,直到另一個執行緒呼叫notify()或notifyAll(),
-
wait(long timeout):指定一個超時時間,超時時間過后執行緒將會被自動喚醒,執行緒也可以在超時時間之前被notify()或notifyAll()喚醒,注意,wait(0)等同于呼叫wait(),
-
wait(long timeout, int nanos):類似于wait(long timeout),主要區別是wait(long timeout, int nanos)提供了更高的精度,
-
notify():隨機喚醒一個在相同鎖物件上等待的執行緒,
-
notifyAll():喚醒所有在相同鎖物件上等待的執行緒,
一個簡單的等待喚醒實體:
public class Apple {
//蘋果數量
private int appleCount = 0;
/**
* 買蘋果
*/
public synchronized void getApple() {
try {
while (appleCount != 0) {
wait();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "買了5個蘋果");
appleCount = 5;
notify();
}
/**
* 吃蘋果
*/
public synchronized void eatApple() {
try {
while (appleCount == 0) {
wait();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "吃了1個蘋果");
appleCount--;
notify();
}
}
/**
* 生產者,買蘋果
*/
public class Producer extends Thread{
private Apple apple;
public Producer(Apple apple, String name){
super(name);
this.apple = apple;
}
@Override
public void run(){
while (true)
apple.getApple();
}
}
/**
* 消費者,吃蘋果
*/
public class Consumer extends Thread{
private Apple apple;
public Consumer(Apple apple, String name){
super(name);
this.apple = apple;
}
@Override
public void run(){
while (true)
apple.eatApple();
}
}
public class Demo {
public static void main(String[] args) {
Apple apple = new Apple();
Producer producer = new Producer(apple,"小明");
Consumer consumer = new Consumer(apple, "小紅");
producer.start();
consumer.start();
}
}
輸出結果:
小明買了5個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小明買了5個蘋果
小紅吃了1個蘋果
......
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/265803.html
標籤:Java
