Android并发编程 多线程与锁

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,若是能给各位看官带来一丝启发或者帮助,那真是极好的。java


前言

前一篇Android并发编程开篇呢,主要是简单介绍一下线程以及JMM,虽然文章不长,但倒是理解后续文章的基础。本篇文章介绍多线程与锁。面试

深刻认识Java中的Thread

Thread的三种启动方式上篇文章已经说了,下面呢,咱们继续看看Thread这个类。编程

线程的状态

Java中线程的状态分为6种。安全

  1. 初始(NEW):新建立了一个线程对象,但尚未调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象建立后,其余线程(好比main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在得到CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程须要等待其余线程作出一些特定动做(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不一样于WAITING,它能够在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

线程的几个常见方法的比较

  1. Thread.sleep(long millis),必定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。做用:给其它线程执行机会的最佳方式。
  2. Thread.yield(),必定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。做用:让相同优先级的线程轮流执行,但并不保证必定会轮流执行。实际中没法保证yield()达到让步目的,由于让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会致使阻塞。该方法与sleep()相似,只是不能由用户指定暂停多长时间。
  3. thread.join()/thread.join(long millis)当前线程里调用其它线程thread的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程thread执行完毕或者millis时间到,当前线程进入就绪状态。
  4. thread.interrupt(),当前线程里调用其它线程thread的interrupt()方法,中断指定的线程。
    若是指定线程调用了wait()方法组或者join方法组在阻塞状态,那么指定线程会抛出InterruptedException
  5. Thread.interrupted,必定是当前线程调用此方法,检查当前线程是否被设置了中断,该方法会重置当前线程的中断标志,返回当前线程是否被设置了中断。
  6. thread.isInterrupted()当前线程里调用其它线程thread的isInterrupted()方法,返回指定线程是否被中断
  7. object.wait()当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
  8. object.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的全部线程。

线程安全

volatile 以及 synchronized 关键字

在上一篇博文中,各位看官已经对JMM模型有了初步的了解,咱们在谈论线程安全的时候也无外乎解决上篇博文中提到的3个问题,原子性、可见性、时序性多线程

volatile

当一个共享变量被volatile修饰以后, 其就具有了两个含义并发

  1. 线程修改了变量的值时, 变量的新值对其余线程是当即可见的。 换句话说, 就是不一样线程对这个变量进行操做时具备可见性。即该关键字保证了可见性
  2. 禁止使用指令重排序。这里提到了重排序, 那么什么是重排序呢? 重排序一般是编译器或运行时环境为了优化程序性能而采起的对指令进行从新排序执行的一种手段。volatile关键字禁止指令重排序有两个含

义: 一个是当程序执行到volatile变量的操做时, 在其前面的操做已经所有执行完毕, 而且结果会对后面的
操做可见, 在其后面的操做尚未进行; 在进行指令优化时, 在volatile变量以前的语句不能在volatile变量后面执行; 一样, 在volatile变量以后的语句也不能在volatile变量前面执行。即该关键字保证了时序性ide

如何正确使用volatile关键字呢
一般来讲, 使用volatile必须具有如下两个条件:工具

  1. 对变量的写操做不会依赖于当前值。 例如自增自减
  2. 该变量没有包含在具备其余变量的不变式中。

synchronized

去面试java或者Android相关职位的时候个东西貌似是必问的,关于synchronized这个关键字真是有太多太多东西了。尤为是JDK1.6以后为了优化synchronized的性能,引入了偏向锁,轻量级锁等各类听起来就头疼的概念,java还有Android面试世界流传着一个古老的名言,考察一我的对线程的了解成度的话,一个synchronized就足够了。不过本篇博文不讲那些,本篇博文本着让各位看官都能理解的初衷试着分析一下synchronized关键字把性能

重入锁ReentrantLock

synchronized 关键字自动提供了锁以及相关的条件。 大多数须要显式锁的状况使用synchronized很是方
便, 可是等咱们了解了重入锁和条件对象时, 能更好地理解synchronized关键字。 重入锁ReentrantLock是
Java SE 5.0引入的, 就是支持重进入的锁, 它表示该锁可以支持一个线程对资源的重复加锁。优化

ReentrantLock reentrantLock = new ReentrantLock();
   reentrantLock.lock();
try {
    ...

} finally {
    reentrantLock.unlock();
}

如上代码所示,这一结构确保任什么时候刻只有一个线程进入临界区, 临界区就是在同一时刻只能有一个任务访问的代码区。 一旦一个线程封锁了锁对象, 其余任何线程都没法进入Lock语句。 把解锁的操做放在finally中是十分必要的。 若是在临界区发生了异常, 锁是必需要释放的, 不然其余线程将会永远被阻塞。

synchronized关键字

咱们再来看看synchronized,synchronized关键字有如下几种使用方式

  1. 同步方法(即直接在方法声明处加上synchronized)

    private synchronized void test() {
    
     }

    等价于

    ReentrantLock reentrantLock = new ReentrantLock();
    
    private void test() {
        reentrantLock.lock();
        try {
           ...
        } finally {
            reentrantLock.unlock();
        }
    }
  2. 同步代码块

    上面咱们说过, 每个Java对象都有一个锁, 线程能够调用同步方法来得到锁。 还有另外一种机制能够获
    得锁, 那就是使用一个同步代码块, 以下所示:

    synchronized(obj){
    }

    其得到了obj的锁, obj指的是一个对象。 同步代码块是很是脆弱的,一般不推荐使用。 通常实现同步最h好用java.util.concurrent包下提供的类, 好比阻塞队列。 若是同步方法适合你的程序, 那么请尽可能使用同步方法, 这样能够减小编写代码的数量, 减小出错的几率。
    咱们在代码中写的synchronized(this){} 实际上是与上面同样的,this指代当前对象

  3. 静态方法加锁

    static synchronized void test();

这种方式网上有人称它为“类锁”,其实这种说法有些迷惑人,咱们只须要记住一点,全部的锁都是锁住的对象,也就是Object自己,你能够简单理解为使用synchronized 是在堆内存中的某一个对象上加了一把锁,而且这个锁是可重入的,意思是说若是一个线程已经得到了某个对象的锁,那么该线程依然能够从新得到这把锁,可是其余线程若是想访问这个对象就必须等待上一个得到锁的线程释放锁。

咱们在回过头来看静态方法加锁,为一个类的静态方法加锁,实际上等价于synchronized(Class),即锁定的是该类的Class对象。

线程同步

Object.wait() / Object.notify() Object.notifyAll()

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、
wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,能够
实现等待/通知模式

  1. 使用的前置条件

    当咱们想要使用Object的监视器方法时,须要或者该Object的锁,代码以下所示

    synchronized(obj){
        .... //1
        obj.wait();//2
        obj.wait(long millis);//2
        ....//3
    }

    一个线程得到obj的锁,作了一些时候事情以后,发现须要等待某些条件的发生,调用obj.wait(),该线程会释放obj的锁,并阻塞在上述的代码2处
    obj.wait()和obj.wait(long millis)的区别在于

    obj.wait()是无限等待,直到obj.notify()或者obj.notifyAll()调用并唤醒该线程,该线程获取锁以后继续执行代码3

    obj.wait(long millis)是超时等待,我只等待long millis 后,该线程会本身醒来,醒来以后去获取锁,获取锁以后继续执行代码3

    obj.notify()是叫醒任意一个等待在该对象上的线程,该线程获取锁,线程状态从BLOCKED进入RUNNABLE

    obj.notifyAll()是叫醒全部等待在该对象上的线程,这些线程会去竞争锁,获得锁的线程状态从BLOCKED进入RUNNABLE,其余线程依然是BLOCKED,获得锁的线程执行代码3完毕后释放锁,其余线程继续竞争锁,如此反复直到全部线程执行完毕。

    synchronized(obj){
        .... //1
        obj.notify();//2
        obj.notifyAll();//2
    }

    一个线程得到obj的锁,作了一些时候事情以后,某些条件已经知足,调用obj.notify()或者obj.notifyAll(),该线程会释放obj的锁,并叫醒在obj上等待的线程,
    obj.notify()和obj.notifyAll()的区别在于

    obj.notify()叫醒在obj上等待的任意一个线程(由JVM决定)

    obj.notifyAll()叫醒在obj上等待的所有线程

  2. 使用范式

    synchronized(obj){
        //判断条件,这里使用while,而不使用if
        while(obj知足/不知足 某个条件){
            obj.wait()
        }
    }

    放在while里面,是防止处于WAITING状态下线程监测的对象被别的缘由调用了唤醒(notify或者notifyAll)方法,可是while里面的条件并无知足(也可能当时知足了,可是因为别的线程操做后,又不知足了),就须要再次调用wait将其挂起

条件对象Condition

JDK1.5后提供了Condition接口,该接口定义了相似Object的监视器方法,与Lock配合能够实现等待/通知模式,可是这二者在使用方式以及功能特性上仍是有差异的

public interface Condition {
    //等待 同object.wait()
    void await() throws InterruptedException;

    //无视中断等待 object没有此类方法
    void awaitUninterruptibly();

    //超时等待 同object.wait(long millis)
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    //超时等待 
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    //超时等待 到未来的某个时间 object没有此类方法
    boolean awaitUntil(Date deadline) throws InterruptedException;

    //通知 同object.notify()
    void signal();

    //通知 同object.notifyAll()
    void signalAll();
}

除了上述API之间的差异外,Condition与Object的监视器方法显著的差异在于前置条件

wait和notify/notifyAll方法只能在同步代码块里用(这个有的面试官也会考察)

Condition接口对象需和Lock接口配合,经过lock.lock()获取锁,lock.newCondition()获取条件对象更为灵活
关于Condition接口的具体实现请往下看

LockSupport.park(Object blocker) / LockSupport.unpark(Thread thread)

上面说的Condition是一个接口,咱们来看一下Condition接口的实现,Condition接口的实现主要是经过另一套等待/通知机制完成的。

LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,
而LockSupport也成为构建同步组件的基础工具。

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。

既然JDK已经提供了Object的wait和notify/notifyAll方法等方法,那么LockSupport定义的一组方法有何不一样呢,咱们来看下面这段代码就明白了

Thread A = new Thread(new Runnable() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            try {
                Thread.sleep(10000);//睡眠10s,保证LockSupport.unpark(A);先调用
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //直接调用park方法阻塞当前线程,没在同步方法或者代码块内
            LockSupport.park(this);
            System.out.println(sum);
        }
    });
A.start();

//调用unpark方法唤醒指定线程,即便unpark(Thread)方法先于park方法调用,依然能唤醒
LockSupport.unpark(A);

对比一下Object的wait和notify/notifyAll方法你就能明显看出区别

final Object obj = new Object();

Thread B = new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized (obj) {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            try {
                Thread.sleep(10000);//睡眠10s,保证obj.notify();先调用
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(sum);
        }

    }
});
B.start();


