Browsed by
作者:Halo Pan

数据画家,闲云匠人,全栈程序员,智能实践者……
《宏观经济学》阅读总结

《宏观经济学》阅读总结

《宏观经济学》版本众多,最近看的是奥利维尔•布兰查德的版本。这是一本教科书,应该算是系统性学习金融知识的一个起点。
之前上学时上课都没有笔记的习惯,阅读一本教科书写一份总结,还真不知道能写一些什么。
社科类畅销书最近也看过一点,之前看得更多的是教材类的书籍。现在又看一本教材,感觉和畅销书最大的一点区别就是“没有观点”。畅销书为了迎合主流读者的口味,贯穿整本书的一般都带有作者的主观观点,并通过不断的例证、阐述佐证自己的观点;而教材主要是罗列知识点和全面的多角度的分析,可以帮助读者系统性的了解并掌握一门知识。
读畅销书或多或少都会收到作者的一些主观意识的引导,而教材一般的分析都比价中正一些。总结一下,以后还是应该以阅读教材为主,畅销书只能作为调节节奏的辅助。
再说说这本教材,只是初读了第一遍。这本书分别分析短期、中期、长期的宏观经济规律,并以此为内容组织的架构。分别分析短期、中期、长期中决定经济总产出水平的因素。
在短期(几年时间)内,经济总产出的变动主要靠需求的拉动,需求的改变可能是消费者信心或者是其它因素的改变,可能导致产出减少,也有可能导致产出增多;在中期(十年时间)内,供给因素,即资本存量,技术水平和劳动力规模决定经济总产出水平;在长期(几十年或者更长)内,教育系统,储蓄率和政府素质决定了经济总产出。这本教材也基本是按照这个思路来组织内容的。
这本教材技术部分主要是IS-LM,AD-AS,索洛模型等几个主要的模型,而这几个模型主要应该注意各种因素对模型的影响。利用这些模型分析财政货币政策的影响应该是学习《宏观经济学》的主要目的。例如货币政策会在短期内影响产出,但在中期和长期内是无效的,更快的货币增长会导致通货膨胀率以同样的速度增长。而财政政策在短期,中期都会对产出有影响。更高的赤字有利于短期内提高产出,但在中期,赤字会降低资本积累进而减少产出。
教材知识点很多,也无法在一篇总结里面一一列举,但是这本教材至少是还需要细读一遍的,这样才能对这些知识点有更深的印象,学习的内容才能比较有系统性。
教材难啃,学习路难,自勉之。

《宇宙的结构》阅读总结

《宇宙的结构》阅读总结

一本烧脑书。
这本书通篇没有物理公式和数学公式,虽说算一本科普物理书,但阅读起来还是挺难懂的。
第一部分主要讲相对论,狭义和广义的。第二部分讲时间的概念,通过熵增加的方向定义时间。第三部分讲宇宙大爆炸理论。第四部分涉及弦轮等新理论。第五部分是讨论时空穿越一些猜想。
自己算是特业余的天文爱好者,从小也对宇宙充满了好奇。一开始以为这是一本科普读物,阅读一下调教心情。结果读起来才知道这本书虽然门槛不高,但要理解其中讲的东西,还是需要努力思考的。
能把一本难懂的书坚持读完(一二三部分细读,四五部分粗读),还是有一些内在原因的:锻炼自己的思维。
目前世面上几乎所有的技术类书籍,形式上都是指导书,很详细的告诉读者要这样做,然后那样做,最后就可以实现什么什么了。但不会详细讲解为什么要这样做。不是说技术类书籍这样的形式不好,其实IT类技术更新快,偏实现,偏工程,这样的形式挺适合工程师阅读的,自己也读了很多这样的技术书,能快速上手工程实现。长期技术积累会让工程师了解很多细节,成为优秀的执行者,但较难成为思考者。
这本书不是技术书,是物理学家写的“科普”书,通过阅读可以体会一下科学家的思考方式。举个栗子:第一部分讲时空观念的时候,先详细讲解牛顿的“绝对空间”(跟着书的思维,我觉得讲得挺对的),然后讲其他物理学家对这一概念的质疑(我觉得也是对的),马赫对时空观念的改进,最后才引入了爱因斯坦的相对论。
其实物理学上的理论没有对错,都是某个时代的物理学家在当时科学实验限制的条件下,通过观察和思考提出的能让规律可解释的观念。大家(物理学家)觉得你提出的理论能很好的解释事物运动或者变化的规律,就会认为你的理论是“对”的。然后随着观察手段的提高,又观察到了其他不符合这一理论的运动或者变化,就需要另外一个天才物理学家通过思考(真的就是思考,实验物理滞后理论物理好多年),想出一个新的理论。
物理理论的完善就是整个人类进步的过程,思考者推动着物理的发展。

《打开量化投资的黑箱》阅读总结

《打开量化投资的黑箱》阅读总结

量化交易对于非技术流或者头疼数学的投资者来说,确实算是一个黑箱。但是如果有一定的数学建模的基础和经验,那么量化投资应该是一个很容易理解的概念。输入数据到量化系统,经过数学模型的计算,最终得到交易策略,其实就是常见的数据分析流程,只是因为涉及到钱,所以关注的人比较多,而这些人里大部分都没有数据经验,所以就显得神秘了。

再具体说说这本书,确实打开了量化投资的黑箱,然后告诉你,黑箱里面还有5个或者更多的小黑箱……

  1. 数据处理
  2. 阿尔法模型
  3. 风险模型
  4. 成本模型
  5. 交易系统

具体可以看看书里面的图。其实量化的核心:模型在每个模块都会有涉及到。书中只是用描述性的语言举例介绍了一些简单的模型,并没有用数学公式进行抽象。考虑到这本书的目标阅读群体,这样的表述是对入门者比较友好的。总体来说这是一本很好的入门书,如果想在量化模型上有更深入的了解,还是需要从金融和数学这样的基础开始的。

这本书目前给我最大的帮助其实就是量化交易系统的划分。上面说的5个模块,就是典型的系统解耦合啊!架构师的最爱👍!之前只知道量化交易系统应该是一个在线系统,但是这个系统如何设计,由于没有一点业务经验,所以也没什么头绪,但是这本书里面的模块划分,让人豁然开朗。每个模块都有独立的功能,可以设计成独立的子系统(微服务),最后在交易系统里融合。当然实施时肯定会遇到各种问题,但是这样模块划分的微服务架构本身不会有太大的问题,方向不会差太远。

书中也描述性的介绍了一些简单的模型,各个模块里的模型都有。入门时可以尝试实现一下这样简单的模型,从数学建模,到代码实现,再到实际数据计算。再慢慢根据实际状况对这些模型进行修正和优化。哈哈,一套量化交易系统就慢慢成型了。技术上无坑,就是钱有风险😄

Scala 语法:implicit

Scala 语法:implicit

Implicit是暗示的意思,这个关键字在Scala中给一个类增加一些方法,用于接收不同类型的对象。

Java IO: BIO, NIO, AIO

Java IO: BIO, NIO, AIO

BIO, NIO, AIO,本身的描述都是在Java语言的基础上的。
而描述IO,我们需要从三个层面:
1. 编程语言
2. 实现原理
3. 底层基础

从编程语言层面

BIO, NIO, AIO以Java的角度理解
– BIO,同步阻塞式IO,简单理解:一个连接一个线程
– NIO,同步非阻塞IO,简单理解:一个请求一个线程
– AIO,异步非阻塞IO,简单理解:一个有效请求一个线程

BIO

在JDK1.4之前,用Java编写网络请求,都是建立一个ServerSocket,然后,客户端建立Socket时就会询问是否有线程可以处理,如果没有,要么等待,要么被拒绝。即:一个连接,要求Server对应一个处理线程。

