Java程序员修炼之道 人民邮电出版社 吴海星译

2021/8/7 11:35:59

本文主要是介绍Java程序员修炼之道 人民邮电出版社 吴海星译,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

  •     并发,性能,字节码和类加载是最让我们着迷的核心技术.
  • java7跟之前版本相比有一个主要区别:它仕第一个明确着眼于下一次发布的新版本.根据Oracle有关发布的"B计划",Java 7为Java 8的主要变化打下了基础.
第一部分:用java 7做开发
  • java 7的 变化可以大致分为两块:Coin项目和NIO.2
    • Coin项目:设计他们的初中仕提高开发人员的生产率,但又不会对底层平台造成太大影响.
      • try-with-resources结构(可以自动关闭资源)
      • switch中的字符串
      • 对数字常量的改进
      • Multi-catch(在一个catch块中声明多个要捕获的异常)
      • 钻石语法(在处理泛型时不用那么繁琐了)
    • 新I/O API (NIO.2):跟java原有的文件系统支持相比,它具有压倒性的优势,还提供了强大的异步能力.
      • 用于引用文件和类文件实体的新path结构
      • 简化文件的创建,赋值,移动和删除的工具类Files
      • 内建的目录树导航
      • 在后台处理大型I/O的将来式和回调式异步I/O
第一章:初始Java 7
  • java既是编程语言,也是平台
  • 语言与平台
    • 控制Java系统的规范有多种,其中最重要的是<Java语言规范>(JLS)和<JVM规范>(VMSpec).
    • 连接Java语言和平台之间的纽带仕统一的类文件(即.class文件)格式定义.认真研究类文件的定义能让你获益匪浅,这是优秀java程序员向伟大java程序员转变的一个途径.
    • 实际上,JVM字节码更像是中途的驿站,仕一种从人类可读的源码向及其码过度的中间状态.用编译原理术语讲,字节码实际上是一种中间语言(IL)形态,不是真正的机器码.也就是说,将Java源码编程字节码的过程不是C/C++程序员所理解的那种变异.Java所谓的编译器javac也不同于gcc,实际上它致使一个针对java源码生成类文件的工具.java体系中真正的编译器仕JIT.
    • 有人说java是"动态编译"的,他们所说的编译仕指JIT的运行时编译,不是指构建时创建类文件的过程.所以如果被问及"Java仕编译型语言还是解释型语言",你可以回答"都是"
  • Coin项目:浓缩的都是精华
    • 我们觉得解释语言"为什么要变"和"变成了什么"同样重要.
    • 语法糖-数字中的下划线
      • 语法糖是描述一种语言特性的短语.它表示这是冗余的语法-在语言中已经存在一种表示形式了-但语法糖用起来更便捷.一般来说,程序的语法糖在编译处理早期会从编译结果中移除,变为相同特性的基础表示形式,这称为"去糖化",因此,语法糖仕比较容易实现的修改,他们通常不需要做太多工作,只需要修改编译器(对java来说就是javac)
    • java7是以开源方式开发后发布的第一个版本.开源的java平台开发主要集中在项目OpenJDK上.
    • Coin项目中的修改:Coin项目主要给java7引入了6个新特性
      • switch语句中的String
      • 更强的数值文本表示法
        • 数字常量(如基本类型种的integer)尅用二进制文本表示
          • int x = 0b1100110;
        • 在整型常量中可以适用下划线来提高可读性.
          • Coin项目中的提案借用了Ruby的创意,用下划线(_)做分隔符.
      • 改善后的异常处理
        • 异常处理有两处改进-multicatch和final重抛.
        • catch中的final重抛中的final关键字不是必需的,但实际上,在向catch和重抛语义调整的过渡阶段,留着它可以给你提个醒.
      • try-with-resources(TWR)
        • 墨菲定律(任何事都可能出错)
        • try (OutputStream out = new FileOutputStream(file);
                  InputStream is = url.openStream()) {
            byte[] buf = new byte[4096];
            int len;
            while ((len = is.read(buf)) > 0) {
            out.write(buf, 0, len);
            }
          }
        • 上面的代码例子是资源自动化管理代码块的基本形式-把资源放在try的圆括号内.
        • 但是在适用try-with-resources特性时还是要小心,因为在某些情况下资源可能无法关闭.比如在下面的代码中,如果从文件(someFile.bin)创建ObjectInputStream时出错,FileInputStream可能就无法正确关闭.
        • try(ObjectInputStream in = new ObjectInputStream(new FileInputStream("someFile.bin"))){...}
        • 要确保try-with-resources生效,正确的用法是为各个资源声明独立变量.
        • TWR和AutoCloseable:目前TWR特性依靠一个新定义的接口实现AutoClosseable.TWR的try从句中出现的资源类都必须实现这个接口.java7平台中的大多数资源类都被修改过,已经实现了AtuoCloseable(java7中还定义了其父接口Closeable),但并不是全部资源相关的类都采用了这项新技术.不过,JDBC4.1已经具备了这个特性.
      • 钻石语法
        • Map<Integer,Map<String,String>> usersLists = new HashMap<>();
        • 编译器为这个特性采用了新的类型推断形式.它能推断出表达式右侧的正确类型,而不是仅仅替换成定义完整类型的文本.
      • 简化变参方法调用
        • java7中的警告去了哪里?过去仕在编译适用API的代码时触发警告,而现在仕在编译这种可能会破坏类型安全的API时触发.编译器会警告创建这种API的程序员,让他注意类型系统的安全.
        • 类型系统的修改:Coin项目曾奉劝诸位贡献者远离类型系统,因为把这么一个小变化讲清楚要大费周章.