synchronized (obj) {
    //若是obj.notify();先于obj.wait()调用,那么调用调用obj.wait()的线程会一直阻塞住
    obj.notify();
}

在LockSupport的类说明上其实已经说明了LockSupport相似于Semaphore,

Semaphore是计数信号量。Semaphore管理一系列许可证。每一个acquire方法阻塞,直到有一个许可证能够得到而后拿走一个许可证;
每一个release方法增长一个许可证,这可能会释放一个阻塞的acquire方法。

然而,其实并无实际的许可证这个对象,Semaphore只是维持了一个可得到许可证的数量。

Semaphore常常用于限制获取某种资源的线程数量。

LockSupport经过许可证来联系使用它的线程。
若是许可证可用,调用park方法会当即返回并在这个过程当中消费这个许可,否则线程会阻塞。
调用unpark会使许可证可用。(和Semaphores有些许区别,许可证不会累加,最多只有一张)
由于有了许可证,因此调用park和unpark的前后关系就不重要了,

如何正确中止一个线程

讲解了上面那么多内容,如今出一个小小的笔试题,如何正确中止一个线程,别说是thread.stop()哈,那个已经被标记过期了。若是您想参与这个问题请在评论区评论。


本篇总结

本篇主要是说了关于多线程与锁的东西。这里总结一下

volatile 保证了共享变量的可见性和禁止重排序,

Synchronized的做用主要有三个:

(1)确保线程互斥的访问同步代码

(2)保证共享变量的修改可以及时可见(这个可能会被许多人忽略了)

(3)有效解决重排序问题。

从JMM上来讲

被volatile修饰的共享变量若是被一个线程更改,那么会通知各个线程大家的副本已通过期了,赶快去内存拉取最新值吧

被Synchronized修饰的方法或者代码块,咱们都知道会线程互斥访问,其实其有像volatile同样的效果,若是被一个线程更改了共享变量,在Synchronized结束处那么会通知各个线程大家的副本已通过期了,赶快去内存拉取最新值吧

因为笔者能力有限,若有不到之处,还请不吝赐教。


下篇预告

Java中的原子类与并发容器


此致,敬礼

相关文章
相关标签/搜索