时间:2021-05-20
本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题。
一.一个典型的Java线程安全例子
上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟你和你老婆同时在取款机进行取钱操作的场景。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:
1 取钱成功, 取出钱数为:700.0
2 余额为:300.0
3 取钱成功, 取出钱数为:700.0
4 余额为:-400.0
也就是说,对于一张只有1000余额的银行卡,你们一共可以取出1400,这显然是有问题的。
经过分析,问题在于Java多线程环境下的执行的不确定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,然后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。
因此,讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。
二.同步方法
对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...
在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synshronized关键字即可。
三.同步代码块
正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。
同步代码块的格式为:
synchronized (obj) { //... }其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。
如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心...)
四.Lock对象同步锁
上面我们可以看出,正因为对同步锁对象的选择需要如此小心,有没有什么简单点的解决方案呢?以方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题。
使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:
五.wait()/notify()/notifyAll()线程通信
在博文《Java总结篇系列:java.lang.Object》中有提及到这三个方法,虽然这三个方法主要都是用于多线程中,但实际上都是Object类中的本地方法。因此,理论上,任何Object对象都可以作为这三个方法的主调,在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通信。
wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。
notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
上面的例子演示了wait()/notify()/notifyAll()的用法。部分输出结果为:
取钱线程 开始要执行wait操作 执行了wait操作 -- i=0
存钱线程 存款:700.0 -- i=0
存钱线程-- 存钱 -- 执行完毕 -- i=0
存钱线程 开始要执行wait操作 -- i=1
取钱线程 执行了wait操作 执行了wait操作 -- i=0
取钱线程 取钱:700.0 -- i=1
取钱线程-- 取钱 -- 执行完毕 -- i=1
取钱线程 开始要执行wait操作 执行了wait操作 -- i=2
存钱线程 执行了wait操作 -- i=1
存钱线程 存款:700.0 -- i=2
存钱线程-- 存钱 -- 执行完毕 -- i=2
取钱线程 执行了wait操作 执行了wait操作 -- i=2
取钱线程 取钱:700.0 -- i=3
取钱线程-- 取钱 -- 执行完毕 -- i=3
取钱线程 开始要执行wait操作 执行了wait操作 -- i=4
存钱线程 存款:700.0 -- i=3
存钱线程-- 存钱 -- 执行完毕 -- i=3
存钱线程 开始要执行wait操作 -- i=4
取钱线程 执行了wait操作 执行了wait操作 -- i=4
取钱线程 取钱:700.0 -- i=5
取钱线程-- 取钱 -- 执行完毕 -- i=5
取钱线程 开始要执行wait操作 执行了wait操作 -- i=6
存钱线程 执行了wait操作 -- i=4
存钱线程 存款:700.0 -- i=5
存钱线程-- 存钱 -- 执行完毕 -- i=5
存钱线程 开始要执行wait操作 -- i=6
取钱线程 执行了wait操作 执行了wait操作 -- i=6
取钱线程 取钱:700.0 -- i=7
取钱线程-- 取钱 -- 执行完毕 -- i=7
取钱线程 开始要执行wait操作 执行了wait操作 -- i=8
存钱线程 执行了wait操作 -- i=6
存钱线程 存款:700.0 -- i=7
由此,我们需要注意如下几点:
1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;
2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;
3.notify()/notifyAll()执行后,如果右面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;
4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;
5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。
当然,上面的例子相对来说比较简单,只是为了简单示例wait()/notify()/noitifyAll()方法的用法,但其本质上说,已经是一个简单的生产者-消费者模式了。
系列文章:
java 多线程实例讲解 (一)
Java 多线程实例详解(二)
Java 多线程实例详解(三)
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
本文承接上一篇文章《Java多线程实例详解(一)》。四.Java多线程的阻塞状态与线程控制上文已经提到Java阻塞的几种具体类型。下面分别看下引起Java线程阻
Java中CountDownLatch进行多线程同步详解CountDownLatch介绍在前面的Java学习笔记中,总结了Java中进行多线程同步的几个方法:1
详解Java中多线程异常捕获Runnable的实现1、背景:Java多线程异常不向主线程抛,自己处理,外部捕获不了异常。所以要实现主线程对子线程异常的捕获。2、
Java多线程实例3种实现方法Java中的多线程有三种实现方式:1.继承Thread类,重写run方法。Thread本质上也是一个实现了Runnable的实例,
本文实例讲述了Android编程开发实现带进度条和百分比的多线程下载。分享给大家供大家参考,具体如下:继上一篇《java多线程下载实例详解》之后,可以将它移植到