public class PlainEchoServer {
  public void serve(int port) throws IOException {
    final ServerSocket socket = new ServerSocket(port); //Bind server to port
    try {
      while (true) {
        //Block until new client connection is accepted
        final Socket clientSocket = socket.accept();
        System.out.println(\"Accepted connection from \" + clientSocket);
        //Create new thread to handle client connection
        new Thread(new Runnable() {
          @Override
          public void run() {
            try {
              BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
              PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
              //Read data from client and write it back
              while (true) {
                writer.println(reader.readLine());
                writer.flush();
              }
            } catch (IOException e) {
              e.printStackTrace();
              try {
                clientSocket.close();
              } catch (IOException ex) {
                // ignore on close
              }
            }
          }
        }).start();
        //Start thread
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

NIO

在Java里的由来,在JDK1.4及以后版本中提供了一套API来专门操作非阻塞I/O,我们可以在java.nio包及其子包中找到相关的类和接口。由于这套API是JDK新提供的I/O API,因此,也叫New I/O,这就是包名nio的由来。这套API由三个主要的部分组成:缓冲区(Buffers)、通道(Channels)和非阻塞I/O的核心类组成。在理解NIO的时候,需要区分,说的是New I/O还是非阻塞IO,New I/O是Java的包,NIO是非阻塞IO概念。这里讲的是后面一种。
NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题:在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。
NIO基于Selector,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

public class PlainNioEchoServer {
  public void serve(int port) throws IOException {
    System.out.println(\"Listening for connections on port \" + port);
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    ServerSocket ss = serverChannel.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    //Bind server to port
    ss.bind(address);
    serverChannel.configureBlocking(false);
    Selector selector = Selector.open();
    //Register the channel with the selector to be interested in new Client connections that get accepted
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
      try {
        //Block until something is selected
        selector.select();
      } catch (IOException ex) {
        ex.printStackTrace();
        //handle in a proper way
        break;
      }
      //Get all SelectedKey instances
      Set readyKeys = selector.selectedKeys();
      Iterator iterator = readyKeys.iterator();
      while (iterator.hasNext()) {
        SelectionKey key = (SelectionKey) iterator.next();
        //Remove the SelectedKey from the iterator
        iterator.remove();
        try {
          if (key.isAcceptable()) {
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            //Accept the client connection
            SocketChannel client = server.accept();
            System.out.println(\"Accepted connection from \" + client);
            client.configureBlocking(false);
            //Register connection to selector and set ByteBuffer
            client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, ByteBuffer.allocate(100));
          }
          //Check for SelectedKey for read
          if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer output = (ByteBuffer) key.attachment();
            //Read data to ByteBuffer
            client.read(output);
          }
          //Check for SelectedKey for write
          if (key.isWritable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer output = (ByteBuffer) key.attachment();
            output.flip();
            //Write data from ByteBuffer to channel
            client.write(output);
            output.compact();
          }
        } catch (IOException ex) {
          key.cancel();
          try {
            key.channel().close();
          } catch (IOException cex) {
          }
        }
      }
    }
  }
}

AIO

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:
+ AsynchronousSocketChannel
+ AsynchronousServerSocketChannel
+ AsynchronousFileChannel
+ AsynchronousDatagramChannel
其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。

public class PlainNio2EchoServer {
  public void serve(int port) throws IOException {
    System.out.println(\"Listening for connections on port \" + port);
    final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
    InetSocketAddress address = new InetSocketAddress(port);
    // Bind Server to port
    serverChannel.bind(address);
    final CountDownLatch latch = new CountDownLatch(1);
    // Start to accept new Client connections. Once one is accepted the CompletionHandler will get called.
    serverChannel.accept(null, new CompletionHandler() {
      @Override
      public void completed(final AsynchronousSocketChannel channel, Object attachment) {
        // Again accept new Client connections
        serverChannel.accept(null, this);
        ByteBuffer buffer = ByteBuffer.allocate(100);
        // Trigger a read operation on the Channel, the given CompletionHandler will be notified once something was read
        channel.read(buffer, buffer, new EchoCompletionHandler(channel));
      }

      @Override
      public void failed(Throwable throwable, Object attachment) {
        try {
          // Close the socket on error
          serverChannel.close();
        } catch (IOException e) {
          // ingnore on close
        } finally {
          latch.countDown();
        }
      }
    });
    try {
      latch.await();
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }

  private final class EchoCompletionHandler implements CompletionHandler {
    private final AsynchronousSocketChannel channel;

    EchoCompletionHandler(AsynchronousSocketChannel channel) {
      this.channel = channel;
    }

    @Override
    public void completed(Integer result, ByteBuffer buffer) {
      buffer.flip();
      // Trigger a write operation on the Channel, the given CompletionHandler will be notified once something was written
      channel.write(buffer, buffer, new CompletionHandler() {
        @Override
        public void completed(Integer result, ByteBuffer buffer) {
          if (buffer.hasRemaining()) {
            // Trigger again a write operation if something is left in the ByteBuffer
            channel.write(buffer, buffer, this);
          } else {
            buffer.compact();
            // Trigger a read operation on the Channel, the given CompletionHandler will be notified once something was read
            channel.read(buffer, buffer, EchoCompletionHandler.this);
          }
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
          try {
            channel.close();
          } catch (IOException e) {
            // ingnore on close
          }
        }
      });
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
      try {
        channel.close();
      } catch (IOException e) {
        // ingnore on close
      }
    }
  }
}

实现原理

说道实现原理,还要从操作系统的IO模型上了解
按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。
如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

收到操作系统的IO模型,又不得不提select/poll/epoll/iocp。
可以理解的说明是:在Linux 2.6以后,java NIO的实现,是通过epoll来实现的,这点可以通过jdk的源代码发现。而AIO,在windows上是通过IOCP实现的,在linux上还是通过epoll来实现的。
这里强调一点:AIO,这是I/O处理模式,而epoll等都是实现AIO的一种编程模型;换句话说,AIO是一种接口标准,各家操作系统可以实现也可以不实现。在不同操作系统上在高并发情况下最好都采用操作系统推荐的方式。Linux上还没有真正实现网络方式的AIO。

底层基础

在windows上,AIO的实现是通过IOCP来完成的,看JDK的源代码,可以发现

WindowsAsynchronousSocketChannelImpl

看实现接口:

implements Iocp.OverlappedChannel

再看实现方法:里面的read0/write0方法是native方法,调用的jvm底层实现。

在linux上,AIO的实现是通过epoll来完成的,看JDK源码,可以发现,实现源码是:

UnixAsynchronousSocketChannelImpl

看实现接口:

implements Port.PollableChannel

这是与windows最大的区别,poll的实现,在linux2.6后,默认使用epoll。

安装完MySQL后必须调整的10项配置

安装完MySQL后必须调整的10项配置

[转载]安装完MySQL后必须调整的10项配置

转载译文
英文原文

当我们被人雇来监测MySQL性能时,人们希望我们能够检视一下MySQL配置然后给出一些提高建议。许多人在事后都非常惊讶,因为我们建议他们仅仅改动几个设置,即使是这里有好几百个配置项。这篇文章的目的在于给你一份非常重要的配置项清单。
我们曾在几年前在博客里给出了这样的建议,但是MySQL的世界变化实在太快了!

写在开始前…

  • 即使是经验老道的人也会犯错,会引起很多麻烦。所以在盲目的运用这些推荐之前,请记住下面的内容:
  • 一次只改变一个设置!这是测试改变是否有益的唯一方法。
  • 大多数配置能在运行时使用SET GLOBAL改变。这是非常便捷的方法它能使你在出问题后快速撤销变更。但是,要永久生效你需要在配置文件里做出改动。
  • 一个变更即使重启了MySQL也没起作用?请确定你使用了正确的配置文件。请确定你把配置放在了正确的区域内(所有这篇文章提到的配置都属于 [mysqld])
  • 服务器在改动一个配置后启不来了:请确定你使用了正确的单位。例如,innodb_buffer_pool_size的单位是MB而max_connection是没有单位的。
  • 不要在一个配置文件里出现重复的配置项。如果你想追踪改动,请使用版本控制。
  • 不要用天真的计算方法,例如”现在我的服务器的内存是之前的2倍,所以我得把所有数值都改成之前的2倍“。

基本配置

你需要经常察看以下3个配置项。不然,可能很快就会出问题。
innodb_buffer_pool_size:这是你安装完InnoDB后第一个应该设置的选项。缓冲池是数据和索引缓存的地方:这个值越大越好,这能保证你在大多数的读取操作时使用的是内存而不是硬盘。典型的值是5-6GB(8GB内存),20-25GB(32GB内存),100-120GB(128GB内存)。
innodb_log_file_size:这是redo日志的大小。redo日志被用于确保写操作快速而可靠并且在崩溃时恢复。一直到MySQL 5.1,它都难于调整,因为一方面你想让它更大来提高性能,另一方面你想让它更小来使得崩溃后更快恢复。幸运的是从MySQL 5.5之后,崩溃恢复的性能的到了很大提升,这样你就可以同时拥有较高的写入性能和崩溃恢复性能了。一直到MySQL 5.5,redo日志的总尺寸被限定在4GB(默认可以有2个log文件)。这在MySQL 5.6里被提高。
一开始就把innodb_log_file_size设置成512M(这样有1GB的redo日志)会使你有充裕的写操作空间。如果你知道你的应用程序需要频繁的写入数据并且你使用的时MySQL 5.6,你可以一开始就把它这是成4G。
max_connections:如果你经常看到‘Too many connections’错误,是因为max_connections的值太低了。这非常常见因为应用程序没有正确的关闭数据库连接,你需要比默认的151连接数更大的值。max_connection值被设高了(例如1000或更高)之后一个主要缺陷是当服务器运行1000个或更高的活动事务时会变的没有响应。在应用程序里使用连接池或者在MySQL里使用进程池有助于解决这一问题。

InnoDB配置

从MySQL 5.5版本开始,InnoDB就是默认的存储引擎并且它比任何其他存储引擎的使用都要多得多。那也是为什么它需要小心配置的原因。
innodb_file_per_table:这项设置告知InnoDB是否需要将所有表的数据和索引存放在共享表空间里(innodb_file_per_table = OFF) 或者为每张表的数据单独放在一个.ibd文件(innodb_file_per_table = ON)。每张表一个文件允许你在drop、truncate或者rebuild表时回收磁盘空间。这对于一些高级特性也是有必要的,比如数据压缩。但是它不会带来任何性能收益。你不想让每张表一个文件的主要场景是:有非常多的表(比如10k+)。
MySQL 5.6中,这个属性默认值是ON,因此大部分情况下你什么都不需要做。对于之前的版本你必须在加载数据之前将这个属性设置为ON,因为它只对新创建的表有影响。
innodb_flush_log_at_trx_commit:默认值为1,表示InnoDB完全支持ACID特性。当你的主要关注点是数据安全的时候这个值是最合适的,比如在一个主节点上。但是对于磁盘(读写)速度较慢的系统,它会带来很巨大的开销,因为每次将改变flush到redo日志都需要额外的fsyncs。将它的值设置为2会导致不太可靠(unreliable)因为提交的事务仅仅每秒才flush一次到redo日志,但对于一些场景是可以接受的,比如对于主节点的备份节点这个值是可以接受的。如果值为0速度就更快了,但在系统崩溃时可能丢失一些数据:只适用于备份节点。
innodb_flush_method: 这项配置决定了数据和日志写入硬盘的方式。一般来说,如果你有硬件RAID控制器,并且其独立缓存采用write-back机制,并有着电池断电保护,那么应该设置配置为O_DIRECT;否则,大多数情况下应将其设为fdatasync(默认值)。sysbench是一个可以帮助你决定这个选项的好工具。
innodb_log_buffer_size: 这项配置决定了为尚未执行的事务分配的缓存。其默认值(1MB)一般来说已经够用了,但是如果你的事务中包含有二进制大对象或者大文本字段的话,这点缓存很快就会被填满并触发额外的I/O操作。看看Innodb_log_waits状态变量,如果它不是0,增加innodb_log_buffer_size。

其他设置

query_cache_size: query cache(查询缓存)是一个众所周知的瓶颈,甚至在并发并不多的时候也是如此。 最佳选项是将其从一开始就停用,设置query_cache_size = 0(现在MySQL 5.6的默认值)并利用其他方法加速查询:优化索引、增加拷贝分散负载或者启用额外的缓存(比如memcache或redis)。如果你已经为你的应用启用了query cache并且还没有发现任何问题,query cache可能对你有用。这是如果你想停用它,那就得小心了。
log_bin:如果你想让数据库服务器充当主节点的备份节点,那么开启二进制日志是必须的。如果这么做了之后,还别忘了设置server_id为一个唯一的值。就算只有一个服务器,如果你想做基于时间点的数据恢复,这(开启二进制日志)也是很有用的:从你最近的备份中恢复(全量备份),并应用二进制日志中的修改(增量备份)。二进制日志一旦创建就将永久保存。所以如果你不想让磁盘空间耗尽,你可以用 PURGE BINARY LOGS 来清除旧文件,或者设置 expire_logs_days 来指定过多少天日志将被自动清除。
记录二进制日志不是没有开销的,所以如果你在一个非主节点的复制节点上不需要它的话,那么建议关闭这个选项。
skip_name_resolve:当客户端连接数据库服务器时,服务器会进行主机名解析,并且当DNS很慢时,建立连接也会很慢。因此建议在启动服务器时关闭skip_name_resolve选项而不进行DNS查找。唯一的局限是之后GRANT语句中只能使用IP地址了,因此在添加这项设置到一个已有系统中必须格外小心。

总结

当然还有其他的设置可以起作用,取决于你的负载或硬件:在慢内存和快磁盘、高并发和写密集型负载情况下,你将需要特殊的调整。然而这里的目标是使得你可以快速地获得一个稳健的MySQL配置,而不用花费太多时间在调整一些无关紧要的MySQL设置或读文档找出哪些设置对你来说很重要上。

也谈谈全栈工程师

也谈谈全栈工程师

[转载]也谈谈全栈工程师

今天看到一篇关于全栈工程师的文章,觉得挺不错的,很符合自己的认知,分享一下
原文地址:四火的唠叨


纵使目标再大,人的精力有限,于我来说,早些时候远大目标隐约是“成功的软件工程师”这个样子,但是目标是需要逐渐细化的。这些年我渐渐对自己的定位和未来有了一个清晰一点的认识。确实我有很强的观点,觉得软件工程师需要有足够的全面性,在《我眼中的工程师文化》中我也说“工程师文化,不是只有权力的一面,它对工程师的要求,是每个人都要足够能干,都要做许多的事”……

但是,全面性不代表没有专精、没有方向。深度和广度统一的问题已经有许许多多过往的人和我说过了,不存在一个在某一领域精深的牛人但是知识却很窄,也不存在一个博学大师但是却没有一个自己擅长的领域;而方向更是不可回避的问题,以前和朋友开玩笑总结了几类工程师的发展方向,就像打怪升级一样,有数据库专精、有前端专精、有语言设计专精、有机器学习领域专精,甚至还有企业流程咨询专精、敏捷实践专精的……领域划分实在是太宽阔了,就看技能点数如何分配。

我当然也给自己寻找了方向。在这个网站的右上角我放上了三个关键词,大概是对当前的我一个侧面最粗略的描述:

  • #Web#是我一直以来感兴趣的领域,早有人说互联网软件的技术和发展甩传统软件好几条大街,尤其在见到很多朋友、牛人投身互联网领域,我更对它充满憧憬;
  • #JavaEE#算是我相对熟悉的领域,虽说这几年接触的东西稍微多一些;
  • #全栈工程师#是我的方向之一,粗略地说我现在也已经符合这样的标准,但是仁者见仁智者见智,这是以我的观点而言的,每个人对它有不同的理解,在这里我会说说我的看法。

其他人的理解

关于这个话题,当前颇有争议,虽说大部分工程师表示认可。在Google搜索“Full Stack Developer”的第一条记录,是Laurence Gellert(前汤森路透的工程师)写的一篇名为《What is a Full Stack developer》的文章,这篇文章的观点其实还是非常切合主线的:

To me, a Full Stack Developer is someone with familiarity in each layer, if not mastery in many and a genuine interest in all software technology.

并且罗列了满足“full stack”应当掌握的各层技术,包括服务器、网络、主机环境,数据建模,业务逻辑,API层/Action层/MVC,界面,用户体验和理解用户、业务所需。

在国内,知乎这个帖子应该算是热帖了,每个人都有自己的看法,比如第一条回复就提到了思维方式和学习能力,但是其中有很多观点偏离了“全栈”这个主线,变成了“我心目中的理想工程师”这样的讨论,就不符合初衷了。

全栈工程师的发展

在系统、全面的大公司,全栈工程师并没有一个稳定的发展职位。我无比赞同知乎那个帖子里面这样的一句话:

一个真正的全栈工程师,目标只有一个:创业。

听起来有些悲凉,但事实就是如此。任何一个方向颇具深度的工程师,都有希望为自己在那个特定的领域赢得自己的一席之地,是权威,也是技艺精深的专家。但是对于所谓的“全栈”而言,很多情况下根本就称不上优势,你会写数门程序语言,会设计API,会写前端代码,会做手机APP,甚至会切图,会和用户沟通,但是倘若在这些方向都难说有哪一项足够强大,那全面性又能在大公司的晋升线路上谋得什么?

但是创业的小公司就完全不是这样了,你不能指望有DBA、技服、产品经理、美工、前端设计师、服务器工程师、操作系统管理员……无数角色,你只能有那么少得可怜的几个人,每个人都必须是全才,搞得定各种事情,经验丰富、视野广阔。出了问题,一个人就可以搞定,而每个人,都可以彼此备份。

这也是“学习能力”在全栈工程师中扮演无比重要角色的原因。毕竟,在全面的工程师,也不可避免地涉足自己不熟悉的领域,快速学习并且把问题搞定,在这样的过程中体现自己的价值。

全栈工程师拥有更广阔的视野和更广泛的学识。全栈工程师可以从更高的角度去看待问题,这比某个领域的专家,更不容易做出错误的决策。

事实上,软件工程本来就是一个复杂的事情,需要工程师掌握和学习的知识很多。在我前一家公司,有这样一个故事,好几年前,公司尝试给软件工程师分档,甚至依此使用不同的雇佣实体:让来自子公司A的最优秀的工程师设计了程序,再让来自子公司B的平庸工程师去实现。最后这个方案彻底失败了,两家子公司的工程师被迫合并,这也证明了,软件工程是一项复杂的脑力劳动,想像流水线工人那样,把整个环境简单地切分成若干个过程,然后通过简单劳动完成,是不可能的。你可以举出很多外包、内包公司中上述的例子,但是在我看来,这只是对劳动力的压榨而已,别指望这样的形式能做出什么伟大的产品来。

“全栈”不等于“全面”

“Full Stack”,这个词其实在英文中使用很普遍,可以直译为所谓“全面的技术栈”(软件工程中,每个领域都拥有相应的数种不同技术,这就是这个领域的技术栈),现在人们加入了自己的理解,但无论如何,它绝不等于“comprehensive”。换言之,一个全栈工程师,绝不等于一个全面的工程师。接触多点领域当然有好处,但是浅尝辄止、仅仅停留在入门级别,那这个领域内,给别人、给项目造成的危害,甚至大过那些一窍不通的人。举例来说,你是愿意去给一坨屎一样的设计和代码修修补补呢,还是愿意干脆重新弄一个呢?当然,也不要走极端,有一些领域的知识,可以透明,那就透明吧,比如,使用云服务的时候,你可以对硬件知之甚少,这对工作并无碍。仅仅为了全栈的名号,追求这样的知识储备并无必要。

“全栈”不等于“全端”

全栈工程师的划分,绝不止以“互联网应用”的维度,更特别地,绝不止以“互联网网站”的维度。微博上很多人说到全栈,就提“全端”,我认为,这实在是莫大的误解,二者是严重不等同的。前端+后端,这只是其中一种粗暴的划分方式而已。就像同事中,有对操作系统熟悉的,有对机器学习熟悉的,把他们粗暴地归结为“后端”工程师,是毫无意义的。即便说到创业,也远远不止互联网领域啊。事实上,要能比较熟悉其中几个领域,就已经是非常难得的人才了。我想不出还有什么其他行业,会像软件行业这样需要不断地扩充自己。

最后,我想用一个无比简单的词来描述全栈工程师,肯定不够准确,但也足够直接——

视野

 

Java 并发编程:AQS框架

Java 并发编程:AQS框架

1. 简介

Java内置的锁,其优势是可以花最小的空间开销创建锁(因为每个JAVA对象都可以作为锁使用)和最少的时间开销获得锁(单线程可以在最短时间内获得锁)。线程同步越来越多地被用在多处理器上,特别是在高并发的情况下,然而,JVM内置锁的表现一般,而且不支持任何公平策略。从Java 5开始在java.util.concurrent包中引入了有别于synchronized的同步框架。
设计一个同步器至少应该具以下有两种操作:一个获取方法,如果当前状态不允许,将一直阻塞这个线程;一个释放方法,修改状态,让其他线程有运行的机会。
并发包中并没有为同步器提供一个统一的API,获取和释放方法在不同的类中的名称不同,比如获取方法有:Lock.lock, Semaphore.acquire, CountDownLatch.await和FutureTask.get.这些方法一般都重载有多种版本:阻塞与非阻塞版本、支持超时、支持中断。
java.util.concurrent包中有很多同步类,比如互斥锁、读写锁、信号量等,这些同步类几乎都可以用不同方式来实现,但是JSR166建立了一个同步中心类AbstractQueuedSynchronizer(简称:AQS)的框架,其中提供了大量的同步操作,而且用户还可以在此类的基础上自定义自己的同步类。其设计目标主要有两点:
1、提高可扩展性,用户可以自定义自己的同步类
2、最大限度地提高吞吐量,提供自定义公平策略

2. 设计和实现

同步器的设计比较直接,前面提到包含获取和释放两个操作:
获取操作过程如下:

while (synchronization state does not allow acquire) {
    enqueue current thread if not already queued;
    possibly block current thread;
}
dequeue current thread if it was queued;

释放操作:

update synchronization state;
if (state may permit a blocked thread to acquire)
    unblock one or more queued threads;

要满足以上两个操作,需要以下3点来支持:
1、原子操作同步状态;
2、阻塞或者唤醒一个线程;
3、内部应该维护一个队列。

3. 同步状态

AQS用的是一个32位的整型来表示同步状态的,可以通过以下几个方法来设置和修改这个状态字段:getState(),setState(),compareAndSetState().这些方法都需要java.util.concurrent.atomic包的支持,采用CAS操作。
将state设置为32位整型是一个务实的决定,虽然JSR166提供了64位版本的原子操作,但它还是使用对象内部锁来实现的,如果采用64位的state会导致同步器表现不良好。32位同步器满足大部分应用,如果确实需要64位的状态,可以使用AbstractQueuedLongSynchronizer类。
AQS是一个抽象类,如果它的实现类想要拥有对获取和释放的控制权,那它必须实现tryAcquire和tryRelease两个方法。

4. 阻塞

JSR166以前还没有好的阻塞和解除阻塞线程的API可以使用,只有Thread.suspend() 和 Thread.resume(),但这两个方法已经被废弃了,原因是有可能导致死锁。如果一个线程拥有监视器然后调用 Thread.suspend() 使自已阻塞,另一个线程试图调用Thread.resume()去唤醒它,那么这个线程去获取监视器时即出现死锁。
前面提到的LockSupport解决了这个问题,LockSupport.park()可以阻塞一个线程,LockSupport.unpack()可以解除阻塞.调用一次park(),然后调用多次unpack()只会唤醒一个线程.阻塞针对线程而不是针对同步器。

5. 队列

同步框架最重要的是要有一个同步队列,在这里被严格限制为FIFO队列,因此这个同步框架不支持基于优先级的同步策略。同步队列采用非阻塞队列毋庸置疑,当时非阻塞队列只有两个可供选择CLH队列锁和MCS队列锁.原始的CLH Lock仅仅使用自旋锁,但是相对于MCS Lock它更容易处理cancel和timeout,所以选择了CLH Lock。
CLH队列锁的优点是:进出队快,无锁,畅通无阻(即使在有竞争的情况下,总有一个线程总是能够很快插入到队尾);检查是否有线程在等待也是很容易的(只需要检查头尾指针是否相同)。最后设计出来的变种CLH Lock和原始的CLH Lock有较大的差别:
1、为了可以处理timeout和cancel操作,每个node维护一个指向前驱的指针。如果一个node的前驱被cancel,这个node可以前向移动使用前驱的状态字段。
2、第二个变动是在每个node里使用一个状态字段去控制阻塞,而不是自旋。一个排队的线程调用acquire(),只有在通过了子类实现的tryAcquire()才能返回,确保只有队头线程才允许调用tryAcquire()。
3、另外还有一些微小的改动:head结点使用的是傀儡结点。
变种的CLH队列如下图所示:
\"java-concurrent-4\"
结点中有一个状态位,这个状态位与线程状态密切相关,这个状态位(waitStatus)是一个32位的整型常量,它的取值如下:

static final int CANCELLED =  1;  //因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收
static final int SIGNAL    = -1;  //表示这个结点的继任结点被阻塞了,到时需要通知它
static final int CONDITION = -2;  //表示这个结点在条件队列中,因为等待某个条件而被阻塞
static final int PROPAGATE = -3;  //使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播
0://新结点会处于这种状态。

同步框架提供了一个ConditionObject,一般和Lock接口配合来支持互斥模型,它提供类似JVM同步器的操作。条件对象可以和其他同步器有效的整合,它修复了JVM内置同步器的不足:一个锁可以有多个条件。条件结点内部也有一个状态字段,条件结点是通过nextWaiter指针串起来的一个独立的队列。条件队列中的线程在获取锁之前,必须先被transfer到同步队列中去。transfer先断开条件队列的第一个结点,然后插入到同步队列中,这个新插入到同步队列中的结点和同步队列中的结点一起排队等待获取锁。

6. 用法

AbstractQueuedSynchronizer是一个采用模板方法模式实现的同步器基类,子类只需要实现获取和释放方法。子类一般不直接用于同步控制。因为获取和释放方法一般是私有的,实现细节不必暴露出来,所以常用委派的方法来使用同步器类:在一个类的内部申请一个私有的AQS的子类,委派它的所有同步方法。
AbstractQueuedSynchronizer类还提供了其他一些同步控制方法,包括超时和中断版的获取方法,还集成了独占模式的同步器,如acquireShared(),tryReleaseShared()等方法。
虽然内部队列被设计为FIFO,但并不意味着这个同步器一定是公平的。前面谈到,在tryAcquire()检查之后再排队。因此,新线程完全可以偷偷排在第一个线程前面。之所以不采用FIFO,有时候是想获得更高的吞吐量,为了减少等待时间,新到的线程与队列头部的线程一起公平竞争,如果新来的线程比队头的线程快,那么这个新来的线程就获取锁。队头线程失去竞争会再次阻塞,它的继任也将会被阻塞,但这样能避免饥饿。
如果需要绝对公平,那很简单,只需要在tryAcquire()方法,不在队头返回false即可。检查是否在队头可以使用getFirstQueuedThread()方法。有一情况是,队列是空的,同时有多个线程一拥而入,谁先抢到锁就谁运行,这其实与公平并不冲突,是对公平的补充。

7. 获取与释放锁

AbstractQueuedSynchronizer中比较重要的两个操作是获取和释放,以下是各种获取操作:

public final void acquire(int arg);
public final void acquireInterruptibly(int arg);
public final void acquireShared(int arg);
public final void acquireSharedInterruptibly(int arg);
protected boolean tryAcquire(int arg); 
protected int tryAcquireShared(int arg);
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException;
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException;

释放操作

public final boolean release(int arg);
protected boolean tryRelease(int arg);
protected boolean tryReleaseShared(int arg);

AbstractQueuedSynchronizer的内部类Node类中有两个常量SHARE和EXCLUSIVE,顾名思义这两个常量用于表示这个结点支持共享模式还是独占模式,共享模式指的是允许多个线程获取同一个锁而且可能获取成功,独占模式指的是一个锁如果被一个线程持有,其他线程必须等待。多个线程读取一个文件可以采用共享模式,而当有一个线程在写文件时不会允许另一个线程写这个文件,这就是独占模式的应用场景。

1) 独占模式

AbstractQueuedSynchronizer类方法中方法名不含shared的默认是独占模式,在独占模式下,子类需要重写tryAcquire()方法。
线程首先通过tryAcquire()方法在独占模式下获取锁,如果获取成功就直接返回,否则通过acquireQueued()获取锁,如果仍然失败则selfInterrupt当前线程

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //如果获取锁失败,那么就创建一个代表当前线程的结点加入到等待队列的尾部
            selfInterrupt();
    }

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) { //判断队列中是否有元素
            node.prev = pred; //如果有
            if (compareAndSetTail(pred, node)) { //就设置当前结点为队尾结点
                pred.next = node;
                return node;
            }
        }
        enq(node); //如果没有元素,表示队列为空,做入队操作
        return node;
    }

    private Node enq(final Node node) { //enq方法采用的是变种CLH算法
        for (;;) {
            Node t = tail;
            if (t == null) { //先看头结点是否为空,这一步只会在队列初始化时会执行
                if (compareAndSetHead(new Node())) //如果为空就创建一个傀儡结点
                    tail = head; //头尾指针都指向这个傀儡结点
            } else { //如果头结点非空
                node.prev = t;
                if (compareAndSetTail(t, node)) { //采用CAS操作将当前结点插入到头结点后面
                    t.next = node; //如果在插入的时候尾结点有变化,就将尾结点向后移动直到移动到最后一个结点为止,然后再把当前结点插入到尾结点后面,尾指针指向当前结点
                    return t; //入队成功
                }
            }
        }
    }

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //判断新结点的前趋结点是否为头结点
                if (p == head && tryAcquire(arg)) { //如果它的前趋是头结点,让前趋获取锁
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && //如果不是头结点,就将前趋结点的状态标志位设置为SIGNAL
                    parkAndCheckInterrupt()) //当前线程可以安全地挂起,整个过程结束
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

将新加入的结点放入队列之后,这个结点有两种状态,要么获取锁,要么就挂起,如果这个结点不是头结点,就看看这个结点是否应该挂起,如果应该挂起,就挂起当前结点,是否应该挂起是通过shouldParkAfterFailedAcquire()方法来判断的

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; //首先检查前趋结点的waitStatus位
        if (ws == Node.SIGNAL) //如果为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) { //如果前趋结点是一个被取消的结点
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0); //就向前遍历跳过被取消的结点,直到找到一个没有被取消的结点为止
            pred.next = node; //将找到的这个结点作为它的前趋结点
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don\'t park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //将找到的这个结点的waitStatus位设置为SIGNAL
        }
        return false; //返回false表示线程不应该被挂起
    }

