文章目錄
- 前言
- 通過一系列的例子,瞭解synchronized 使用
- 總結
前言
上一篇瞭解了synchronized,但是呢光懂理論沒用,關鍵是要會用,用demo的形式寫一下各種使用場景,這麼一來,就會對synchronized的使用更加透徹。
通過一系列的例子,瞭解synchronized 使用
1、synchronized 都會在哪些地方使用?
修飾一個代碼塊,作用的對象是調用這個代碼塊的對象。
修飾一個方法,作用的對象是調用這個方法的對象。
修飾一個靜態方法,作用是這個類的所有對象。
修飾一個類,作用的是這個類的所有對象
2、怎麼使用同步代碼塊?
兩個線程訪問同一個對象代碼塊,只有一個線程執行,另外一個線程被阻塞。
比如:
class MyRunnable implements Runnable{
private static int count;//定義一個變量count;
public MyRunnable(){
count = 0;
}
@Override public void run() {
synchronized (this){//同步代碼塊,鎖住的是MyRunnable這個實例對象
for (int i = 0; i < 5; i++){
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount(){
return count;
}
}
輸出的日誌如下
2022-12-31 14:36:03.266 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 14:36:03.366 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:1
2022-12-31 14:36:03.467 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 14:36:03.567 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 14:36:03.667 8015-8053/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 14:36:03.768 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 14:36:03.868 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:6
2022-12-31 14:36:03.968 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:7
2022-12-31 14:36:04.068 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:8
2022-12-31 14:36:04.169 8015-8054/com.ssz.mvvmdemo I/System.out: 線程2:9
可以看到先執行線程1,再執行線程2,達到了同步目的。
2.1 這個時候,我們將執行的對象換一下
比如:
MyRunnable myRunnable = new MyRunnable();
MyRunnable myRunnable2 = new MyRunnable();
Thread thread1 = new Thread(myRunnable, "線程1");
Thread thread2 = new Thread(myRunnable2, "線程2");
thread1.start();
thread2.start();
執行結果:
2022-12-31 14:42:33.957 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 14:42:33.957 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:1
2022-12-31 14:42:34.057 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 14:42:34.057 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:2
2022-12-31 14:42:34.157 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 14:42:34.157 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:3
2022-12-31 14:42:34.257 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 14:42:34.257 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:4
2022-12-31 14:42:34.358 8237-8274/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 14:42:34.358 8237-8273/com.ssz.mvvmdemo I/System.out: 線程1:5
可以看到線程1和線程2,變成隨機的執行代碼塊,爲什麼呢?
因爲他們作用的不是同一個對象,線程1 執行的是 myRunnable對象,
而線程2 執行的是 myRunnable2對象。所以他們互不干擾,兩個線程就能同時執行。
2.2 當一個線程訪問一個對象的 synchronized(this) 代碼塊的時候,另外一個線程還是可以訪問, 該對象的沒有被synchronized(this) 修飾的代碼塊。
比如:
class Counter implements Runnable{
private int count;
public Counter(){
count = 0;
}
/**
* 對count 執行自增操作
* */
public void countAdd(){
synchronized (this){
for (int i = 0; i < 5; i++){
try {
System.out.println(Thread.currentThread().getName()+ ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//沒有對count 執行自增操作,只是打印
public void printCount(){
for(int i = 0; i < 5; i++){
try {
System.out.println(Thread.currentThread().getName() + " count:" + count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override public void run() {
String threadName = Thread.currentThread().getName();
if ("線程A".equals(threadName)){//線程A 要執行自增操作
countAdd();
}else if("線程B".equals(threadName)){
printCount();
}
}
}
使用:
Counter counter = new Counter();
Thread threadA = new Thread(counter, "線程A");
Thread threadB = new Thread(counter, "線程B");
threadA.start();
threadB.start();
結果:
2022-12-31 15:03:52.249 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:0
2022-12-31 15:03:52.249 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B count:1
2022-12-31 15:03:52.349 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B count:1
2022-12-31 15:03:52.349 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:1
2022-12-31 15:03:52.449 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:2
2022-12-31 15:03:52.449 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B count:2
2022-12-31 15:03:52.549 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:3
2022-12-31 15:03:52.549 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B count:3
2022-12-31 15:03:52.650 9366-9403/com.ssz.mvvmdemo I/System.out: 線程A:4
2022-12-31 15:03:52.650 9366-9404/com.ssz.mvvmdemo I/System.out: 線程B count:5
可以看到線程A 和 線程B 他們是互不影響的,線程B還是隨機執行的。
也就是說一個線程 在執行同一個對象的 synchronized(this)的代碼塊時候,
另外一個線程可以執行該對象的沒有被synchronized(this)修飾的代碼塊,不會阻塞。
3、怎麼給一個對象加鎖呢?
class Account {
String name;
float amount;
public Account(String name, float amount){
this.name = name;
this.amount = amount;
}
/**
* 存錢
* */
public void save(float money){
amount += money;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 取錢
* */
public void out(float money){
amount -= money;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public float getAmount(){
return amount;
}
}
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account){
this.account = account;
}
@Override public void run() {
synchronized (account){//對這個對象進行加鎖操作
account.save(500);//存錢500;
account.out(500);//取錢500;
System.out.println(Thread.currentThread().getName() + ":" + account.amount);
}
}
}
使用:
Account account = new Account("ssz", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);
final int THREAD_NUM = 10;
//創建5個線程去隨機執行。
Thread[] threads = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++){
threads[i] = new Thread(accountOperator, "Thread" + i);
threads[i].start();
}
結果:
2022-12-31 15:31:09.675 11884-11927/com.ssz.mvvmdemo I/System.out: Thread0:10000.0
2022-12-31 15:31:09.875 11884-11928/com.ssz.mvvmdemo I/System.out: Thread1:10000.0
2022-12-31 15:31:10.076 11884-11930/com.ssz.mvvmdemo I/System.out: Thread2:10000.0
2022-12-31 15:31:10.276 11884-11931/com.ssz.mvvmdemo I/System.out: Thread3:10000.0
2022-12-31 15:31:10.477 11884-11934/com.ssz.mvvmdemo I/System.out: Thread5:10000.0
2022-12-31 15:31:10.677 11884-11932/com.ssz.mvvmdemo I/System.out: Thread4:10000.0
2022-12-31 15:31:10.878 11884-11936/com.ssz.mvvmdemo I/System.out: Thread7:10000.0
2022-12-31 15:31:11.078 11884-11937/com.ssz.mvvmdemo I/System.out: Thread8:10000.0
2022-12-31 15:31:11.279 11884-11938/com.ssz.mvvmdemo I/System.out: Thread9:10000.0
2022-12-31 15:31:11.480 11884-11935/com.ssz.mvvmdemo I/System.out: Thread6:10000.0
可以看到線程是隨機的執行,但是呢,對於存錢取錢的操作仍然是不受影響的,因爲這塊是進行了同步存取操作,是對Account進行了加鎖操作,保證了不受其他線程的干擾。
3.1 假如沒有明確的對象作爲鎖,只想同步一段代碼塊怎麼辦呢?
class Test implements Runnable{
private byte[] lock = new byte[0]//一個特殊的實例對象
public void test(){
synchronized(lock){
//todo 要同步的代碼
}
}
public void run(){
}
}
爲什麼使用byte[] 作爲對象呢?
因爲byte 數組創建起來比任何對象都經濟。
跟Object object = new Object() 比起來的話,查看編譯後的字節碼發現。
byte 數組 只需要3行操作碼,而 object 需要7行操作碼
3.2 synchronized 在修飾方法的時候能不能繼承呢?
synchronized 是不能繼承的,也就是如果子類要同步,
要嘛調用父類的同步方法,要嘛就是在方法前,添加一個synchronized關鍵字。
比如:
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }//調用父類的同步方法
}
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { } //添加關鍵字
}
4、怎麼修飾一個靜態方法呢?
當我們在修飾靜態方法的時候,因爲靜態方法是屬於類的,所以鎖住的是類的所有對象。
比如:
class NewRunnable implements Runnable{
private static int count;
public NewRunnable(){
count = 0;
}
public synchronized static void method(){ //修飾的是一個靜態方法
for (int i = 0; i < 5; i++){
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override public void run() {
method();
}
}
使用:
NewRunnable newRunnable = new NewRunnable();
NewRunnable newRunnable2 = new NewRunnable();
Thread thread1 = new Thread(newRunnable, "線程1"); //newRunnable
Thread thread2 = new Thread(newRunnable2, "線程2");//newRunnable2
thread1.start();
thread2.start();
結果:
2022-12-31 17:02:21.483 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 17:02:21.583 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:1
2022-12-31 17:02:21.683 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 17:02:21.783 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 17:02:21.884 20792-20824/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 17:02:21.984 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 17:02:22.084 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:6
2022-12-31 17:02:22.184 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:7
2022-12-31 17:02:22.285 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:8
2022-12-31 17:02:22.385 20792-20825/com.ssz.mvvmdemo I/System.out: 線程2:9
可以看到,雖然是不同的對象,但是呢,卻還是按順序執行了。最主要是同步的是靜態方法,而靜態方法是屬於類的,這相當於鎖住了這個類,所以,就能阻止其他線程調用,只能等待第一個線程執行完,再執行第二個線程。
4.1 怎麼修飾一個類呢?
我們對上面修飾靜態方法做個改造,就是把synchronized 放到方法裏頭,然後使用 synchronized (NewRunnable.class)
比如:
class NewRunnable implements Runnable{
private static int count;
public NewRunnable(){
count = 0;
}
public void method(){ //不是靜態方法,就是普通方法
synchronized (NewRunnable.class){ //鎖的作用對象是類
for (int i = 0; i < 5; i++){
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override public void run() {
method();
}
}
結果情況1:
2022-12-31 17:10:52.476 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:0
2022-12-31 17:10:52.577 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:1
2022-12-31 17:10:52.677 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:2
2022-12-31 17:10:52.778 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:3
2022-12-31 17:10:52.878 22805-22840/com.ssz.mvvmdemo I/System.out: 線程2:4
2022-12-31 17:10:52.978 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:5
2022-12-31 17:10:53.078 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:6
2022-12-31 17:10:53.179 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:7
2022-12-31 17:10:53.279 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:8
2022-12-31 17:10:53.379 22805-22839/com.ssz.mvvmdemo I/System.out: 線程1:9
結果情況2:
2022-12-31 17:12:17.787 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:0
2022-12-31 17:12:17.887 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:1
2022-12-31 17:12:17.987 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:2
2022-12-31 17:12:18.087 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:3
2022-12-31 17:12:18.187 23462-23496/com.ssz.mvvmdemo I/System.out: 線程1:4
2022-12-31 17:12:18.288 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:5
2022-12-31 17:12:18.388 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:6
2022-12-31 17:12:18.489 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:7
2022-12-31 17:12:18.589 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:8
2022-12-31 17:12:18.689 23462-23497/com.ssz.mvvmdemo I/System.out: 線程2:9
我們看到可能先執行的線程1,也可能先執行的線程2,但是不管是誰開始執行,只要誰先開始,另外一個線程就得等着。
所以,這就保證了同步。
這裏是給這個類加鎖,所以他們都是共用1把鎖,只有一方把鎖釋放,另外一方纔能執行。
5 使用synchronized 有哪些注意點呢?
1、定義接口的時候,不能使用synchronized 關鍵字。
2、構造方法不能使用synchronized 關鍵字,但可以使用synchronized來同步代碼塊。
總結
總的來講:
1、synchronized 關鍵字用在非靜態的方法上,或者對象上,所獲得的鎖是針對對象的;
如果是一個靜態方法,或者一個類,那麼鎖是針對類的所有對象的。
2、每個對象只有一把鎖與之關聯,需要等一方釋放,另一方纔能執行。
3、使用同步是需要系統很大開銷的,所以非必要情況下不要進行同步鎖操作。