- 先看一段代碼
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
private int count = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this,TestActivity.class);
startActivity(intent);
}
});
for (int i=0;i < 10;i++){
Thread thread = new Thread("Thread_"+i){
@Override
public void run(){
for (int j=0;j < 1000;j++){
count = count+1;
Log.d(TAG,Thread.currentThread().getName()+":"+count);
}
}
};
thread.start();
}
}
}
我們預(yù)測下這段代碼的執(zhí)行結(jié)果,也就是count的最終值。有人可能會說是10000。但是實際結(jié)果是小于等于10000的一個數(shù)。原因是
count = count+1;
是一個非原子操作,至少包含三個語句
- 從內(nèi)存取出count的值
- 給count加1
- 寫回內(nèi)存
那極有可能存在這種情況
線程1從內(nèi)存中讀到count的值為1,此時線程2也讀到了1,然后2個線程都給count做自增操作并寫回內(nèi)存,此時內(nèi)存中count的值為2,并不是正確結(jié)果3。
- 那如何解決多線程并發(fā)導致不能獲得正確結(jié)果的問題呢?
java引入了鎖的機制,也就是關(guān)鍵字synchronized同步鎖,每個對象都有一把獨立的鎖,類對象只有一個鎖。只有獲得鎖,才能執(zhí)行被synchronized修飾的代碼塊或者方法。
- synchronized修飾代碼塊
我們把上面的代碼稍微修改下
for (int i=0;i < 10;i++){
final Thread thread = new Thread("Thread_"+i){
@Override
public void run(){
synchronized (MainActivity.this){
for (int j=0;j < 1000;j++){
count = count+1;
Log.d(TAG,Thread.currentThread().getName()+":"+count);
}
}
}
};
thread.start();
}
每個線程執(zhí)行的時候,都嘗試去獲取MainActivity對象的鎖,如果拿不到,就阻塞等。這就保證了同一時刻只有一個線程讀寫count這個變量,從而確保了結(jié)果的正確性,但是計算耗時增加了,效率變低。
修飾代碼塊很好理解,sychronized拿到的是實參對象的鎖。
- 修飾成員方法
看一段代碼
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
private int count = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Thread thread = new Thread("Thread_"+"test"){
@Override
public void run(){
try {
test();
}catch (Exception e){
e.printStackTrace();
}
}
};
thread.start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
synchronized (MainActivity.class){
Log.d(TAG,"我獲得了類鎖");
}
}catch (Exception e){
e.printStackTrace();
}
}
});
thread1.start();
}
public synchronized void test() throws Exception{
while (true){
Thread.sleep(1000);
}
}
}
我們用sychrionized修飾了increase這個方法。如果這個sychrionized拿到的是類鎖,那么
Log.d(TAG,"我拿到了類鎖");
代碼將永遠得不到執(zhí)行,但是實際情況是
01-02 18:03:49.760 14175-14203/com.debug.pluginhost D/MainActivity: 我獲得了類鎖
這行代碼得到了執(zhí)行,這說明synchronized修飾成員方法的時候,獲得是對象的鎖。
- 修飾靜態(tài)方法
修飾靜態(tài)方法我們就不舉例了,這種情況,synchronized獲得是類對象的鎖。
- wait(),notify()和notifyAll()
synchrinized拿到鎖后,如果需要,可以中途可以通過lockobj.wait()釋放鎖并阻塞在wait()處,等待其他線程通過lockobj.notify()或者lockobj.notifyAll()喚醒繼續(xù)執(zhí)行,一個典型的例子就是阻塞隊列。
看代碼
static class BlockQ{
ArrayList<Object> queue = new ArrayList<>();
int maxSize = 1;
public void push(Object o){
try {
synchronized (queue){
while (queue.size() >= maxSize){
queue.wait();
}
Log.d(TAG,"push :"+o);
queue.add(o);
queue.notify();
}
}catch (Exception e){
e.printStackTrace();
}
}
public Object pop(){
try {
synchronized (queue){
while (queue.size() == 0){
queue.wait();
}
Object o = queue.remove(0);
Log.d(TAG,"pop對象:"+o.toString());
queue.notify();
return o;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
我們想象有兩個集合CompetitionSet和WaitSet,我們簡稱為C和W,在C中的線程是有資格獲取對象鎖的,在W中的線程是沒資格的,但是如果有人通知它們或者它們當中一個,它們就可以進入C集合,參加競爭鎖。默認情況下,大家都在集合C,假設(shè)某個線程T獲得鎖,它開始執(zhí)行代碼,但是它發(fā)現(xiàn)自己無法處理當前的情況,因此只能等待,通過調(diào)用wait方法進入W集合,并釋放自己拿到的鎖
while (queue.size() == 0){ //我無法處理這種情況
queue.wait();//進入等待集合吧,并且釋放了自己獲得的鎖
}
當有線程T1獲得了鎖,并成功執(zhí)行push方法后,發(fā)出了notify()通知,此時JVM會從W中隨機選一個線程T進入C集合去競爭鎖,一旦T獲得鎖,它將從wait()處繼續(xù)執(zhí)行代碼。如果能把notify弄明白,那么notifyAll就不難了,從字面意思就能理解,notifyAll會把W集合里所有的等待線程都放入到C集合里去競爭鎖。
- 死鎖
大家仔細考慮下上面的代碼有什么問題?
我們假設(shè)有2個線程C1和C2調(diào)用pop方法,有1個線程P1調(diào)用push方法,假設(shè)執(zhí)行順序是這樣的
- C1執(zhí)行,發(fā)現(xiàn)queue里沒數(shù)據(jù),那么C1進入W集合
- C2執(zhí)行,發(fā)現(xiàn)queue里沒數(shù)據(jù),那么C2進入W集合
- P1執(zhí)行,push一個數(shù)據(jù)到隊列后,喚醒C1線程,此時C集合里有C1和P1,如果此時P1再次獲得對象鎖,那么P1進入W集合,此時C集合里只剩下C1,queue.size = 1
- C1獲得對象鎖,pop數(shù)據(jù)后,喚醒了C2,此時C1、C2在集合C中,queue.size = 0
- C1獲得對象鎖,因為queue.size = 0,因此C1進入W集合
- C2獲得對象鎖,因為queue.size = 0,因此C2進入W集合
- 程序陷入死鎖
思考notify和notifyAll的不同,在這種場景下只能用notifyAll。因為notifyAll會喚醒所有在W集合中的線程。