線程安全概念
當(dāng)多個線程訪問某一個類(對象或方法)時,這個對象始終都能表現(xiàn)出正確的行為,那么這個類(對象或方法)就是線程安全的。
synchronized:可以在任意對象及方法上加鎖,而加鎖的這段代碼稱為"互斥區(qū)"或"臨界區(qū)"
方法鎖
public class MyThread extends Thread{
private int count = 5 ;
//synchronized加鎖
public synchronized void run(){
count--;
System.out.println(this.currentThread().getName() + " count = "+ count);
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
Thread t3 = new Thread(myThread,"t3");
Thread t4 = new Thread(myThread,"t4");
Thread t5 = new Thread(myThread,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
當(dāng)多個線程訪問
myThread
的run
方法時,以排隊的方式進行處理(這里排隊是按照 CPU 分配的先后順序而定的),一個線程想要執(zhí)行synchronized
修飾的方法里的代碼需要做兩種操作:
1、嘗試獲得鎖
2、如果拿到鎖,執(zhí)行synchronized
代碼體內(nèi)容;拿不到鎖,這個線程就會不斷的嘗試獲得這把鎖,直到拿到為止,而且是多個線程同時去競爭這把鎖。(也就是會有鎖競爭的問題)
對象鎖
public class MultiThread {
private static int num = 0;
/** static */
public static synchronized void printNum(String tag){
try {
if(tag.equals("a")){
num = 100;
System.out.println("tag a, set num over!");
Thread.sleep(1000);
} else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//注意觀察run方法輸出順序
public static void main(String[] args) {
//倆個不同的對象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}
輸出結(jié)果:
方法printNum()
未加static
后的輸出結(jié)果:
tag b, set num over!
tag a, set num over!
tag b, num = 200
tag a, num = 100
在方法printNum()
加上static
后的輸出結(jié)果:
tag a, set num over!
tag a, num = 100
tag b, set num over!
tag b, num = 200
1、在本例中關(guān)鍵字
synchronized
取得的鎖都是對象鎖,而不是把一段代碼(方法)當(dāng)做鎖,所以代碼中哪個線程先執(zhí)行synchronized
關(guān)鍵字的方法,哪個線程就持有該方法所屬對象的鎖(Lock
)
2、在靜態(tài)方法上加synchronized
關(guān)鍵字,表示鎖定.class
類,類一級別的鎖(獨占.class
類)。
對象鎖異步同步的問題
public class MyObject {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** synchronized */
public void method2(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
final MyObject mo = new MyObject();
/**
* 分析:
* t1線程先持有object對象的Lock鎖,t2線程可以以異步的方式調(diào)用對象中的非synchronized修飾的方法
* t1線程先持有object對象的Lock鎖,t2線程如果在這個時候調(diào)用對象中的同步(synchronized)方法則需等待,也就是同步
*/
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mo.method1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
mo.method2();
}
},"t2");
t1.start();
t2.start();
}
}
輸出結(jié)果:
method2()
未加synchronized
關(guān)鍵字:
t2
t1
method2()
加上synchronized
關(guān)鍵字:
t1
t2
在本例中
1、method2()方法未加
synchronized關(guān)鍵字, t1 線程先持有
object對象的
Lock鎖,t2 線程可以以異步的方式調(diào)用對象中的非
synchronized修飾的方法 2、如果在
method2()方法上加上synchronized
關(guān)鍵字此時,t1 線程先持有
object對象的
Lock`鎖,t2 線程如果在這個時候調(diào)用對象中的同步(synchronized)方法則需等待,也就是同步
臟讀、原子性問題
在某些情況下業(yè)務(wù)整體需要使用完整的synchronized
,保持業(yè)務(wù)的原子性。
public class DirtyRead {
private String username = "loofer";
private String password = "123";
public synchronized void setValue(String username, String password){
this.username = username;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最終結(jié)果:username = " + username + " , password = " + password);
}
public void getValue(){
System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
}
public static void main(String[] args) throws Exception{
final DirtyRead dr = new DirtyRead();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
dr.setValue("z3", "456");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
}
輸出結(jié)果:
getValue方法得到:username = z3 , password = 123
setValue最終結(jié)果:username = z3 , password = 456
由于使用了
sleep()
導(dǎo)致getValue()
方法先執(zhí)行,原本我們期望的應(yīng)該是setValue()
方法先執(zhí)行,并且 password 應(yīng)該都為 456 才對
synchronized的重入
例子1
public class SyncDubbo1 {
public synchronized void method1(){
System.out.println("method1..");
method2();
}
public synchronized void method2(){
System.out.println("method2..");
method3();
}
public synchronized void method3(){
System.out.println("method3..");
}
public static void main(String[] args) {
final SyncDubbo1 sd = new SyncDubbo1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sd.method1();
}
});
t1.start();
}
}
例子2
public class SyncDubbo2 {
static class Main {
public int i = 10;
public synchronized void operationSup(){
try {
i--;
System.out.println("Main print i = " + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Sub extends Main {
public synchronized void operationSub(){
try {
while(i > 0) {
i--;
System.out.println("Sub print i = " + i);
Thread.sleep(100);
this.operationSup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Sub sub = new Sub();
sub.operationSub();
}
});
t1.start();
}
}
輸出結(jié)果:
例子 1:
method1..
method2..
method3..
例子2:
Sub print i = 9
Main print i = 8
Sub print i = 7
Main print i = 6
Sub print i = 5
Main print i = 4
Sub print i = 3
Main print i = 2
Sub print i = 1
Main print i = 0
synchronized 異常
public class SyncException {
private int i = 0;
public synchronized void operation(){
while(true){
try {
i++;
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " , i = " + i);
if(i == 20){
//Integer.parseInt("a");
throw new RuntimeException();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SyncException se = new SyncException();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
se.operation();
}
},"t1");
t1.start();
}
}
輸出結(jié)果
t1 , i = 1
t1 , i = 2
t1 , i = 3
t1 , i = 4
t1 , i = 5
t1 , i = 6
t1 , i = 7
t1 , i = 8
t1 , i = 9
Exception in thread "t1" java.lang.RuntimeException
at com.loofer.base.sync005.SyncException.operation(SyncException.java:18)
at com.loofer.base.sync005.SyncException$1.run(SyncException.java:32)
at java.lang.Thread.run(Thread.java:745)
t1 , i = 10
這里我們除了 main 線程外還有一個 t1 線程,當(dāng) t1 中拋出一個異常時,由于我們在
operation()
方法上加了鎖,所以我們整個應(yīng)用程序最后退出了,對于這種問題一般情況下可以采取 continue 寫入日志發(fā)布告警,讓應(yīng)用程序繼續(xù)執(zhí)行。
線程細節(jié)問題
使用 synchronized 聲明的方法在某些情況下是有弊端的,比如 A 線程調(diào)用同步的方法執(zhí)行一個很長時間的任務(wù),那么 B 線程必須等待比較長的時間才能執(zhí)行,這樣的情況下可以使用 synchronized去優(yōu)化代碼執(zhí)行時間,也就是通常所說的減小鎖的粒度。
鎖對象的改變的問題
鎖對象的改變問題,當(dāng)使用一個對象進行加鎖的時候,要注意對象本身發(fā)生改變的時候,那么持有的鎖就不同。如果對象本身不發(fā)生改變,那么依然是同步的,即使是對象的屬性發(fā)生了改變。
public class ChangeLock {
private String lock = "lock";
private void method(){
synchronized (lock) {
try {
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + "開始");
lock = "change lock";
Thread.sleep(2000);
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + "結(jié)束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ChangeLock changeLock = new ChangeLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
},"t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
輸出結(jié)果
當(dāng)前線程 : t1開始
當(dāng)前線程 : t2開始
當(dāng)前線程 : t1結(jié)束
當(dāng)前線程 : t2結(jié)束
在本例中由于 lock 成員變量在線程執(zhí)行的過程中被改變了,導(dǎo)致鎖失效了
死鎖問題
在設(shè)計程序時就應(yīng)該避免雙方相互持有對方的鎖的情況
public class DeadLock implements Runnable{
private String tag;
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public void setTag(String tag){
this.tag = tag;
}
@Override
public void run() {
if(tag.equals("a")){
synchronized (lock1) {
try {
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + " 進入lock1執(zhí)行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + " 進入lock2執(zhí)行");
}
}
}
if(tag.equals("b")){
synchronized (lock2) {
try {
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + " 進入lock2執(zhí)行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + " 進入lock1執(zhí)行");
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
d1.setTag("a");
DeadLock d2 = new DeadLock();
d2.setTag("b");
Thread t1 = new Thread(d1, "t1");
Thread t2 = new Thread(d2, "t2");
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
輸出結(jié)果
當(dāng)前線程 : t1 進入lock1執(zhí)行
當(dāng)前線程 : t2 進入lock2執(zhí)行
在程序運行的運行時啟動了兩個線程,每個線程中都持有 lock1、lock2 兩個鎖,當(dāng) t1 運行時持有了 lock1 當(dāng) t2 運行的時候 lock2 被持有了,當(dāng) t1 運行到后面去嘗試獲得 lock2 就會一直等待,而 t2 繼續(xù)運行嘗試去獲得 t1 發(fā)現(xiàn)被占用了也進入等待,這時就進入了互相等待的狀態(tài)。也就是我們所說的死鎖。
同一對象屬性的修改不會影響鎖的情況
public class ModifyLock {
private String name ;
private int age ;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public synchronized void changeAttributte(String name, int age) {
try {
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + " 開始");
this.setName(name);
this.setAge(age);
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + " 修改對象內(nèi)容為: "
+ this.getName() + ", " + this.getAge());
Thread.sleep(2000);
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + " 結(jié)束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final ModifyLock modifyLock = new ModifyLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("張三", 20);
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
modifyLock.changeAttributte("李四", 21);
}
},"t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
輸出結(jié)果
當(dāng)前線程 : t1 開始
當(dāng)前線程 : t1 修改對象內(nèi)容為: 張三, 20
當(dāng)前線程 : t1 結(jié)束
當(dāng)前線程 : t2 開始
當(dāng)前線程 : t2 修改對象內(nèi)容為: 李四, 21
當(dāng)前線程 : t2 結(jié)束
使用synchronized代碼塊減小鎖的粒度,提高性能
public class Optimize {
public void doLongTimeTask(){
try {
System.out.println("當(dāng)前線程開始:" + Thread.currentThread().getName() +
", 正在執(zhí)行一個較長時間的業(yè)務(wù)操作,其內(nèi)容不需要同步");
Thread.sleep(2000);
synchronized(this){
System.out.println("當(dāng)前線程:" + Thread.currentThread().getName() +
", 執(zhí)行同步代碼塊,對其同步變量進行操作");
Thread.sleep(1000);
}
System.out.println("當(dāng)前線程結(jié)束:" + Thread.currentThread().getName() +
", 執(zhí)行完畢");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final Optimize otz = new Optimize();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
},"t2");
t1.start();
t2.start();
}
}
輸出結(jié)果
當(dāng)前線程開始:t1, 正在執(zhí)行一個較長時間的業(yè)務(wù)操作,其內(nèi)容不需要同步
當(dāng)前線程開始:t2, 正在執(zhí)行一個較長時間的業(yè)務(wù)操作,其內(nèi)容不需要同步
當(dāng)前線程:t1, 執(zhí)行同步代碼塊,對其同步變量進行操作
當(dāng)前線程:t2, 執(zhí)行同步代碼塊,對其同步變量進行操作
當(dāng)前線程結(jié)束:t1, 執(zhí)行完畢
當(dāng)前線程結(jié)束:t2, 執(zhí)行完畢
字符串加鎖
public class StringLock {
public void method() {
//new String("字符串常量")
synchronized (new String("字符串常量")) {
try {
while(true){
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + "開始");
Thread.sleep(1000);
System.out.println("當(dāng)前線程 : " + Thread.currentThread().getName() + "結(jié)束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final StringLock stringLock = new StringLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t2");
t1.start();
t2.start();
}
}
輸出結(jié)果
當(dāng)前線程 : t1開始
當(dāng)前線程 : t1結(jié)束
當(dāng)前線程 : t1開始
當(dāng)前線程 : t1結(jié)束
...
t1 永遠持有鎖,t2 永遠進不去
synchronized 代碼塊對字符串的鎖,注意String
常量池的緩存功能