zoukankan      html  css  js  c++  java
  • JavaSE 多线程(三)

    1. 同步问题

    1.1 线程间的通信

    管道流可以连接两个线程间的通信。

    1.2 线程间的资源互斥共享

    通常,一些同时运行的线程需要共享数据。在这种时候,每个线程就必须考虑与它一起共享数据的其他线程的状态与行为,否则就不能保证共享数据的一致性,因而也不能保证程序的正确性。

    在 Java 中,通过提供一个特殊的锁定标志来处理数据。

    1.3 对象的锁定标志

    在 Java 语言中,引入了 “对象互斥锁” 的概念(又称为监视器、管程)来实现不同线程对共享数据操作的同步。“对象互斥锁” 阻止了多个线程同时访问同一条件变量。Java 可以为每一个对象的实例配有一个 “对象互斥锁”。

    在 Java 语言中,有两种方法可以实现 “对象互斥锁”:

    • 用关键字 volatile 声明一个共享数据(变量)。

    • 用关键字 synchronized 声明操作共享数据的一个方法或一段代码。

    因为等待一个 对象的锁定标志 的线程 要等到 持有该标志的线程 将其 返还后 才能继续运行,所以在 不使用该标志时 将其返还 就显得十分重要了。

    事实上,当持有锁定标志的线程运行完 synchronized() 调用包含的程序块后,这个标志会被自动返还。

    Java 保证了该标志通常能够被正确地返还,即使被同步的程序块产生了异常,或者某个循环中断跳出了该程序块,这个标志也能被正确返还。

    同样,如果一个线程两次调用了同一个对象,在退出最外层后,这个标志也将被正确释放,而在退出内层时则不会执行释放。

    1.3.1 volatile 关键字

    volatile 三大特性:

    • 可见性

    volatile 关键字的使用主要是为了同步 在公共堆栈中的变量 和 在线程私有堆栈中的变量。主要通过当线程访问该变量时,强制其从公共堆栈中取值。

    • 原子性

    在 X86 架构64位JDK 版本中,写 double 或 long 是原子的。

    • 进制代码重排序

    在 Java 程序运行时,JIT(Just-In-Time Compiler,即时编译器)可以动态地改变程序代码运行的顺序,例如,有如下代码:

    A代码 - 重耗时 B代码 - 轻耗时 C代码 - 重耗时 D代码 - 轻耗时

    在多线程的环境中,JIT 有可能进行代码重排,重排序后的代码顺序可能如下:

    B代码 - 轻耗时 D代码 - 轻耗时 A代码 - 重耗时 C代码 - 重耗时

    这样做的主要原因是 CPU 流水线是同时执行这 4 个指令的,那么轻耗时的代码在很大程度上先执行完,以让出 CPU 流水线资源给其他指令,所以代码重排序是为了追求更高的程序运行效率。

    重排序发生在没有依赖关系时,若上述代码中存在依赖关系,则不会进行重排序。

    使用 volatile 可以禁止代码重排序,例如有如下代码:

    A 变量操作
    B 变量操作
    volatile Z 变量操作
    C 变量操作
    D 变量操作

    那么有 4 种情况发生:

    1)A、B 可以重排序。

    2)C、D 可以重排序。

    3)A、B 不可以重排到 Z 的后面。

    4)C、D 不可以重排到 Z 的前面。

    换言之,变量 Z 是一个 “屏障”,Z 变量之前或之后的代码不可以跨越 Z 变量,这就是屏障的作用。

    以上所述三种特性,都可以使用 synchronized 实现。

    1.3.2 总结

    关键字 volatile 的主要作用就是让其他线程可以看到最新的值,volatile 只能修饰变量。

    使用场景:
    当想实现一个变量的值被更改时,让其他线程能够获取到最新的值时,就要对变量使用 volatile。

    1.4 同步方法

    用 sychronized 标识的代码段或方法即为 “对象互斥锁” 锁住的部分。如果一个程序内有两个或以上的方法使用 sychronized 标志,则它们在同一个 “对象互斥锁” 管理之下。

    一般情况下,多使用 synchronized 关键字在方法的层次上实现对共享资源操作的同步,很少使用 volatile 关键字声明共享变量。

    同步 synchronized 在字节码指令中的原理

    在方法中使用 synchronized 关键字实现同步的原因是使用了 flag 标记 ACC_SYNCHRONIZED,当调用方法时,调用指令会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置,如果设置了,执行线程会先持有同步锁,然后执行方法,最后在方法完成时释放锁。

    sychronized() 语句的标准写法为

    public void push(char c) {
          sychronized(this) {
                ...
          }
    }
    

    由于 synchronzied() 语句的参数必须是 this ,因此 Java 语言允许下面的简便写法(不建议):

    public sychronized void push(char c) {
          ...
    }
    

    比较以上两种写法,后者使用 sychronized 将整个方法视为同步块,这会使得持有锁定标记的时间比实际需要的要长,从而降低了效率。另一方面,使用前者来标记可以提醒用户同步在发生。这对于下面的 死锁 非常重要。

    1.4.1 同步写法案例比较

    使用关键字 synchronized 的写法比较多,常用的有如下几种:

    public class MyService {
        synchronized public static void testMethod1() {
        }
    
        public void testMethod2() {
            synchronized (MyService.class) {
            }
        }
        
        synchronized public void testMethod3() {
        }
    
        public void testMethod4() {
            synchronized (this) {
            }
        }
        
        public void testMethod5() {
            synchronized ("abc") {
            }
        }
    }
    

    上面的代码出现了 3 种类型的锁对象:

    (A)testMethod1() 和 testMethod2() 持有的锁是同一个,即 MyService.java 对应 Class 类的对象。

    (B)testMethod3() 和 testMethod4() 持有的锁是同一个,即 MyService.java 类的对象。

    (C)testMethod5() 持有的锁是字符串 "abc"。

    说明 testMethod1() 和 testMethod2() 是同步关系,testMethod3() 和 testMethod4() 是同步关系.

    上述三种类型中,A 和 C 之间是异步关系,B 和 C 之间是异步关系,A 和 B 之间是异步关系。

    1.4.2 总结

    关键字 synchronized 的主要作用就是保证同一时刻,只有一个线程可以执行某一个方法,或是某一个代码块,synchronized 可以修饰方法及代码块。

    使用场景:当多个线程对同一个对象中的同一个实例变量进行操作时,为了避免出现非线程安全问题,就要使用 synchronized。

    2. 死锁

    如果一个线程持有一个锁并试图获取另一个锁时,就有死锁的危险。这是在多线程竞争使用多资源的程序中,有可能出现的情况。

    死锁情况发生在第一个线程等待第二个线程所持有的锁,而第二个线程又在等待第一个线程持有的锁的时候,每个线程都不能继续运行,除非有一个线程运行完同步程序块。而恰恰因为哪个线程都不能继续进行,所以哪个线程都无法运行完同步程序块。

    Java 既不检测也不采取办法避免这种状态,因此保证不发生死锁只能靠程序员自身的设计。

    具体避免死锁的方法可见本人另一篇博客 :银行家算法 C++实现

    3. 线程交互————wait() 和 notify()

    3.1 生产者与消费者

    具体的例子,如生产者与消费者:

    有两个人,一个人在刷盘子,另一个在把盘子烘干。这两个人各自代表一个线程,他们之间有一个共享对象————碗架,刷好而等待烘干的盘子放在碗架上,显然,碗架上有刷好的盘子时,负责烘干的人才能开始工作;而如果刷盘子的人刷的太快,刷好的盘子占满了碗架时,他就不能继续工作了,要等到碗架上空位置了才行。

    涉及多线程间共享数据操作时,除了同步问题之外,还会遇到的就是,如何控制相互交互的线程之间的运行速度,即多线程的同步。

    上图说明的问题是,生产者生产一个产品后就放入共享对象中,而不管共享对象中是否已有产品。消费者从共享对象中取用产品,但不检测是否已经取过。

    若共享对象中只能存放一个数据,可能会出现以下问题:

    • 生产者比消费者快时,消费者会漏掉一些数据取不到。

    • 消费者比生产者快时,消费者取的数据相同。

    为了解决所出现的问题,在 Java 中可以用 wait() 和 notify()/notifyAll() 方法(在java.lang.Object中定义)协调线程间的运行速度(读取)关系。

    注意:

    • 在执行 wait() 调用的时候,Java 首先吧锁定标志返回给对象,因此即使一个线程由于执行 wait() 调用而被阻塞,它也不会影响其他等待锁定标志的线程的运行。

    • 当一个线程被 notify() 后,它并不立即变为可执行状态,而仅仅是从等待队列中移入锁定标志队列中。这样在冲洗获得锁定标志之前,它仍旧不能继续运行。

    在实际实现中,方法 wait() 既可以被 notify() 终止,也可以通过调用线程的 interrupt() 来终止。后一种情况下,wait() 会抛出一个 InterruptedException 异常,所以需要把它放在 try/catch 结构中。

    3.2 守护线程

    守护线程,是为其他线程提供服务的线程,它一般应该是一个独立的线程,它的 run() 方法是一个无限循环。

    可以用 public boolean isDaemon() 来确定一个线程是否是守护线程,同时也可以用 public void setDaemon(boolean)设定一个线程为守护线程。

    一般地,守护线程都用来做辅助性工作,如用于提示、帮助等。

  • 相关阅读:
    Microsoft.VisualBasic.PowerPacks相关错误解决办法
    GridView绑定技巧终结者
    类型初始值设定项引发异常处理办法
    目前为目最全的CURL中文说明CURL
    [转]大型网站架构之优酷篇
    [原]ecshop代码分析二(缓存处理)
    [转]大型网站架构不得不考虑的10个问题
    [原]ecshop代码分析一(init.php文件)
    发布一款坐标转换软件
    坐标换算软件操作方法及下载地址
  • 原文地址:https://www.cnblogs.com/john1015/p/13949628.html
Copyright © 2011-2022 走看看