Java synchronized 关键字

  1. 先看一段代码
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();
        }
    }
}

我们预测下这段代码的执行结果,也就是count的最终值。有人可能会说是10000。但是实际结果是小于等于10000的一个数。原因是

count = count+1;

是一个非原子操作,至少包含三个语句

  • 从内存取出count的值
  • 给count加1
  • 写回内存
    那极有可能存在这种情况
    线程1从内存中读到count的值为1,此时线程2也读到了1,然后2个线程都给count做自增操作并写回内存,此时内存中count的值为2,并不是正确结果3。
  1. 那如何解决多线程并发导致不能获得正确结果的问题呢?
    java引入了锁的机制,也就是关键字synchronized同步锁,每个对象都有一把独立的锁,类对象只有一个锁。只有获得锁,才能执行被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();
}

每个线程执行的时候,都尝试去获取MainActivity对象的锁,如果拿不到,就阻塞等。这就保证了同一时刻只有一个线程读写count这个变量,从而确保了结果的正确性,但是计算耗时增加了,效率变低。
修饰代码块很好理解,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,"我拿到了类锁");

代码将永远得不到执行,但是实际情况是

01-02 18:03:49.760 14175-14203/com.debug.pluginhost D/MainActivity: 我获得了类锁

这行代码得到了执行,这说明synchronized修饰成员方法的时候,获得是对象的锁。

  • 修饰静态方法
    修饰静态方法我们就不举例了,这种情况,synchronized获得是类对象的锁。
  1. wait(),notify()和notifyAll()
    synchrinized拿到锁后,如果需要,可以中途可以通过lockobj.wait()释放锁并阻塞在wait()处,等待其他线程通过lockobj.notify()或者lockobj.notifyAll()唤醒继续执行,一个典型的例子就是阻塞队列。
    看代码
    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,假设某个线程T获得锁,它开始执行代码,但是它发现自己无法处理当前的情况,因此只能等待,通过调用wait方法进入W集合,并释放自己拿到的锁

while (queue.size() == 0){ //我无法处理这种情况
       queue.wait();//进入等待集合吧,并且释放了自己获得的锁
 }

当有线程T1获得了锁,并成功执行push方法后,发出了notify()通知,此时JVM会从W中随机选一个线程T进入C集合去竞争锁,一旦T获得锁,它将从wait()处继续执行代码。如果能把notify弄明白,那么notifyAll就不难了,从字面意思就能理解,notifyAll会把W集合里所有的等待线程都放入到C集合里去竞争锁。

  1. 死锁
    大家仔细考虑下上面的代码有什么问题?
    我们假设有2个线程C1和C2调用pop方法,有1个线程P1调用push方法,假设执行顺序是这样的
  • C1执行,发现queue里没数据,那么C1进入W集合
  • C2执行,发现queue里没数据,那么C2进入W集合
  • P1执行,push一个数据到队列后,唤醒C1线程,此时C集合里有C1和P1,如果此时P1再次获得对象锁,那么P1进入W集合,此时C集合里只剩下C1,queue.size = 1
  • C1获得对象锁,pop数据后,唤醒了C2,此时C1、C2在集合C中,queue.size = 0
  • C1获得对象锁,因为queue.size = 0,因此C1进入W集合
  • C2获得对象锁,因为queue.size = 0,因此C2进入W集合
  • 程序陷入死锁
    思考notify和notifyAll的不同,在这种场景下只能用notifyAll。因为notifyAll会唤醒所有在W集合中的线程。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 听完润总这期第三个8小时想到了之前听大鹏演讲自己的成功之路 孟母三迁般的随着公司的脚步搬家 就为了每次有演艺工作都...
    臧宝珑阅读 1,648评论 0 0
  • 心想生自在人生自在经 提问 1我现在头脑知道物质世界不存在,是假相,所有的山川河流都是假相,可就是看起来特别真实,...
    实现幸福的人生阅读 4,704评论 0 6
  • 别忘了学习。20来岁正处于脑力最好、学习效率最高的人生阶段,要珍惜。不要相信先搞人脉,后搞学习。人脉再广,没有能力...
    Kecvin阅读 1,639评论 0 0

友情链接更多精彩内容