线程安全之:同步锁

一、线程安全

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

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。所以说,在具体使用时要根据适当情况选择。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容