线程安全之:同步锁

一、线程安全

线程安全是多线程编程中的一个概念,在多线程的执行过程中访问修改某个共享数据时,线程安全的代码会在同步机制的保证下各个线程正常且正确的运行,数据按照程序既定的逻辑正确输出结果,不会出现数据污染等意外情况,反之就是线程非安全。
举一个反例:

public class ThreadSafetyActivity extends Activity {

    private int count = 0;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_safety);
        findViewById(R.id.start_multi_thread).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new OneThread()).start();
                new Thread(new TwoThread()).start();
            }
        });
    }

    private void getCount() {
        count++;
        Log.i("==TAG==", "count : " + count);
    }

    private class OneThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                getCount();
            }
        }
    }

    private class TwoThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                getCount();
            }
        }
    }
}

如果执行这段程序的话,我们会得到一段错误的数据,如下:

count : 11
count : 12
count : 14
count : 14
count : 16
count : 16
count : 18
count : 18

我们可以看到两个同样的结果,这就表明我们的线程是不安全的,如果在项目中有这种代码的话就是一个重大bug,出现这种问题的原因就是当我们的A线程修改完数据准备打印的时候B线程进入方法修改了数据导致两个线程输出了相同的结果,在这种情况下我们就得要用同步锁来限制线程的访问了。

二、同步锁synchronized

synchronized是一个关键字,用synchronized修饰了的方法就代表加了锁,任何一个线程运行到这儿的时候,都要检查有没有其他线程正在用这个方法,如果有的话等待正在用的线程执行完了之后再去执行这个方法,如果没有的话锁定该方法,然后直接运行。
他有两种方法:synchronized方法和synchronized块

(1)synchronized同步方法

接着我们用synchronized修饰getCount()方法再执行一下程序

private synchronized void getCount() {
        count++;
        Log.i("==TAG==", "count : " + count);
    }

输出结果:

count : 11
count : 12
count : 13
count : 14
count : 15
count : 16
count : 17
count : 18

我们可以看到,这回已经没有相同输出的情况了,这就表明我们的方法已变为线程安全,不再担心数据错乱的情况。
上面讲到的是同步方法的好处,但同步方法也有它的缺点,如果A线程调用的同发方法执行一个长时间的任务,那么B线程就必须耗很长时间来等待A线程执行完,这个时候我们可以用到同步块来解决这个问题。

(2)synchronized同步块

同步块是指用synchronized把方法内的某一段代码包起来,并使用一个Object对象去监视这个同步块代码,这个Object对象被称为"监视器对象",一个线程访问该同步代码块时另一个线程的访问会被阻塞,等到当前线程执行完为止。

同一个"监视器对象"的同一个同步块
我们写一个同步块代码并睡眠2秒钟,再使用两个线程同时访问:

    private void function(String t) {
        Log.i("==TAG==", "进入方法    " + t);
        synchronized (this) {
            try {
                Log.i("==TAG==", "进入同步块  " + t);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Log.i("==TAG==", "结束方法    " + t);
    }

结果输出:

进入方法    A
进入方法    B
进入同步块  A
结束方法    A
进入同步块  B
结束方法    B

可以看到当一个线程访问一个"监视器对象"中的同步块代码时另一个线程再去访问这个同步块会被阻塞,但仍然可以访问方法内的非同步块代码。
同一个"监视器对象"的不同同步块
接下来我们把代码修改为两个同步块代码,并用同一个监视器对象ThreadSafetyActivity监视,再用两个线程同时访问。

private void functionA() {
       Log.i("==TAG==", "进入了方法 A");
       synchronized (ThreadSafetyActivity.this) {
           try {
               Log.i("==TAG==", "进入了同步块 A");
               Thread.sleep(2000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       Log.i("==TAG==", "结束了方法 A");
}

private void functionB() {
      Log.i("==TAG==", "进入了方法 B");
      synchronized (ThreadSafetyActivity.this) {
          try {
              Log.i("==TAG==", "进入了同步块 B");
              Thread.sleep(2000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
      Log.i("==TAG==", "结束了方法 B");
}

输出结果:

进入了方法 A
进入了同步块 A
进入了方法 B
结束了方法 A
进入了同步块 B
结束了方法 B

可以看出两个线程同时访问同一个"监视器对象"的不同同步块时仍然会阻塞,这就是同步块的另一个特性:
同步块是同步在某个对象上的,所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作,所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出,反之如果不是同一个"监视器对象"的同步块的话可同时执行。

三、同步锁Lock

Lock是一个接口,需要手动去维护状态的一个同步锁,而且它只有一个实现ReentrantLock(可重入锁),为什么有synchronized的情况下还会出Lock 呢?因为synchronized有一个缺点,那就是一个线程必须等待前一个线程执行完之后才能去执行,如果前一个线程有耗时操作的话后一个线程一直在等待的状态中,这就导致程序不灵活且效率低下,所以java在jdk1.6中加入了Lock来解决这个问题,Lock的获取锁、释放锁、线程中断等都可以由我们在程序中控制,给多线程同步带来了更多扩展性。
我们看下其源码:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

发现只有六个方法,接下来我们逐一去解读下,其中lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的,我们先来看看lock()和unLock()的使用方法:

    private Lock lock = new ReentrantLock();

    private void functionLock() {
        lock.lock();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

可以看到方法内部先用lock()方法加了锁,最后在finally中释放了锁,因为finally是无论如何都会执行的代码,所以把unlock放入finally就不会导致死锁现象。
接下来再看看tryLock()和tryLock(long time, TimeUnit unit)方法,这两个方法有相同的作用,那就是会返回获取锁的状态,如果获取成功返回true,获取失败返回flase,tryLock(long time, TimeUnit unit)是可以设定一个等待时间,如果在这个时间内还拿不到锁就会返回false,以下是用法:

private void functionTryLock() {
        //todo 不等待获取锁
        if (lock.tryLock()) {
            try {
                //处理任务
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        } else {
            //没有获取到锁,可以干别的事情
        }

        //todo 等待获取锁
        try {
            if(lock.tryLock(1000, TimeUnit.SECONDS)){
                try {
                    //处理任务
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }else {
                //规定时间内没有获取到锁,可以干别的事情
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

接下来再看看lockInterruptibly()方法,这个方法没有返回值,说明它也是个阻塞的方法,两个线程同时获取锁时,一个获取成功,一个获取失败继续等待,这个过程中我们可以调用等待线程的interrupt()方法让其中断,而用synchronized修饰的话是无法中断的,注意的是interrupt()方法对正在执行的线程无效。
使用如下:

    private void funcionInterruptibly(){
        try {
            lock.lockInterruptibly();
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

四、总结

(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
(3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
(4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
(5)Lock可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

你可能感兴趣的:(线程安全之:同步锁)