Android并发编程 多线程与锁

时间:2022-09-01 22:30:01

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


前言

前一篇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):表示该线程已经执行完毕。

    Android并发编程 多线程与锁

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

  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变量前面执行。即该关键字保证了时序性

如何正确使用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中的原子类与并发容器


此致,敬礼

Android并发编程 多线程与锁的更多相关文章

  1. 并发编程-多线程&comma;GIL锁

    本章内容: 1.什么是GIL 2.GIL带来的问题 3.为什么需要GIL 4.关于GIL的性能讨论 5.自定义的线程互斥锁与GIL的区别 6.线程池与进程池 7.同步异步,阻塞非阻塞 一.什么是GIL ...

  2. python 并发编程 多线程 互斥锁

    互斥锁 并行变成串行,牺牲效率 保证数据安全,实现局部串行 保护不同的数据,应该加不同的锁 现在一个进程 可以有多个线程 所有线程都共享进程的地址空间 实现数据共享 共享带来问题就会出现竞争 竞争就会 ...

  3. python 并发编程 多线程 目录

    线程理论 python 并发编程 多线程 开启线程的两种方式 python 并发编程 多线程与多进程的区别 python 并发编程 多线程 Thread对象的其他属性或方法 python 并发编程 多 ...

  4. &lbrack;并发编程 - 多线程:信号量、死锁与递归锁、时间Event、定时器Timer、线程队列、GIL锁&rsqb;

    [并发编程 - 多线程:信号量.死锁与递归锁.时间Event.定时器Timer.线程队列.GIL锁] 信号量 信号量Semaphore:管理一个内置的计数器 每当调用acquire()时内置计数器-1 ...

  5. Android并发编程 开篇

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  6. python并发编程&amp&semi;多线程(二)

    前导理论知识见:python并发编程&多线程(一) 一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性 官网链 ...

  7. 7&period;并发编程--多线程通信-wait-notify

    并发编程--多线程通信-wait-notify 多线程通信:线程通信的目的是为了能够让线程之间相互发送信号; 1. 多线程通信: 线程通信的目的是为了能够让线程之间相互发送信号.另外,线程通信还能够使 ...

  8. Python并发编程——多线程与协程

    Pythpn并发编程--多线程与协程 目录 Pythpn并发编程--多线程与协程 1. 进程与线程 1.1 概念上 1.2 多进程与多线程--同时执行多个任务 2. 并发和并行 3. Python多线 ...

  9. python并发编程&amp&semi;多线程(一)

    本篇理论居多,实际操作见:  python并发编程&多线程(二) 一 什么是线程 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程,一 ...

随机推荐

  1. SSIS 学习(6):包配置(上)【转】

    Integrartion Services 包实际上就是一个对象属性的集合,在前面我们开发的所有 Integration Services包,其中的变量.属性,比如:数据库链接.同步文件目录等,我们都 ...

  2. mysqli和mysql和pdo查询

      mysql mysql_connect($db_host, $db_user, $db_password); mysql_select_db($dn_name); $result = mysql_ ...

  3. wall

    wall 是在linux中用于发送广播信息的命令,他可以让所有的在线用户都收到信息 wall hi 然后,按Ctrl + c 退出

  4. http协议说明

    今天公司有同事让我给他讲一讲http..然后自己写了一个示例代码,这如果都看不懂.那我也没办法了.... <?php //这里服务器以apache举例.nginx.iis.他们实际上处理方式的都 ...

  5. &commat;produces在spring mvc中是什么意思

    @RequestMapping(value = "/produces", produces = "application/json"):表示将功能处理方法将生产 ...

  6. EXT2&sol;EXT3文件系统(一)

    整理自<鸟哥的Linux私房菜>,整理者:华科小涛http://www.cnblogs.com/hust-ghtao/ 1.文件系统概念引入 文件系统是一种存储和组织计算机数据的方法,它使 ...

  7. STAR法则的感想

    STAR法则百度百科上被解释为,面试官用于收集面试者信息的工具,而我个人理解,它更像是一个表达技巧,叙述结构,我们先来看看什么是STAR法则: STAR法则,即为Situation Task Acti ...

  8. UBuntu sudo 命令 :xxx is not in the sudoers file&period; This incident will be reported&period;

    [1]分析问题 提示内容翻译成中文即:用户XXX(一般是新添加的用户名称)没有权限使用sudo. 解决方法修改新用户的权限,具体操作即修改一下/etc/sudoers文件. [2]切换至root用户模 ...

  9. groupby elasticsearch

    GET usertag/usertag/_search { "query": { "match": { "tagname": "春 ...

  10. python apache下出现The &lowbar;imaging C module is not installed

    操作系统:win7 64位 安装python版本 win32 2.7版本 安装的PIL插件PIL-1.1.7.win32-py2.7.exe 用本地自带的开发服务器上传图片处理等一切正常 放到APAC ...