独占模式下释放锁是通过方法release ()来实现的,首先调用子类的tryRelease()尝试释放锁,如果失败,直接返回;如果成功调用unparkSuccessor ()方法做后续处理。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

释放锁成功后需要唤醒继任结点,是通过方法unparkSuccessor实现的

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus; //node参数传进来的是头结点,首先检查头结点的waitStatus位
        if (ws < 0) //如果为负,表示头结点还需要通知后继结点,后面会通知后续节点,因此将该该标志位清0.
            compareAndSetWaitStatus(node, ws, 0);
        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next; //然后查看头结点的下一个结点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        } //如果下一个结点不为空且它的waitStatus<=0,表示后继结点没有被取消,是一个可以唤醒的结点,于是唤醒后继结点返回;如果后继结点为空或者被取消了,则寻找下一个可唤醒的结点,然后唤醒它返回。
        if (s != null)
            LockSupport.unpark(s.thread);
}

2) 共享模式

如果子类想支持共享模式,同样必须重写tryAcquireShared()方法,线程首先通过tryAcquireShared()方法在共享模式下获取锁,如果获取成功就直接返回,否则通过doAcquireShared()获取锁

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) //如果获取成功就直接返回
            doAcquireShared(arg);
    }

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED); //创建一个新结点(共享模式),加入到队尾
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //判断新结点的前趋结点是否为头结点
                if (p == head) { //如果它的前趋是头结点 
                    int r = tryAcquireShared(arg); //让前趋在共享模式下获取锁
                    if (r >= 0) { //如果获取成功,把当前结点设置为头结点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) && //如果不是头结点,就将前趋结点的状态标志位设置为SIGNAL
                    parkAndCheckInterrupt()) //当前线程可以安全地挂起,整个过程结束
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

共享模式下释放锁是通过方法releaseShared()来实现的,首先调用子类的tryReleaseShared()尝试释放锁,如果失败,直接返回;如果成功调用doReleaseShared方法做后续处理。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
doReleaseShared()方法
    private void doReleaseShared() { //这个方法就一个目的,就是把当前结点设置为SIGNAL或者PROPAGATE
        for (;;) {
            Node h = head;
            if (h != null && h != tail) { //如果当前结点不是头结点也不是尾结点
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //先判断当前结点的状态位是否为SIGNAL,如果是就设置为0
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

因为共享模式下更多使用PROPAGATE来传播,SIGNAL会被经过两步改为PROPAGATE:

compareAndSetWaitStatus(h, Node.SIGNAL, 0)
compareAndSetWaitStatus(h, 0, Node.PROPAGATE)

为什么要经过两步呢?原因在unparkSuccessor()方法:如果直接从SIGNAL到PROPAGATE,那么到unparkSuccessor()方法里面又被设置为0:SIGNAL->PROPAGATE->0->PROPAGATE,对头结点相当于多做了一次compareAndSet操作。

Java 并发编程:锁

Java 并发编程:锁

1. Object上的锁

首先介绍一下Java中最基本的锁:Object.wait()、Object.notify()和Object.notifyAll()。
wait、notify和notifyAll方法是Object类的final native方法。所以这些方法不能被子类重写。
void notifyAll()解除所有那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
void notify()随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法也只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。需要注意的是notify选择在该对象上调用wait方法的线程是随机的
void wait()、void wait(long millis)和void wait(long millis,int nanos)使线程进入等待状态,直到它被其他线程通过notify或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
Object.wait()和Object.notify()和Object.notifyAll()必须写在synchronized方法内部或者synchronized块内部,这是因为:这几个方法要求当前正在运行Object.wait()方法的线程拥有object的对象锁。即使你确实知道当前上下文线程确实拥有了对象锁,也不能将Object.wait()这样的语句写在当前上下文中。
下面是一个简单的例子:
info.halo9pan.samples.java.thread.obj.ObjectNotify

        final Object lock = new Object();
        Thread waitThread = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(\"Wait Thread was started.\");
                    synchronized (lock) {
                        lock.wait();
                    }
                    System.out.println(\"Wait Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread notifyThread = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(\"Notify Thread was started.\");
                    System.out.println(\"Notify Thread sleep 1s.\");
                    Thread.sleep(1000L);
                    System.out.println(\"Notify Thread notify.\");
                    synchronized (lock) {
                        lock.notify();
                    }
                    System.out.println(\"Notify Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        waitThread.start();
        notifyThread.start();
Wait Thread was started.
Notify Thread was started.
Notify Thread sleep 1s.
Notify Thread notify.
Notify Thread was finished.
Wait Thread was finished.

这三个方法提供了最基本的锁机制,而且这三个方法是Java的超级父类Object类的final方法,因此所有的Java对象都可以做锁。

2. Thread常用方法

在Java的多线程操作中,用得最多的还是Thread类了。
Thread类中也提供了线程控制的方法,sleep()算是用得最多的了。join()也是一个很实用的方法。Thread.join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的join()方法,直到线程A执行完毕后,才会继续执行线程B。主线程生成并起动了子线程,而子线程里要进行大量的耗时的运算,当主线程处理完其他的事务后,需要用到子线程的处理结果,这个时候一般就要用到join()方法了。
Thread.interrupt()也是线程控制常用的方法。但是Thread.interrupt()生效的情况比较复杂,调用Thread.interrupt()时只会在Object.wait()、Thread.join()和Thread.sleep()几个方法会主动抛出InterruptedException异常。而在其它的情况下,只是通过设置了Thread的一个标志位信息,需要程序自我进行处理。下面是Thread.interrupt()的一段示例:
info.halo9pan.samples.java.thread.obj.ThreadInterrupt

        class WaitThread extends Thread {
            @Override
            public void run() {
                System.out.println(\"Wait Thread was started.\");
                int times = Integer.MIN_VALUE;
                while (times++ < Integer.MAX_VALUE) {
                }
                System.out.println(\"Wait Thread was finished.\");
            }
        }
        final WaitThread waitThread = new WaitThread();
        Thread interruptThread = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(\"Interrupt Thread was started.\");
                    TimeUnit.MILLISECONDS.sleep(10L);
                    System.out.println(\"Wait Thread state.\" + waitThread.getState());
                    waitThread.interrupt();
                    System.out.println(\"Interrupt Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        waitThread.start();
        interruptThread.start();
    }
Wait Thread was started.
Interrupt Thread was started.
Wait Thread state.RUNNABLE
Interrupt Thread was finished.
Wait Thread was finished.

这里的Thread.interrupt()并不会打断线程的执行。在Object.wait()的情况下线程才会被打断:
info.halo9pan.samples.java.thread.obj.ThreadInterruptWait

        final Object lock = new Object();
        class WaitThread extends Thread {

            @Override
            public void run() {
                try {
                    System.out.println(\"Wait Thread was started.\");
                    synchronized (lock) {
                        lock.wait();
                    }
                    System.out.println(\"Wait Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        final WaitThread waitThread = new WaitThread();
        Thread interruptThread = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(\"Interrupt Thread was started.\");
                    TimeUnit.SECONDS.sleep(1L);
                    System.out.println(\"Wait Thread state.\" + waitThread.getState());
                    waitThread.interrupt();
                    System.out.println(\"Interrupt Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        waitThread.start();
        interruptThread.start();
    }
Wait Thread was started.
Interrupt Thread was started.
Wait Thread state.WAITING
Interrupt Thread was finished.
java.lang.InterruptedException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:503)
    at info.halo9pan.samples.java.thread.obj.ThreadInterruptWait$1WaitThread.run(ThreadInterruptWait.java:16)

Thread.interrupt()设计的目的主要是用于处理线程处于block状态,比如wait(),sleep()状态就是个例子。但可以在程序设计时为支持task cancel,同样可以支持RUNNING状态。比如Thread.join()和一些支持interrupt的NIO channel设计:
info.halo9pan.samples.java.thread.obj.ThreadInterruptCheck

        class WaitThread extends Thread {
            @Override
            public void run() {
                System.out.println(\"Wait Thread was started.\");
                int times = Integer.MIN_VALUE;
                try {
                    while (times++ < Integer.MAX_VALUE) {
                        if (Thread.interrupted()) {
                            System.out.println(\"Wait Thread was interrupted.\");
                            throw new InterruptedException(Thread.currentThread().getName());
                        }
                    }
                } catch (InterruptedException e) {
                    new RuntimeException(e);
                }
                System.out.println(\"Wait Thread was finished.\");
            }
        }
        final WaitThread waitThread = new WaitThread();
        Thread interruptThread = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(\"Interrupt Thread was started.\");
                    TimeUnit.MILLISECONDS.sleep(1000L);
                    System.out.println(\"Wait Thread state.\" + waitThread.getState());
                    waitThread.interrupt();
                    System.out.println(\"Interrupt Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        waitThread.start();
        interruptThread.start();
    }
Wait Thread was started.
Interrupt Thread was started.
Wait Thread state.RUNNABLE
Interrupt Thread was finished.
Wait Thread was interrupted.
Wait Thread was finished.

3. java.util.concurrent.locks.Lock

之前介绍的都是比较常规的Java锁,也是Java 5之前用得较多的锁实现。但是在Java 5中引入了全新的并发框架,在并发编程的时候,锁的选择也越来越多了。java.util.concurrent.locks.Lock就是在Java 5中引入的,主要有下面几个方法:

public interface Lock {
    void lock(); //常规地获得锁
    void lockInterruptibly() throws InterruptedException; //可中断地获得锁
    boolean tryLock(); //尝试性获得锁,非阻塞
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //尝试性获得锁,如果超时则返回
    void unlock(); //解锁
    Condition newCondition(); //生成和当前锁相关的条件(队列)对象
}

而java.util.concurrent.locks.ReentrantLock是Lock的主要实现,实现了Lock和Serializable接口。主要的属性只有内部类Sync的对象属性sync,ReentrantLock类的操作实际上都落在了sync身上。除此之外,ReentrantLock是可重入锁,还有一些支持可重入的方法,这里不细说。可以说ReetrantLock是基于它的内部类Sync的对象来实现的。在ReentrantLock中,Sync有FairSync和Nonfair两个子类,而父层有AbstactOwnableSynchronizer和AbstractQueuedSynchronizer。前者实现了当前同步器被哪个线程占有的逻辑,实现了get/setExclusiveOwnerThread()方法,确定获取当前互斥锁的Thread对象。后者则是java.util.concurrent包中非常重要的类,它为并发包中的其他synchronizers提供了一组公共的基础设施。AbstractQueuedSynchronizer会在下面的章节中介绍。这里还是主要介绍ReentrantLock。
ReentrantLock三个加锁的方法中,lockInterruptibly 与 lock比较区别在于:lockInterruptibly 优先考虑响应中断,而不是响应锁定的普通获取或重入获取,而tryLock是非阻塞的,只是尝试性的加锁。下面通过三个例子来看看它们的区别:
info.halo9pan.samples.java.thread.lock.BasicReentrantLock

    final Lock lock = new ReentrantLock();
        class WaitThread extends Thread{
            String token;
            public WaitThread(String token) {
                super();
                this.token = token;
            }
            @Override
            public void run() {
                try {
                    System.out.println(\"Wait Thread \" + token + \" was started.\");
                    lock.tryLock();
                    System.out.println(\"Wait Thread \" + token + \" got the lock.\");
                    TimeUnit.SECONDS.sleep(4L);
                    System.out.println(\"Wait Thread \" + token + \" was finished.\");
                    lock.unlock();
                } catch (InterruptedException e) {
                    System.out.println(\"Wait Thread \" + token + \" was interrupted.\");
                }
            }
        }
        final WaitThread oneThread = new WaitThread(\"one\");
        final WaitThread twoThread = new WaitThread(\"two\");
        Thread modifyThread = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(\"Interrupt Thread was started.\");
                    TimeUnit.SECONDS.sleep(1L);
                    twoThread.interrupt();
                    System.out.println(\"Interrupt Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        oneThread.start();
        twoThread.start();
        modifyThread.start();
    }
Wait Thread one was started.
Interrupt Thread was started.
Wait Thread two was started.
Wait Thread one got the lock.
Interrupt Thread was finished.
Wait Thread one was finished.
Wait Thread two got the lock.
Wait Thread two was interrupted.

info.halo9pan.samples.java.thread.lock.BasicReentrantInterruptLock

    lock.lockInterruptibly();
Wait Thread one was started.
Wait Thread one got the lock.
Wait Thread two was started.
Interrupt Thread was started.
Interrupt Thread was finished.
Wait Thread two was interrupted.
Wait Thread one was finished.

可以看到,在调用ReentrantLock.lockInterruptibly()加锁时,Thread two优先被中断了,而ReentrantLock.lock ()加锁时,Thread two获得了锁之后才会被中断。
info.halo9pan.samples.java.thread.lock.BasicReentrantTryLock

    boolean success = lock.tryLock();
    System.out.println(\"Wait Thread \" + token + \" got the lock: \" + success);
Wait Thread one was started.
Wait Thread one got the lock: true
Interrupt Thread was started.
Wait Thread two was started.
Wait Thread two got the lock: false
Interrupt Thread was finished.
Wait Thread two was interrupted.
Wait Thread one was finished.

在调用ReentrantLock.tryLock()加锁时,Thread one获得了锁,但是Thread two并没有获得锁。

4. Condition条件变量

条件变量是线程同步对象中的一种,主要用来等待某种条件的发生,条件发生后,可以唤醒等待在该条件上的一个线程,或所有线程。条件变量要与锁一起协同工作。条件变量作用于前面提到的Object.wait()、Object.notify()和Object.notifyAll()是一致的。
条件变量调用Lock.newCondition()获得一个实例,通常的调用方式如下:
info.halo9pan.samples.java.thread.lock.BasicLockCondition

    final Lock lock = new ReentrantLock();
        final Condition condition = lock.newCondition();
        class WaitThread extends Thread{
            String token;
            public WaitThread(String token) {
                super();
                this.token = token;
            }
            @Override
            public void run() {
                try {
                    System.out.println(\"Wait Thread \" + token + \" was started.\");
                    lock.lock();
                    System.out.println(\"Wait Thread \" + token + \" got the lock.\");
                    System.out.println(\"Wait Thread \" + token + \" start to wait.\");
                    condition.await();
                    System.out.println(\"Wait Thread \" + token + \" finish waiting.\");
                    lock.unlock();
                    System.out.println(\"Wait Thread \" + token + \" was finished.\");
                } catch (InterruptedException e) {
                    System.out.println(\"Wait Thread \" + token + \" was interrupted.\");
                }
            }
        }
        final WaitThread waitThread = new WaitThread(\"one\");
        Thread modifyThread = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println(\"Signal Thread was started.\");
                    TimeUnit.SECONDS.sleep(1L);
                    lock.lock();
                    System.out.println(\"Signal Thread got the lock.\");
                    condition.signal();
                    lock.unlock();
                    System.out.println(\"Signal Thread was finished.\");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        waitThread.start();
        modifyThread.start();
    }

值得注意的是,当Condition.await()时,隐式的将条件变量关联的Lock解锁,而使其他线程有机会获得Lock,而检查条件,并在条件满足时,等待在条件变量上。

5. java.util.concurrent.locks.ReentrantReadWriteLock

在使用某些种类的 Collection 时,可以使用 ReentrantReadWriteLock 来提高并发性。通常,在预期 collection很大,读取者线程访问它的次数多于写入者线程,并且读写操作的开销高于同步开销时,和适合使用ReadWriteLock。
而且Java 5中还提供了读写锁:ReadWriteLock。在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另一个线程也在写,同样也会导致线程前后看到的数据的不一致性。这时候可以在读写方法中加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的“读-读”操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在“读-写”,“写-写”期间上锁就行了。
对于这种情况,读写锁则最好的解决方案!读写锁的基本原则是:“读-读”不互斥,“读-写”互斥,“写-写”互斥,即在任何时候必须保证:只有一个线程在写入;线程正在读取的时候,写入操作等待;线程正在写入的时候,其他线程的写入操作和读取操作都要等待;

6. synchronized与ReentrantLock性能比较

前面提到过synchronized关键字,加上ReentrantLock,是Java里面主要的两种锁机制。ReentrantLock是Java 5的新特性,采用ReentrantLock可以完全替代替换synchronized传统的锁机制,而且采用ReentrantLock的方式更加面向对象,也更加灵活。也许有不少老文章认为ReentrantLock有比synchronized更好的性能,但是随着Java对synchronized的不断优化,synchronized在大部分的场景下已经有很好的性能,所以简单的写了一些例子测试一下,比较一下两者的性能。
info.halo9pan.samples.java.thread.lock.perf.InstantOperation

    public static void main(String[] args) {
        int[] numbers = {1, 2, 4, 8, 16, 2000};
        int times = 1;
        for (int i : numbers) {
            System.out.println(\"================================================================================\");
            SimpleDemo sync = new InstantOperation(i, new SyncRunner());
            System.out.print(String.format(\"%-20s\", \"Using synchronize:\"));
            sync.batchShow(times);
            SimpleDemo lock = new InstantOperation(i, new LockRunner());
            System.out.print(String.format(\"%-20s\", \"Using ReentrantLock:\"));
            lock.batchShow(times);
        }
    }
    public InstantOperation(int threadNumber, Runner runner) {
        super(threadNumber, runner);
    }
    static class LockRunner extends Runner {
        private Lock lock = new ReentrantLock();
        @Override
        public void doSomething(int invoker) {
            lock.lock();
            this.identifier++;
            lock.unlock();
        }
    }
    static class SyncRunner extends Runner {
        @Override
        public void doSomething(int invoker) {
            synchronized (this) {
                this.identifier++;
            }
        }
}
================================================================================
Using synchronize:  Used thread:    1. Run times: 1. Spent time:      7636137ns. Average time:  7636137ns. 
Using ReentrantLock:Used thread:    1. Run times: 1. Spent time:       741505ns. Average time:   741505ns. 
================================================================================
Using synchronize:  Used thread:    2. Run times: 1. Spent time:      1095153ns. Average time:   547576ns. 
Using ReentrantLock:Used thread:    2. Run times: 1. Spent time:      1124660ns. Average time:   562330ns. 
================================================================================
Using synchronize:  Used thread:    4. Run times: 1. Spent time:      1261072ns. Average time:   315268ns. 
Using ReentrantLock:Used thread:    4. Run times: 1. Spent time:       972424ns. Average time:   243106ns. 
================================================================================
Using synchronize:  Used thread:    8. Run times: 1. Spent time:      2028663ns. Average time:   253582ns. 
Using ReentrantLock:Used thread:    8. Run times: 1. Spent time:      1751133ns. Average time:   218891ns. 
================================================================================
Using synchronize:  Used thread:   16. Run times: 1. Spent time:      3870453ns. Average time:   241903ns. 
Using ReentrantLock:Used thread:   16. Run times: 1. Spent time:      3483023ns. Average time:   217688ns. 
================================================================================
Using synchronize:  Used thread: 2000. Run times: 1. Spent time:    489466690ns. Average time:   244733ns. 
Using ReentrantLock:Used thread: 2000. Run times: 1. Spent time:    381708507ns. Average time:   190854ns.

info.halo9pan.samples.java.thread.lock.perf.ProcessOperation

    public static void main(String[] args) {
        int[] numbers = {1, 2, 4, 8, 16, 2000};
        int times = 1;
        for (int i : numbers) {
            System.out.println(\"================================================================================\");
            SimpleDemo sync = new ProcessOperation(i, new SyncRunner());
            System.out.print(String.format(\"%-20s\", \"Using synchronize:\"));
            sync.batchShow(times);
            SimpleDemo lock = new ProcessOperation(i, new LockRunner());
            System.out.print(String.format(\"%-20s\", \"Using ReentrantLock:\"));
            lock.batchShow(times);
        }
    }
    public ProcessOperation(int threadNumber, Runner runner) {
        super(threadNumber, runner);
    }
    static class LockRunner extends Runner {
        private Lock lock = new ReentrantLock();
        @Override
        public void doSomething(int invoker) {
            lock.lock();
            try {
                TimeUnit.MICROSECONDS.sleep(2);
            } catch (InterruptedException e) {
            }
            this.identifier++;
            lock.unlock();
        }
    }
    static class SyncRunner extends Runner {
        @Override
        public void doSomething(int invoker) {
            synchronized (this) {
                try {
                    TimeUnit.MICROSECONDS.sleep(2);
                } catch (InterruptedException e) {
                }
                this.identifier++;
            }
        }
}
================================================================================
Using synchronize:  Used thread:    1. Run times: 1. Spent time:      6863843ns. Average time:  6863843ns. 
Using ReentrantLock:Used thread:    1. Run times: 1. Spent time:       722262ns. Average time:   722262ns. 
================================================================================
Using synchronize:  Used thread:    2. Run times: 1. Spent time:      2398133ns. Average time:  1199066ns. 
Using ReentrantLock:Used thread:    2. Run times: 1. Spent time:      1552714ns. Average time:   776357ns. 
================================================================================
Using synchronize:  Used thread:    4. Run times: 1. Spent time:      4536268ns. Average time:  1134067ns. 
Using ReentrantLock:Used thread:    4. Run times: 1. Spent time:      4403704ns. Average time:  1100926ns. 
================================================================================
Using synchronize:  Used thread:    8. Run times: 1. Spent time:      9560887ns. Average time:  1195110ns. 
Using ReentrantLock:Used thread:    8. Run times: 1. Spent time:      9407796ns. Average time:  1175974ns. 
================================================================================
Using synchronize:  Used thread:   16. Run times: 1. Spent time:     18672337ns. Average time:  1167021ns. 
Using ReentrantLock:Used thread:   16. Run times: 1. Spent time:     18420465ns. Average time:  1151279ns. 
================================================================================
Using synchronize:  Used thread: 2000. Run times: 1. Spent time:   2358474671ns. Average time:  1179237ns. 
Using ReentrantLock:Used thread: 2000. Run times: 1. Spent time:   2316398302ns. Average time:  1158199ns. 

InstantOperation只是做了简单的自增,而ProcessOperation模拟某些操作,中间暂停了两微秒,在Intel酷睿i3的处理器,Windows 7上做的测试。可以看出,ReentrantLock比synchronized有20%左右的性能提升。

7. synchronized与ReentrantReadWriteLock性能比较

info.halo9pan.samples.java.thread.lock.perf.InstantReadWrite

    public static void main(String[] args) {
        Runner sync = new SyncRunner();
        Runner lock = new LockRunner();
        int[] readNumber = { 2, 4, 1, 4, 2000, 1, 2000 };
        int[] writeNumber = { 2, 1, 4, 4, 1, 2000, 2000 };
        int times = 1;
        for (int i = 0 ; i < readNumber.length; i++) {
            int read = readNumber[i];
            int write = writeNumber[i];
            System.out.println(\"================================================================================\");
            ReadWriteDemo syncDemo = new InstantReadWrite(read, write, sync);
            System.out.print(String.format(\"%-32s\", \"Using synchronize:\"));
            syncDemo.batchShow(times);
            ReadWriteDemo lockDemo = new InstantReadWrite(read, write, lock);
            System.out.print(String.format(\"%-32s\", \"Using ReentrantReadWriteLock:\"));
            lockDemo.batchShow(times);
        }
    }
    public InstantReadWrite(int read, int write, Runner runner) {
        super(read, write);
        setReader(runner);
        setWriter(runner);
    }
    static class SyncRunner extends Runner{
        @Override
        protected String safeRead() {
            String key = getRandomKey();
            String s = null;
            synchronized (this){
                s = super.read(key);
            }
            return s;
        }
        @Override
        protected String safeWrite() {
            String key = getRandomKey();
            String value = randomString(32);
            String s = null;
            synchronized (this){
                s = super.write(key, value);
            }
            return s;
        }
    }
    static class LockRunner extends Runner{
        ReadWriteLock lock = new ReentrantReadWriteLock();
        Lock readLock = lock.readLock();
        Lock writeLock = lock.writeLock();
        @Override
        protected String safeRead() {
            String key = getRandomKey();
            String s = null;
            readLock.lock();
            s = super.read(key);
            readLock.unlock();
            return s;
        }
        @Override
        protected String safeWrite() {
            String key = getRandomKey();
            String value = randomString(32);
            String s = null;
            writeLock.lock();
            s = super.write(key, value);
            writeLock.unlock();
            return s;
        }
    }
================================================================================
Using synchronize:              Used thread:        2/2. Run times: 1. Spent time:      4665840ns. Average time:  1166460ns. 
Using ReentrantReadWriteLock:   Used thread:        2/2. Run times: 1. Spent time:      1255941ns. Average time:   313985ns. 
================================================================================
Using synchronize:              Used thread:        4/1. Run times: 1. Spent time:      1450083ns. Average time:   290016ns. 
Using ReentrantReadWriteLock:   Used thread:        4/1. Run times: 1. Spent time:      1222158ns. Average time:   244431ns. 
================================================================================
Using synchronize:              Used thread:        1/4. Run times: 1. Spent time:      1214889ns. Average time:   242977ns. 
Using ReentrantReadWriteLock:   Used thread:        1/4. Run times: 1. Spent time:     29544329ns. Average time:  5908865ns. 
================================================================================
Using synchronize:              Used thread:        4/4. Run times: 1. Spent time:      1789192ns. Average time:   223649ns. 
Using ReentrantReadWriteLock:   Used thread:        4/4. Run times: 1. Spent time:      1521069ns. Average time:   190133ns. 
================================================================================
Using synchronize:              Used thread:     2000/1. Run times: 1. Spent time:    494456670ns. Average time:   247104ns. 
Using ReentrantReadWriteLock:   Used thread:     2000/1. Run times: 1. Spent time:    383064085ns. Average time:   191436ns. 
================================================================================
Using synchronize:              Used thread:     1/2000. Run times: 1. Spent time:    559353787ns. Average time:   279537ns. 
Using ReentrantReadWriteLock:   Used thread:     1/2000. Run times: 1. Spent time:    456515882ns. Average time:   228143ns. 
================================================================================
Using synchronize:              Used thread:  2000/2000. Run times: 1. Spent time:   1158579953ns. Average time:   289644ns. 
Using ReentrantReadWriteLock:   Used thread:  2000/2000. Run times: 1. Spent time:   1044899135ns. Average time:   261224ns. 

info.halo9pan.samples.java.thread.lock.perf.ProcessReadWrite

    public static void main(String[] args) {
        Runner sync = new SyncRunner();
        Runner lock = new LockRunner();
        int[] readNumber = { 2, 4, 1, 4, 2000, 1, 2000 };
        int[] writeNumber = { 2, 1, 4, 4, 1, 2000, 2000 };
        int times = 1;
        for (int i = 0 ; i < readNumber.length; i++) {
            int read = readNumber[i];
            int write = writeNumber[i];
            System.out.println(\"================================================================================\");
            ReadWriteDemo syncDemo = new ProcessReadWrite(read, write, sync);
            System.out.print(String.format(\"%-32s\", \"Using synchronize:\"));
            syncDemo.batchShow(times);
            ReadWriteDemo lockDemo = new ProcessReadWrite(read, write, lock);
            System.out.print(String.format(\"%-32s\", \"Using ReentrantReadWriteLock:\"));
            lockDemo.batchShow(times);
        }
    }
    public ProcessReadWrite(int read, int write, Runner runner) {
        super(read, write);
        setReader(runner);
        setWriter(runner);
    }
    static class SyncRunner extends ProcessRunner{
        @Override
        protected String safeRead() {
            String key = getRandomKey();
            String s = null;
            synchronized (this){
                s = super.read(key);
            }
            return s;
        }
        @Override
        protected String safeWrite() {
            String key = getRandomKey();
            String value = randomString(32);
            String s = null;
            synchronized (this){
                s = super.write(key, value);
            }
            return s;
        }
    }
    static class LockRunner extends ProcessRunner{
        ReadWriteLock lock = new ReentrantReadWriteLock();
        Lock readLock = lock.readLock();
        Lock writeLock = lock.writeLock();
        @Override
        protected String safeRead() {
            String key = getRandomKey();
            String s = null;
            readLock.lock();
            s = super.read(key);
            readLock.unlock();
            return s;
        }
        @Override
        protected String safeWrite() {
            String key = getRandomKey();
            String value = randomString(32);
            String s = null;
            writeLock.lock();
            s = super.write(key, value);
            writeLock.unlock();
            return s;
        }
    }
================================================================================
Using synchronize:              Used thread:        2/2. Run times: 1. Spent time:      9807628ns. Average time:  2451907ns. 
Using ReentrantReadWriteLock:   Used thread:        2/2. Run times: 1. Spent time:      3200789ns. Average time:   800197ns. 
================================================================================
Using synchronize:              Used thread:        4/1. Run times: 1. Spent time:      6286118ns. Average time:  1257223ns. 
Using ReentrantReadWriteLock:   Used thread:        4/1. Run times: 1. Spent time:      2699610ns. Average time:   539922ns. 
================================================================================
Using synchronize:              Used thread:        1/4. Run times: 1. Spent time:     27321096ns. Average time:  5464219ns. 
Using ReentrantReadWriteLock:   Used thread:        1/4. Run times: 1. Spent time:      5425305ns. Average time:  1085061ns. 
================================================================================
Using synchronize:              Used thread:        4/4. Run times: 1. Spent time:      9596380ns. Average time:  1199547ns. 
Using ReentrantReadWriteLock:   Used thread:        4/4. Run times: 1. Spent time:      7441139ns. Average time:   930142ns. 
================================================================================
Using synchronize:              Used thread:     2000/1. Run times: 1. Spent time:   2367678917ns. Average time:  1183247ns. 
Using ReentrantReadWriteLock:   Used thread:     2000/1. Run times: 1. Spent time:    524961877ns. Average time:   262349ns. 
================================================================================
Using synchronize:              Used thread:     1/2000. Run times: 1. Spent time:   2300219030ns. Average time:  1149534ns. 
Using ReentrantReadWriteLock:   Used thread:     1/2000. Run times: 1. Spent time:   2381686271ns. Average time:  1190248ns. 
================================================================================
Using synchronize:              Used thread:  2000/2000. Run times: 1. Spent time:   4661729854ns. Average time:  1165432ns. 
Using ReentrantReadWriteLock:   Used thread:  2000/2000. Run times: 1. Spent time:   2915582988ns. Average time:   728895ns. 

InstantReadWrite只是做了简单的自增,ReentrantReadWriteLock与ReentrantLock比较接近,比synchronized有20%左右的性能提升。而ProcessReadWrite模拟某些操作,中间暂停了两微秒,在2000读线程,1个写线程的情景下,ReentrantReadWriteLock就充分体现了优势,花费的时间只有synchronized的10%!而且在2000读线程,2000写线程的情景下,平均时间也只有synchronized的60%。这个性能的提升是非常明显的。

8. java.util.concurrent.locks.LockSupport

最后再简单说说LockSupport,LockSupport主要有两个方法:park和unpark,它们主要是为了代替之前Thread中的Thread.suspend()和Thread.resume()的,这两个在Thread中的方法,Java已经不建议使用了,被标注为@Deprecated。
LockSupport的park()和unpark(),与Object.wait()和notify()的功能比较类似,但还是有区别的。首先,它们面向的主体不一样。LockSuport主要是针对Thread进进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait()是以对象为基础,阻塞当前的线程和唤醒单个或者所有线程。其次,实现机制不同。虽然LockSuport可以指定观察的object对象,但和Object.wait(),两者的阻塞队列并不交叉,Object.notifyAll()不能唤醒LockSupport的阻塞线程。
LockSupport还有一个方法:park(Object)可以传递一个blocker对象,对应的blcoker会记录在Thread的一个parkBlocker属性中,通过jstack命令可以非常方便的监控具体的阻塞对象。
LockSupport.park()能响应interrupt事件,但不会抛出InterruptedException异常。并且LockSupport.park()在任何时候都会“无原因”的返回,所以通常会把它放在无限循环中通过条件判断来退出,它提供了一个比自旋锁性能更好锁机制。
注意LockSupport的park()和unpark()是基于许可的,并且任何时候只有一个许可是有效的。这意味着如果就算LockSupport.park()之前被调用了多次,而LockSupport.unpark()只需要调用一次就能获得许可。同时如果LockSupport.unpark()先于LockSupport.park()调用,LockSupport同样能获得许可而不会被阻塞。
LockSupport往往被应用于线程的框架程序中,而在应用级别的并发程序中用得较少。如果你想开发自己的并发框架,才会较多的用到LockSupport,而编写一般的多线程程序,LockSupport就不是特别合适了。