第二章:新I/O
  • JSR-203
  • NIO.2是一组新的类和方法,主要存在于java.nio包内.优点
    • 它完全取代了java.io.File与文件系统的交互.
    • 它提供了新的异步处理类,让你无需手动配置线程池和其他底层并发控制,便可在后台线程中执行文件和网络I/O操作.
    • 它引入了新的Network-Channel构造方法,简化了套接字socket与通道的编码工作.
  • 将try-with-resources和NIO.2中的新API结合起来可以写出非常安全的I/O程序,这在java中还是破天荒的第一次!
  • Perl,正则表达式之王.
  •    2.2文件I/O的基石:Path
    • NIO.2中的Path仕一个抽象构造. NIO.2把位置(由Path表示)的概念和物理文件系统的处理(比如赋值一个文件)分的很清楚,物理文件系统的处理通常是由Files辅助类实现的.
    • Path不一定代表真实的文件或目录.你可以随心所欲地操作Path,用Files中的功能来检查文件是否存在,并对它进行处理.
    • Path并不仅限于传统的文件系统,它也能表示zip或jar这样的文件系统.
    • 创建Path时可以用相对路径.比如:"../xxx"
    • 移除冗余项:在java7中,有两个辅助方法可以用来弄清Path的真是位置.
      • 用normalize()方法去掉Path中的冗余信息.
      • toRealPath()方法也很有效,它融合了toAbsolutePath()和normalize()两个方法的功能,还能检测并跟随符号连接.
    • 转换Path
      • 合并Path,调用resolve方法.
      • 取得两个Path之间的路径,用relativize(Path)方法.
      • 用startsWith(Path prefix),equals(Path path)等值比较或endsWith(Path suffix)来对路径进行比较.
    • NIO.2 Path和Java已有的File类
      • 新API中的类可以完全替代过去基于java.io.File的API.
      • java.io.File类中新增了toPath()方法,它可以马上把已有的File转化为新的Path
      • Path类中有toFile()方法,它可以马上把已有的path转化为File
  • 2.3 处理目录和目录树
    • 在目录中查找文件
      • Path dir = Paths.get("C:\\workspace\\java7developer"); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir,
        "*.properties")) {
        for (Path entry : stream) {
        System.out.println(entry.getFileName());
        }
        } catch (IOException e) {
        System.out.println(e.getMessage());
        }
    • 遍历目录树
      • java7支持整个目录树的遍历.
      • 关键方法:Files.walkFileTree(Path startingDir,FileVisitor<? super Path> visitor);
      • java7的设计者们已经提供了一个默认实现类,SimpleFileVisitor<T>.
      • public static void main(String[] args) throws IOException {
        Path startingDir = Paths
        .get("/Users/karianna/Documents/workspace/java7developer_code_trunk");
        Files.walkFileTree(startingDir, new FindJavaVisitor());
        }
        private static class FindJavaVisitor extends SimpleFileVisitor<Path> {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        if (file.toString().endsWith(".java")) {
        System.out.println(file.getFileName());
        }
        return FileVisitResult.CONTINUE;
        }
        }
      • 为了保证地柜等操作的安全性,walkFileTree方法不会自动跟随符号链接.如果你却是需要跟符号链接,就需要检查哪个属性并执行相应的操作.
  • 2.4 NIO.2的文件系统I/O
    • Files类
    • WatchService
    • NIO.2 API对原子操作的支持有很大改进,但涉及文件系统处理时,仍然主要依靠代码来提供保护.即使仕执行了一般的操作,也很可能会因为突然断网等情况的原因而出错.尽管API的某些方法还是会偶尔跑出各RuntimeException,但某些异常组昂矿可以由Files.exists(Path)这样的辅助方法来缓解.
    • 2.4.1 创建和删除文件
      • 如果在创建文件时要制定访问许可,不要忽略其父目录强加给该文件的umask限制或受限许可.比如说,你会发现即便你为新文件制定了rw-rw-rw许可,但由于目录的掩码,实际上文件最终的访问许可却是rw-r--r--
      • 删除文件,可以用Files.delete(Path)方法.
    • 2.4.2文件的复制和移动
      • Files.copy(Path source,Path target),复制文件时通常需要设置某些选项
        • REPLACE_EXISTING:覆盖即替换已有文件
        • COPY_ATTRIBUTES:复制文件属性
        • ATOMIC_MOVE:确保在连gia你的操作都成功,否则回滚
      • 移动和复制很像,都是用原子Files.move(Path source,Path target)方法完成的.
    • 2.4.3文件的属性
      • 文件属性控制者谁能对文件做什么.一般情况下,做什么许可包括能否读取,写入或执行文件,而由谁许可包括属主,群组或所有人.
      • 接口BasicFileAttributes定义了这个通用集.但工具类Files就能得到一些属性了
      • java7也支持跨文件系统的文件属性查看和处理功能.
      • 为了支持文件系统特定的文件属性,java7允许文件系统ing提供者实现FileAttributeView和BasicFileAttributes接口.
      • 在编写特定文件系统的代码时一定要小心.一定要确保你的逻辑和异常处理考虑到了代码在不同文件系统上运行的情况.
      • java7对符号链接的支持
        • 在写软件时,比如备份工具或部署脚本,你需要慎重考虑仕否应该跟随符号链接,NIO.2允许你做出选择.
        • java7对符号链接的支持遵循UNIX操作系统中实现的语义.
        • Files.isSymbolicLink(Path path);
        • Files.readSymbolicLink(Path path);
        • NIO.2 API默认会跟随符号链接.如果不想跟随,需要用LInkOption.NOFOLLOW_LINKS选项.
        • 如果你要读取符号链接本身的基本文件属性,应该调用:Files.readAttributes(target,BasicFileAttributes.class,LinkOption.NOFOLLOW_LINKS);
        • 符号链接仕java7对特定文件系统支持最常用的例子,API设计者也考虑到了未来对特定文件系统支持特性的扩展,比如量子加密文件系统.
    • 2.4.4 快速读写数据
      • 打开文件
        • java7可以直接用带缓冲区的读取器和写入器或输入输出流(为了和以前的java I/O代码兼容)打开文件.
        • 在处理String时,不要忘了查看它的字符编码.忘记设置字符编码(通过StandardCharsets类)可能导致不可预料的字符编码问题.
      • 简化读取和写入
        • 辅助类Files有两个辅助方法,用于读取文件中的全部行和全部字节.
    • 2.4.5 文件修改通知
      • 在java7中可以用java.nio.file.WatchService类检测文件或目录的变化.和很多持续轮询的设计一样,它也需要一个轻量的退出机制.
    • SeekableByteChannel
      • 非常重要,抽象的新API-用于数据的读写,使异步I/O成为现实的SeekableByteChannel.
      • java7引入SeekableByteChannel接口,是为了让开发人员能够改变字节通道的位置和大小.
      • JDK中有一个java.nio.channels.SeekableByteChannel接口的实现类--java.nio.channels.FileChannel.FileChannel类的寻址能力意味着开发人员可以更加灵活地处理文件内容.
  • 2.5 异步I/O操作.
    •     如果你还没接触过NIO通道,我们建议你去看看Ron Hitchens写的java NIO(O'Reilly ,2002)一书,你会从中获益匪浅.
    • java7中有三个新的异步通道:
      • AsynchronousFileChannel-用于文件I/O
      • AsynchronousSocketChannel-用于套接字I/O,支持超时.
      • AsynchronousServerSocketChannel-用于套接字接受异步连接.
    • 适用新的异步I/O API时,主要有两种形式,将来式回调式
    • 2.5.1 将来式
      • NIO.2 API的设计人员用将来式(future)这个术语来表明适用java.util.concurrent.Future接口. 通常会用Future get()方法(带或不带超时参数)在异步I/O操作完成时获取其结果.
      • public static void main(String[] args) {
        try {
        Path file = Paths.get("/usr/karianna/foobar.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(file); ByteBuffer buffer = ByteBuffer.allocate(100_000);
        Future<Integer> result = channel.read(buffer, 0); while (!result.isDone()) {
        ProfitCalculator.calculateTax();
        } Integer bytesRead = result.get();
        System.out.println("Bytes read [" + bytesRead + "]");
        } catch (IOException | ExecutionException | InterruptedException e) {
        System.out.println(e.getMessage());
        }
        }
      • 一定要注意,我们在这里用isDone()手工判断result是否结束.通常情况下,result或结束(主线程会继续执行),或等待后台I/O完成.
      • AsynchronousFileChannel的javadoc有解释:AsynchronousFileChannel会关联线程池,它的任务仕接收I/O处理事件,并分发给负责处理通道中I/O操作结果的结果处理器.跟通道中发起的I/O操作关联的结果处理器确保仕由线程池中的某个线程产生的.
      • 默认的线程池是由AsynchronousChannelGroup类定义的系统属性进行配置的.
    • 回调式
      • 基本思想是主线程会派一个侦查员CompletionHandler到独立的线程中执行I/O操作.这个侦查员将带着I/O操作的结果返回到主线程中,这个结果会触发它自己的completed或failed方法(你会重写这两个方法).
      • 在异步事件刚一成功或失败并需要马上采取行动时,一般会用回调式.
  • 2.6 Socket 和Channel 的整合
    • java7推出了NetworkChannel,把Socket和Channel结合到一起,让开发人员可以轻松应对.
    • NetworkChannel
      • 新接口java.nio.channels.NetworkChannel代表一个链接到网络套接字通道的映射.NetworkChannel的出现使得多播操作成为可能.
    • MulticastChannel
      • 像BitTorrent这样的对等网络程序一般都具备多播的功能.在java的早起版本中,虽然拼凑一下也能实现多播,但却没有很好的API抽象层.java7中的新接口MulticastChannel解决了这个问题.
      • 术语多播(或组播)表示一对多的网络通讯,通常用来指呆IP多播.其基本前提是将一个包发送到一个组播地址,然后网络对该包进行复制,分发给所有的接收端(注册到组播地址汇中).
      • 为了让新来的NetworkChannel加入多播组,java7提供了一个新接口java.nio.channels.MulticastChannel及其默认实现类DatagramChannel.也就是说可以轻松的对多播组发送和接收数据.
  • 小结
    • 在平台特性的支持下,java7可以任意穿梭于文件系统中,并能够处理大型目录结构.
第二部分:关键技术 第三章:依赖注入 知识注入:理解IoC和DI
  1. 依赖注入(控制反转的一种形式).简言之,适用DI技术可以让对象从别处得到依赖项,而不是由它自己来构造.Java DI的官方标准JSR-330,JSR-330的参考实现(RI)Guice 3---一个众所周知的轻量,精巧的DI框架.
  2. 对象关系映射(Object Relational Mapping,ORM)框架,比如Hibernate...
  3. 控制反转
    1. 使用IoC,这个"中心控制"的设计原则会被反转过来.调用者的代码处理程序的执行顺序,而程序逻辑则被封装在接收调用的子流程中.IoC也被成为好莱坞原则("不要给我们打电话,我们会打给你".好莱坞经纪人总是给人打电话,而不让别人打给他们!),其思想可以归结为会有另一段代码拥有最初的控制线程,并且由它来调用你的代码,而不是由你的代码调用它.
    2. 程序的主控被反转了,将控制权从应用逻辑中转移到GUI框架.
    3. IoC有几种不同的实现,包括工厂模式,服务定位器模式,当然,还有依赖注入.这一术语最初由Martin Fowler在"控制反转容器和依赖注入模式"中提出.
    4. 从字面上来看,IoC是指一种机制,使用这种机制的用例很多,实现方式也很多.DI只是其中一种具体用例的具体实现方式.但因为DI非常流行,所以人们经常误以为IoC就是DI,并且认为DI这种叫法比IoC更贴切.
  4. 依赖注入
    1. 依赖注入是IoC的一种特定形态,是指寻找依赖项的过程不在当前执行代码的直接控制之下.可以把IoC容器看做运行时环境.java中为依赖注入提供的容器有Guice,Spring和PicoContainer.
    2. 把依赖项注入对象的方法有很多种.可以用专门的DI框架,但也可以不这么做!显式地创建对象实例(依赖项)并把他们传入对象中也可以和框架注入做的一样好.
  5. 转成DI
    1. 把不用IoC的代码变成使用工厂(或服务定位器)模式的代码,再变成使用DI的代码.在这些转变之后有一个共同的关键技术,即面向接口编程.使用面向接口编程,甚至可以在运行时更换对象.
    2. 打个比方,DI框架就是把你的代码抱起来的运行时环境,在你需要时为你注入依赖项.DI框架的优势在于它可以随时随地为你的代码提供依赖项.因为框架中有IoC容器,在运行时,你的代码需要的所有依赖项都会在哪里准备好.
    3. 尽管JSR-330注解可以在方法上注入依赖项,但通常志勇于构造方法或set方法中.
    4. 新的DI标准化方式(JSR-330)就是要解决这个问题.它对大多数java DI框架的核心能力做了很好的汇总.
  6. java中标准化的DI
    1. JSR-330(javax.inject)规范.倡导java se的DI标准化
    2. java ee中的DI标准化情况如何?java企业应用从JEE 6开始构建了自己的依赖注入体系(即CDI),由JSR-299(java ee平台中的上下文及依赖注入)规范确定,你可在http://jcp.org/中搜索JSR-299了解其详细信息.简言之,JSR-299构建在JSR-330基础之上,旨在为企业应用提供标准化的配置.
    3. 警告:实际上,代码迁移并不容易.一旦你的代码用到了仅由特定ID框架支持的特性,就不太可能拜托这一框架了.尽管javax.inject包提供了常用DI功能的子集,但是你可能需要适用更高级的DI特性.正如你想象的那样,对于哪些特性应该作为通用的标准也是众说纷纭,很难统一.虽然现状不尽如人意,但java毕竟朝DI框架的标准化方向迈出了一步.
    4. javax.inject包只是提供了一个接口和几个注解类型,这些都会被遵循JSR330标准的各种DI框架实现.也就是说,除非你在创建与JSR-330兼容的IoC容器(如果如此,想你致敬),通常不用自己实现它们.
    5. javax.inject包:这个包志明了获取对象的一种方式,与传统的构造方法,工厂模式和服务器定位器墨仕(比如JNDI)等相比,这种方式的可重用性,可测试性和可维护性都的到了极大提升.这种方式成为依赖注入,对于大多数非小型应用程序都很有帮助.
    6. @Inject注解
      1. 规范中规定向构造器注入的参数数量为0或多个,所i在不含参数的构造器上使用@Inject也是合法的.警告:因为JRE无法决定构造器注入的优先级,所以规范中规定类中只能有一个构造器带@Inject注解.
      2. @Inject注解方法,运行时可注入参数的数量也可以是0或多个.但适用参数注入的方法不能声明为抽象方法,也不能声明其自身的类型参数.
      3. 提示:向构造器中注入的通常仕类中必需的依赖项,而对于非必需的依赖项,通常仕在set方法上注入.比如已经给出了默认值的属性就是非必需的依赖项.这一最佳时间已经成了惯例.
      4. 可以直接在属性上注入,只要他们不是final,虽然这样做简单直接,但你最好不要用,因为这样可能会让单元测试更加困难.
    7. @Qualifier注解
      1. 支持JSR-330规范的框架要用注解@Qualifier限定(标识)要注入的对象.
      2. 如果你用过由框架实现的限定符,应该指导要创建一个@Qualifier实现必需遵循如下规则:
        1.     必需标记为@Qualifier和@Retention(RUNTIME),以确保该限定注解在运行时一直有效.
        2. 通常还应该加上@Documented注解,这样该实现就能加到API的公共javadoc中了.
        3. 可以有属性
        4. @Target注解可以限定其使用范围;比如将其适用范围限制为属性,而不是限定为属性的默认值和方法中的参数.
      3. JSR-330规范中要求所有IoC容器都要提供一个默认的@Qualifier注解:@Named
    8. @Named注解
      1. @Named仕一个特别的@Qualifier注解,借助@Named可以用名字标明要注入的对象.将@Named和@Inject一起使用,符合制定名称并且类型正确的对象会被注入
    9. @Scope注解
      1. @Scope注解用于定义注入器(即IoC容器)对注入对象的重用方式.JSR-330规范中明确了如下集中默认行为.
        1.      如果没有声明任何@Scope注解接口的实现,注入器应该创建注入对象并且仅使用该对象一次.
        2. 如果声明了@Scope注解接口的实现,那么注入对象的生命周期由所声明的@Scope注解实现决定.
        3. 如果注入对象在@Scope实现中要由多个线程使用,则需要保证注入对象的线程安全性.  
        4. 如果某个磊尚声明了多个@Scope注解,或声明了不受支持的@Scope注解,IoC容器应该抛出异常.
      2. DI框架管理注入对象的生命周期时不会超出这些默认行为划定的界限.因为大家工人的通用@Scope实现只有@Singleton一个,所以JSR-330规范中仅确定了它这么一个标准的生命周期注解.
    10. @Singleton注解
      1. 在需要注入一个不会改变的对象时,就要用@Singleton
      2. 请谨慎使用单例模式,因为它有时候会变成反模式.
      3. 大多数DI框架都将@Singleton作为注入对象的默认生命周期,无需显式声明.
    11. 接口Prvoider<T>
      1. 如果你想对由DI框架注入代码中的对象拥有更多的控制权,可以要求DI框架将Prvoider<T>接口实现注入对象.控制对象的好处在于:
        1.      可以获取该对象的多个实例.
        2. 可以延迟获取该对象(延迟加载)
        3. 可以打破循环依赖
        4. 可以定义作用域,能在比整个被加载的应用小的作用域中查找对象. 
  7. java中的DI参考实现:Guice 3:这一节看晕了,待以后琢磨
    1. Guice 3是JSR-330规范的完整参考实现.   "为了让注入器创建对象关系图,需要创建声明各种绑定关系的模块,其中绑定是用来明确要注入的具体实现类的." 
    2. 水手绳结:Guice的各种绑定
  8. 小结:IoC是个复杂的概念.但通过对工厂和服务定位器模式的探讨你能了解基本IoC实现仕如何工作的.工厂模式有助于你理解DI以及DI给代码带来的好处.JSR-330不仅仅是统一DI通用功能的重要标准,它还提供了你需要了解的幕后规则及限制.通过研究标准DI注解集,你会更加欣赏不同DI框架对规范的实现,因而可以更有效地使用他们.
第四章:现代并发
  • 并发理论简介:系统设计和实现中"设计原则"的影响以及其中最主要的两个原则:安全性和活跃度.
    • 解释java线程模型:java线程模型建立在两个基本概念之上
      • 共享的,默认可见的可变状态
      • 抢占式线程调度
      • 思考这两个概念:
        • java基于线程和锁的并发非常底层,并且一般都比较难用.为了解决这个问题,java 5引入了一组并发类库java.util.concurrent.
    • 设计理念
      • Doug Lea在创造他那里程碑式的作品java.util.concurrent时列出了下面这些最重要的设计原则:
        • 安全性(也叫做并发类型安全性)
          • 安全性是指不管同时发生多少操作都能确保对象保持自相一致.如果一个对象系统具备这以特性,那它就是并发类型安全的.
          • 并发类型安全的概念跟对象类型安全一样,但它用在更复杂的环境下.在这样的环境中,其他线程在不同CPU内核上同时操作同一对象.
          • 保证安全: 保证安全的策略之一是在处于非一致状态时决不能从非私有方法中返回,也决不能调用任何非私有方法,而且也决不能调用其他任何对象中的方法.如果把这个策略跟某种对非一致对象的保护方法(比如同步锁或临界区)结合起来,就可以 保证系统是安全的.  
        • 活跃度
          • 在一个活跃的系统中,所有做出尝试的活动最终或取得进展,或者失败.     
        • 性能
          • 暂时可以看成仕测量系统用给定资源能做多少工作的办法.
        • 重用性 
          •     用可重用工具集(比如:java.util.concurrent),并把不可重用的应用代码构建在工具集之上是一种可行的办法.
      • 这些原则如何以及为何会相互冲突
        • 安全性与活跃度相互对立--安全性是为了确保坏事不会发生,而活跃度要求见到进展.
        • 可重用的系统倾向于对外开放其内核,可这会引发安全问题.
        • 一个安全但编写方式幼稚的系统性能通常都不会太好,因为里面一般会用大量的锁来保证安全性.
        • 以上问题实战技巧,如下:
          • 尽可能限制子系统之间的通信.隐藏数据对安全性非常有帮助. 
          • 尽可能保证子系统内部结构的确定性.比如说,即便子系统会以并发的,非确定性的方式进行交互,子系统内部的设计也应该参照线程和对象的静态知识.
          • 采用客户端应用必须遵守的策略方针.这个技巧虽然强大,却依赖于用户应用程序的合作程度,并且如果某个糟糕的应用不遵守规则,便很难发现问题所在.
          • 在文档中记录所要求的行为.这是最逊的办法,但如果代码要部署在非常通用的环境中,就必须采用这个办法.
      • 系统开销之源
        • 并发系统中的系统开销是与生俱来的,来自:
          • 锁与检测
          • 环境切换的次数
          • 线程的个数
          • 调度
          • 内存的局部性
            • 局部性值得是程序行为的一种规律:在程序运行中的短时间内,程序访问数据位置的集合限于局部范围.局部性有两种基本形式:时间局部性与空间局部性.时间局部性是指反复访问同一个位置的数据;空间局部性指的是反复访问相邻的数据.-译者注  
          • 算法设计 :推荐书籍Thomas H.Corman等人编著的<算法导论>(MIT,2009) 和Steven Skiena写的<算法设计手册>(Springer-Verlag,2008)
        • 假设有一个基本事务处理系统.构建这种程序有个简单的标准办法,就是先将业务流程的 不同环节对应到应用程序的不同阶段,然后用不同的线程池表示不同的应用阶段,每个线程池逐一接收公主偶像,在对每个工作项进行一系列的处理后,交给下一个线程池.通常来说,好的设计会让每个线程池所做的处理集中在一个特定功能区内.
      • 块结构并发(java5之前)
        • 同步与锁
          • 临界区的概念
          • 同步与锁的基本事实:
            • 被锁定的对象数组中的单个对象不会被锁定.
            • 同步方法可以视同为包含整个方法的同步(this) {...}代码块(但要注意它们的二进制码 表示是不同的).
            • 如果要锁定一个类对象,请慎重考虑是用显示锁定,还是用getClass(),两种方式对子类的影响不同.
            • 内部类的同步是独立于外部类的(要明白为什么会这样,请记住内部类是如何实现的).
            • synchronized并不是方法签名的组成部分,所以不能出现在接口的方法声明中.
            • 非同步的方法不查看或关心任何锁的状态,而且在同步方法运行时它们仍能继续运行.
            • Java的线程锁是可重入的.也就是说持有锁的线程在遇到同一个锁的同步点(比如一个同步方法调用同一个类内的另一个同步方法)时是可以继续的.
          • 线程的状态模型
          • 完全同步对象
            • 如果一个类遵从下面的所有规则,就可以认为它是线程安全并且活跃的:一个满足下面所有条件的类就是完全同步类.
              • 所有域在任何构造方法中的初始化都能达到一致的状态.
              • 没有公共域
              • 从任何非私有方法返回后,都可以保证对象实例处于一致的状态(假定调用方法时状态是一致的).
              • 所有方法经证明都可在有限时间内终止.
              • 所有方法都是同步的.
              • 当处于非一致状态时,不会调用其他实例的方法.
              • 当处于非一致状态时,不会调用非私有方法.
          • 死锁
            • 有一个处理死锁的技巧,就是在所有县城中都以相同的顺序获取线程锁.
            • 就完全同步对象方式而言,要防止这种死锁出现是因为代码破坏了状态一致性规则.
          • 为什么是synchronized
            • 以前,并发变成过去主要考虑如果分享CPU时间,县城门在单核上轮流上位,相互调换.现在做的应该把多个线程在同一物理时刻运行在不同核心(并且很可能会操作共享的数据)的情况也考虑在内.
            • 在synchronized代码块(或方法)执行完之后,对被锁定对象所做的任何修改全部都会在线程锁释放之前刷回到主内存中.
            • 当进入一个同步的代码块,得到线程锁之后,对被锁定对象的任何修改都是从主内存中读出来的,所i在锁定区域代码开始执行之前,持有锁的线程就和锁定对象朱内存中的视图同步了.
          • 关键子volatile
            • 是一种简单的对象域同步处理办法,包括原始类型.
            • 一个volatile域需遵循一下规则:
              • 线程所见的值在使用之前总会从朱内存中再读出来.
              • 线程所写的值总会在指令完成之前被刷回到主内存中.
            • 付出的代价是每次访问都要额外刷依次内存,还有就是volatile变量不会引入线程锁,所以使用volatile变量不可能发生死锁.
            • volatile变量是真正线程安全的.但只有写入时不依赖当前状态(读取的状态)的变量才应该声明为volatile变量.
          • 不可变性
            • 构建器模式:它由两部分组成,一个是实现了构建器泛型接口的内部静态类,另一个是构建不可变类实例的私有构造方法.内部静态类是不可变类的构建器,开发人员只能通过它获取不可变类的新实例.比较常见的实现方式是让构建器类拥有于不可变类一模一样的域,但构建器的域是可修改的.
            • 关键字final仅对其直接只想的对象有用.也就是说final引用可以指向带有非final域的对象.
            • 有时候只用不可变对象开发效率不行,因为每次修改对象状态就需要构建一个新对象.
        • 现代并发应用程序的构件
          • 原子类:java.util.concurrent.atomic
            • 语义基本上和volatile一样,致使封装在一个API里了,这个API包含为操作提供的适当的原子(要么不做,要么就全做)方法.
            • 常见的用法是实现序列号机制,在AtomicInteger或AtomicLong上用原子操作getAndIncrement()方法.要做序列号,该类应该有个nextId()方法,每次调用时肯定能返回一个唯一并且完全增长的数值.这和数据库里的序列号的概念很像.
            • 原子类不是从有相似名称的类继承而来的,所以AtomicBoolean不能当Boolean用,AtomicInteger也不是Integer,虽然它确实扩展了Number.
          • 线程锁:java.util.concurrent.locks
            • 块结构同步方式基于锁这样一个简单的概念.这种方式有几个缺点:
              • 锁只有一种类型
              • 对被锁住对象的所有同步操作都是一样的作用.
              • 在同步代码块或方法开始时取得线程锁.
              • 在同步代码块或方法结束时释放线程锁.
              • 线程或者得到锁,或者阻塞--没有其他可能.
            • 有两个实现类
              • ReentrantLock---本质上跟用在同步块上那种锁是一样的,但它要稍微灵活点
              • ReentrantReadWriteLock---在需要读取很多线程而写入很少线程时,用它性能会更好.
              • 用锁时带上try...finally,把lock()放在try...finally块中(释放也在这里)的模式是另外一个好用的小工具.在跟块结构并发相似的情景中它同样很好用.而另一方面,如果需要传递Lock对象,比如从一个方法中返回,则不能用这个模式.适用Lock对象可能要比块结构方式强大得多,但有时用它们很难设计出完善的锁定策略.
              • 对付死锁的策略有很多,但你应该特别注意一个不起任何作用的策略.用带有超时机制的Lock.tryLock()替换了无条件的锁.通过这种办法可以为其他线程提供得到线程锁的机会,从而去除死锁.但这种方法死锁问题并没有真正解决.
            • CountDownLatch:锁存器
              • 是一种简单的同步模式,这种模式允许线程在通过同步屏障之前做些少量的准备工作.为了达到这种效果,在构建新的CountDownLatch实例时要给它提供一个int值(计数器).


这篇关于Java程序员修炼之道 人民邮电出版社 吴海星译的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程