记录每一次进步

工作生活中的每一次坑,都是程序员的一块勋章

同步容器类

同步容器类包括Vector和HashTable,二者都是早期JDK的一部分,此外还包括在JDK1.2当中添加的一些功能相似的类,这些同步的封装类是由Collections.synchronizedXxx等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次都只有一个线程能够访问容器的状态。相较于并行容器,同步容器的实现原理其实很简单,就是对普通容器做了一层封装,并实现容器的每一个方法,在方法上实现同步。比如通过Collections类的工厂方法将一个普通的List封装成一个同步容器:

1
2
3
4
5
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}

其他包装函数
同步容器包装函数

同步容器类的问题

在并发编程当中,虽然同步容器类是线程安全的,但是在某些情况下可能需要额外的客户端加锁来保护复合操作。如下面一段代码:

1
2
3
4
5
6
7
8
9
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}

public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}

上述两个函数中,虽然Vector是线程安全的,但是获取Vector大小与获取/删除之间没有锁保护,当获得Vector大笑之后,如另外一个线程删除了Vector中的最末尾位置的元素,则每个函数的最后一句代码执行将报错。因此,对于复合操作,需要在符合操作上用锁来保证操作的原子性:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Object getLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}

public static void deleteLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}

在之前的文章《Java集合ArrayList中modCount详解及subList函数要点》中,曾经提到过ConcurrentModificationException异常,在对集合进行迭代操作的过程中,如果修改了原集合,将导致异常的发生。同样,如果在迭代期间modCount被其他线程修改,那么同样将发生ConcurrentModificationException异常。由于使用同步类容器需要保证在对容器进行复合操作及其他一些操作要进行客户端加锁,导致了实现线程安全的同步操作的保障将分散代码的各个地方,这将增加代码实现的难度以及维护的难度。正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略以及简化维护工作。因此,更能实现该目的的并行容器,也就成了更好的选择。

并发容器

同步容器类存在两个问题,一个问题就是上面提到的复合操作需要客户端加锁,以保证操作的正确性。另外一个就是同步容器将所有对容器状态的访问都串行化,以实现他们的线程安全性,但这种方法的代价是严重降低并发性,当多个线程竞争访问容器的锁时,吞吐量将严重降低。因此,通过并发容器代替同步容器,可以极大地提高伸缩性并降低风险。并发容器注重以下特性:

  1. 根据具体场景进行设计,尽量避免使用锁,提高容器的并发访问性。
  2. 并发容器定义了一些线程安全的复合操作。
  3. 并发容器在迭代时,可以不封闭在synchronized中。但是未必每次看到的都是”最新的、当前的”数据。如果说将迭代操作包装在synchronized中,可以达到”串行”的并发安全性,那么并发容器的迭代达到了”脏读”。

可以通过下图简单了解concurrent中关于容器类的接口和类:
并发容器继承关系

两个接口

  • ConcurrentMap
    该接口定义Map的原子操作:putIfAbsent、remove、replace

  • BlockingQueue
    阻塞队列,不允许null值;
    取元素时,如果队列为空则等待;存元素时,如果没有空间则等待;

阻塞队列的方法有四种形式–当操作不能立即得到满足,但可能在未来某一时刻被满足的时候,有四种不同的方式来处理:

  • 抛出异常
  • 返回特殊的值(null或false,取决与具体的操作)
  • 无期限地阻塞当前线程,直到该操作成功
  • 仅在指定的最大时长内阻塞,过后还不成功就放弃

实现类

通过上面的图可以知道,concurrent包中的并发容器主要可以四类,分别是:

  • CopyOnWrite容器:CopyOnWriteArrayList、CopyOnWriteArraySet
  • CocurrentMap的实现类:ConcurrentHashMap、ConcurrentSkipListMap
  • 阻塞队列的实现类(共七种)
  • 其他:ConcurrentLinkedQueue、ConcurrentLikedDeque、ConcurrentSkipListSet

CopyOnWrite容器

其实现原理是,在创建CopyOnWrite容器实例时,是通过安全方式发布了一个事实不可变对象,由前一篇文章中我们知道,安全发布的事实不可变对象是线程安全的,那么在访问该对象时就不再需要进一步的同步。但是在每次修改时,都会创建并重新发布一个新的容器副本就行修改,从而实现可变性。需要注意的时,每当修改容器是都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时。所以,建议仅当迭代操作远远多余修改操作时,才应该使用“写入时复制”容器。

CopyOnWriteArrayList(写入时复制List)

CopyOnWriteArrayList用于替代同步List,其在迭代期间不需要对容器进行加锁或复制。

CopyOnWriteArraySet

java.util.ArrayList的线程安全版本:所有的修改操作都是通过对底层数组的最新copy来实现。

ConcurrentMap的实现类

ConcurrentHashMap

与HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但它使用一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并部署在每个方法上都用同一个锁进行同步并使得只能有一个线程访问容器,而是使用一种粒度更细的锁机制来实现更大程度的共享,这种机制成为分段锁。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。,所谓分段锁,简单来说就是将数据进行分段,每一段锁用于锁容器中的一部分数据,那么当多线程访问容器里的不容数据段的数据时,线程间就不会存在锁竞争,从而可以有效地提高并发访问效率。有些方法需要跨段,比如size(),就需要按照顺序锁定所有的段,完成操作后,再按顺序释放锁。有关分段锁的应用,可以参看ConcurrentHashMap分段锁技术

ConcurrentSkipListMap

ConcurrentSkipListMap在JDK并发工具类使用范围不是很广,它是针对某一特殊需求而设计的——支持排序,同时支持搜索目标返回最接近匹配项的导航方法。ConcurrentSkipListMap使用SkipList(跳表)实现排序,而TreeMap使用红黑树。

阻塞队列

阻塞队列是一个支持阻塞插入和阻塞移除的队列:当队列满时,队列会阻塞插入元素的线程,直到队列不满;当队列为空时,队列会阻塞获取元素的线程,直到队列不空。阻塞队列常用于生产者和消费者模式,生产者向队列中添加元素,消费者则从队列中取出元素。线程池当中使用阻塞队列来实现任务的排队,在这里简单介绍一下阻塞队列的几个具体实现类。

ArrayBlockingQueue

使用数组实现的有界阻塞队列,按照FIFO的原则对元素排序;内部使用重入锁可实现公平访问。内部使用一个重入锁来控制并发修改操作,即同一时刻,只能进行放或取中的一个操作。初始化时,必须指定容量大小。

LinkedBlockingQueue

使用链表实现的有界阻塞队列,按照FIFO的原则对元素排序;默认和最大长度均为Integer.MAX_VALUE,所以在使用的时候,要注意指定最大容量,否则可能会导致元素数量过多,内存溢出。内部使用两个重入锁来控制并发操作,即同一时刻,允许同时进行放和取。

PriorityBlockingQueue

支持优先级的无界阻塞队列,默认情况下元素按照自然顺序升序排列,可以自定义类实现compareTo()方法来指定元素的排序规则,或在初始化PriorityBlockingQueue时指定构造参数Comparator来对元素进行排序,但不能保证同优先级元素的顺序;

DelayQueue

支持延时获取元素的无界阻塞队列,队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延迟期满后,才能从队列中获取元素。
DelayQueue可以应用在缓存系统的设计(用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素,表示缓存有效期到了)、定时任务调度等场景(ScheduledThreadPoolExecutor中的ScheduledFutureTask类就是实现的Delayed接口)

SyncronousQueue

不存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素,支持公平访问队列,非常适合传递性场景,即把生产者线程处理的数据直接传递给消费者线程,队列本身不存储任何元素。SyncronousQueue的吞吐量高于ArrayBlockingQueue和LinkedBlockingDeque。

LinkedTransferQueue

使用链表实现的无界阻塞TransferQueue,当有消费者正在等待接受元素时,队列可以通过transfer()方法把生产者传入的元素立即传给消费者。

LinkedBlockingDeque

使用链表实现的双向阻塞队列,可以在队列的两端进行插入和移除元素。

其他(等补充)

总结

因此,在使用容器进行开发时,我们有三种选择,第一种是使用普通的容器,二是使用同步容器,三是使用并发容器。在容器的选择上,还是需要我们根据具体的业务需要,选择合适的容器来实现业务功能。

欢迎关注个人公众号:
个人公号

学习自:http://www.infoq.com/cn/articles/android-in-depth-gradle (大部分可能有差不多,只有少部分改了纰漏和认为他写的不对的地方)

安装(mac)

brew install groovy

查看版本号

groovy -verson

环境

Android studio 2.0

基础

  • Groovy注释标记和Java一样,支持//或者//
  • Groovy语句可以不用分号结尾。Groovy为了尽量减少代码的输入,确实煞费苦心
  • Groovy中支持动态类型,即定义变量的时候可以不指定其类型。Groovy中,变量定义可以使用关键字def。注意,虽然def不是必须的,但是为了代码清晰,建议还是使用def关键字
def var =1 
def str= "i am a person"
def int x = 1//也可以指定类型
  • 函数定义时,参数的类型也可以不指定。比如
String function(arg1,args2){//无需指定参数类型
}
  • 除了变量定义可以不指定类型外,Groovy中函数的返回值也可以是无类型的。比如:
    //无类型的函数定义,必须使用def关键字
def  nonReturnTypeFunc(){
     last_line   //最后一行代码的执行结果就是本函数的返回值
}
//如果指定了函数返回类型,则可不必加def关键字来定义函数
String  getString(){
   return "I am a string"
}

其实,所谓的无返回类型的函数,我估计内部都是按返回Object类型来处理的。毕竟,Groovy是基于Java的,而且最终会转成Java Code运行在JVM上

  • 函数返回值:Groovy的函数里,可以不使用return xxx来设置xxx为函数返回值。如果不使用return语句的话,则函数里最后一句代码的执行结果被设置成返回值。比如
//下面这个函数的返回值是字符串"getSomething return value"
def getSomething(){
   "getSomething return value" //如果这是最后一行代码,则返回类型为String
    1000 //如果这是最后一行代码,则返回类型为Integer
}

注意,如果函数定义时候指明了返回值类型的话,函数中则必须返回正确的数据类型,否则运行时报错。如果使用了动态类型的话,你就可以返回任何类型了。

  • Groovy对字符串支持相当强大,充分吸收了一些脚本语言的优点:
    1 单引号''中的内容严格对应Java中的String,不对$符号进行转义

    def singleQuote='I am $ dolloar'  //输出就是I am $ dolloar

    2 双引号""的内容则和脚本语言的处理有点像,如果字符中有$号的话,则它会$表达式先求值。

    def doubleQuoteWithoutDollar = "I am one dollar" //输出 I am one dollar
    def x = 1
    def doubleQuoteWithDollar = "I am $x dolloar" //输出I am 1 dolloar

    3 三个引号'''xxx'''中的字符串支持随意换行 比如

    def multieLines = ''' begin
       line  1 
       line  2
       end '''
  • 最后,除了每行代码不用加分号外,Groovy中函数调用的时候还可以不加括号。比如:

    println("test") ---> println "test"

注意,虽然写代码的时候,对于函数调用可以不带括号,但是Groovy经常把属性和函数调用混淆。比如

def getSomething(){
   "hello"
}

getSomething() //如果不加括号的话,Groovy会误认为getSomething是一个变量。
所以,调用函数要不要带括号,我个人意见是如果这个函数是Groovy API或者Gradle API中比较常用的,比如println,就可以不带括号。否则还是带括号。Groovy自己也没有太好的办法解决这个问题,只能兵来将挡水来土掩了。

数据类型

Groovy中的数据类型我们就介绍两种和Java不太一样的:

  • 一个是Java中的基本数据类型。
  • 另外一个是Groovy中的容器类。
  • 最后一个非常重要的是闭包。

基本数据类型
作为动态语言,Groovy世界中的所有事物都是对象。所以,int,boolean这些Java中的基本数据类型,在Groovy代码中其实对应的是它们的包装数据类型。比如int对应为Integer,boolean对应为Boolean。比如下图中的代码执行结果:


image005.png

容器类
List类

变量定义:List变量由[]定义,比如

def aList = [5,’string’,true] //List由[]定义,其元素可以是任何对象

变量存取:可以直接通过索引存取,而且不用担心索引越界。如果索引超过当前链表长度,List会自动
往该索引添加元素

assert aList[1] == ‘string’
assert aList[5] == null //第6个元素为空
aList[100] = 100 //设置第101个元素的值为10
assert aList[100] == 100

那么,aList到现在为止有多少个元素呢?

println aList.size ===>结果是101

Map类

容器变量定义

变量定义:Map变量由[:]定义,比如

def aMap = [‘key1’:’value1’,’key2’:true]

Map由[:]定义,注意其中的冒号。冒号左边是key,右边是Value。key必须是字符串,value可以是任何对象。另外,key可以用’’或””包起来,也可以不用引号包起来。比如

def aNewMap = [key1:”value”,key2:true] //其中的key1和key2默认被
处理成字符串”key1”和”key2”

不过Key要是不使用引号包起来的话,也会带来一定混淆,比如

def key1=”wowo”
def aConfusedMap=[key1:”who am i?”]

aConfuseMap中的key1到底是”key1”还是变量key1的值“wowo”?显然,答案是字符串”key1”。如果要是”wowo”的话,则aConfusedMap的定义必须设置成:

def aConfusedMap=[(key1):”who am i?”]

Map中元素的存取更加方便,它支持多种方法:

println aMap.keyName <==这种表达方法好像key就是aMap的一个成员变量一样
println aMap[‘keyName’] <==这种表达方法更传统一点
aMap.anotherkey = “i am map” <==为map添加新元素

Range类
Range是Groovy对List的一种拓展,变量定义和大体的使用方法如下:

def aRange = 1..5  <==Range类型的变量 由begin值+两个点+end值表示
                      左边这个aRange包含1,2,3,4,5这5个值

如果不想包含最后一个元素,则

def aRangeWithoutEnd = 1..<5 <==包含1,2,3,4这4个元素
println aRange.from
println aRange.to

API
Groovy的API文档位于 http://www.groovy-lang.org/api.html

闭包
闭包,英文叫Closure,是Groovy中非常重要的一个数据类型或者说一种概念了。闭包的历史来源,种种好处我就不说了。我们直接看怎么使用它!

闭包,是一种数据类型,它代表了一段可执行的代码。其外形如下:

def aClosure = {//闭包是一段代码,所以需要用花括号括起来..  
    String param1, int param2 ->  //这个箭头很关键。箭头前面是参数定义,箭头后面是代码  
    println"this is code" //这是代码,最后一句是返回值,  
   //也可以使用return,和Groovy中普通函数一样  
}

简而言之,Closure的定义格式是:

def xxx = {paramters -> code}  //或者  
def xxx = {无参数,纯code}  这种case不需要->符号

说实话,从C/C++语言的角度看,闭包和函数指针很像。闭包定义好后,要调用它的方法就是:
闭包对象.call(参数) 或者更像函数指针调用的方法:
闭包对象(参数)
比如

aClosure.call("this is string",100)  或者  
aClosure("this is string", 100)

上面就是一个闭包的定义和使用。在闭包中,还需要注意一点:
如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫it,和this的作用类似。it代表闭包的参数。
比如:

def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'

等同于

def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'

但是,如果在闭包定义时,采用下面这种写法,则表示闭包没有参数!

def noParamClosure = { -> true }

这个时候,我们就不能给noParamClosure传参数了!

noParamClosure ("test")  <==报错喔!

Closure使用中的注意点

  1. 省略圆括号

闭包在Groovy中大量使用,比如很多类都定义了一些函数,这些函数最后一个参数都是一个闭包。比如:

public static <T> List<T> each(List<T> self, Closure closure)

上面这个函数表示针对List的每一个元素都会调用closure做一些处理。这里的closure,就有点回调函数的感觉。但是,在使用这个each函数的时候,我们传递一个怎样的Closure进去呢?比如:

def iamList = [1,2,3,4,5]  //定义一个List
iamList.each{  //调用它的each,这段代码的格式看不懂了吧?each是个函数,圆括号去哪了?
      println it
}

上面代码有两个知识点:
each函数调用的圆括号不见了!原来,Groovy中,当函数的最后一个参数是闭包的话,可以省略圆括号。比如

def  testClosure(int a1,String b1, Closure closure){
      //do something
      closure() //调用闭包
}
那么调用的时候,就可以免括号!
testClosure (4, "test", {
   println "i am in closure"
} )  //红色的括号可以不写..

注意,这个特点非常关键,因为以后在Gradle中经常会出现图7这样的代码:


image008.png

经常碰见图7这样的没有圆括号的代码。省略圆括号虽然使得代码简洁,看起来更像脚本语言,但是它这经常会让我confuse(不知道其他人是否有同感),以doLast为例,完整的代码应该按下面这种写法:

 doLast({
   println 'Hello world!'
})

有了圆括号,你会知道 doLast只是把一个Closure对象传了进去。很明显,它不代表这段脚本解析到doLast的时候就会调用println 'Hello world!' 。

但是把圆括号去掉后,就感觉好像println 'Hello world!'立即就会被调用一样!

  1. 如何确定Closure的参数

另外一个比较让人头疼的地方是,Closure的参数该怎么搞?还是刚才的each函数:

public static <T> List<T> each(List<T> self, Closure closure)

如何使用它呢?比如:

def iamList = [1,2,3,4,5]  //定义一个List变量
iamList.each{  //调用它的each函数,只要传入一个Closure就可以了。
  println it
}

看起来很轻松,其实:
对于each所需要的Closure,它的参数是什么?有多少个参数?返回值是什么?

我们能写成下面这样吗?

iamList.each{String name,int x ->
  return x
}  //运行的时候肯定报错!

所以,Closure虽然很方便,但是它一定会和使用它的上下文有极强的关联。要不,作为类似回调这样的东西,我如何知道调用者传递什么参数给Closure呢?

此问题如何破解?只能通过查询API文档才能了解上下文语义。比如下图8:


image009.png

image010.png

图8中:
each函数说明中,将给指定的closure传递Set中的每一个item。所以,closure的参数只有一个。
findAll中,绝对抓瞎了。一个是没说明往Closure里传什么。另外没说明Closure的返回值是什么.....。

对Map的findAll而言,Closure可以有两个参数。findAll会将Key和Value分别传进去。并且,Closure返回true,表示该元素是自己想要的。返回false表示该元素不是自己要找的。示意代码所示:

def result = aMap.findAll {
    key, value ->
        println "key=$key,value=$value"
        if (key == "k1")
            return true
        return false
}

Closure的使用有点坑,很大程度上依赖于你对API的熟悉程度,所以最初阶段,SDK查询是少不了的。

脚本类

import
groovy也可以像java那样写package,然后写类

package bean
class Person {
    String name
    String gender
    Person(name, gender) {
        this.name = name
        this.gender = gender
    }
    def print() {
        println name + " " + gender
    }
}
import bean.Person
def name = 'EvilsoulM'
def person=new Person(name,"male");
person.print()

groovy和Java类很相似。当然,如果不声明public/private等访问权限的话,Groovy中类及其变量默认都是public的。

脚本到底是什么
Java中,我们最熟悉的是类。但是我们在Java的一个源码文件中,不能不写class(interface或者其他....),而Groovy可以像写脚本一样,把要做的事情都写在xxx.groovy中,而且可以通过groovy xxx.groovy直接执行这个脚本。这到底是怎么搞的?

Groovy把它转换成这样的Java类:
执行 groovyc -d classes test.groovy
groovyc是groovy
的编译命令,-d classes用于将编译得到的class文件拷贝到classes文件夹下
图13是test.groovy脚本转换得到的java class。用jd-gui反编译它的代码:


image015.png
  • test.groovy被转换成了一个test类,它从script派生。
  • 每一个脚本都会生成一个static main函数。这样,当我们groovy test.groovy的时候,其实就是用java去执行这个main函数
  • 脚本中的所有代码都会放到run函数中。比如,println 'Groovy world',这句代码实际上是包含在run函数里的。
  • 如果脚本中定义了函数,则函数会被定义在test类中。

groovyc是一个比较好的命令,读者要掌握它的用法。然后利用jd-gui来查看对应class的Java源码。

3.脚本中的变量和作用域
前面说了,xxx.groovy只要不是和Java那样的class,那么它就是一个脚本。而且脚本的代码其实都会被放到run函数中去执行。那么,在Groovy的脚本中,很重要的一点就是脚本中定义的变量和它的作用域。举例:

def x = 1 <==注意,这个x有def(或者指明类型,比如 int x = 1)  
def printx(){  
   println x  
}

printx() <==报错,说x找不到

为什么?继续来看反编译后的class文件。


image016.png


图中,x也没有被定义成test的成员函数,而是在run的执行过程中,将x作为一个属性添加到test实例对象中了。然后在printx中,先获取这个属性。

注意,Groovy的文档说 x = 1这种定义将使得x变成test的成员变量,但从反编译情况看,这是不对的.....(这是infoQ文章中说的,但是测试来说这句话是对的,应该是文章作者没有定义成class)

虽然printx可以访问x变量了,但是假如有其他脚本却无法访问x变量。因为它不是test的成员变量。

比如,我在测试目录下创建一个新的名为test1.groovy。这个test1将访问test.groovy中定义的printx函数:

def atest=new test()
atest.printx()

这种方法使得我们可以将代码分成模块来编写,比如将公共的功能放到test.groovy中,然后使用公共功能的代码放到test1.groovy中
执行groovy test1.groovy,报错。说x找不到。这是因为x是在test的run函数动态加进去的。怎么办?

import groovy.transform.Field;   //必须要先import
@Field x = 1  <==在x前面加上@Field标注,这样,x就彻彻底底是test的成员变量了。

查看编译后的test.class文件,得到:


image019.png


这个时候,test.groovy中的x就成了test类的成员函数了。如此,我们可以在script中定义那些需要输出给外部脚本或类使用的变量了!

eg:
ScriptBase.groovy类 (用了filed 就相当这就是一个class 就不用再自己定义class了)

import groovy.transform.Field;
@Field author = 'EvilsouM'
@Field gender = 'male'
@Field age = 25
//必须要先import
def printInfo() {
    println "name->$author  gender->$gender age->$age"
}

或者自己定义class

class ScriptBase {
    def author = 'EvilsouM'
    def gender = 'male'
    def age = 25//必须要先import
    def printInfo() {
        println "name->$author  gender->$gender age->$age"
    }
}

scripttest.groovy类

def Closure printAuthorInfo = {
            String name, String gender, int age ->
                println "name->$name  gender->$gender age->$age"
}
def ScriptBase base = new ScriptBase()
base.printInfo()
printAuthorInfo.call(base.author, base.gender, base.age) 上面两种方式都能拿到成员变量

文件I/O操作
本节介绍下Groovy的文件I/O操作。直接来看例子吧,虽然比Java看起来简单,但要理解起来其实比较难。尤其是当你要自己查SDK并编写代码的时候。

整体说来,Groovy的I/O操作是在原有Java I/O操作上进行了更为简单方便的封装,并且使用Closure来简化代码编写。主要封装了如下一些了类:


image020.png

1 读该文件中的每一行:eachLine的唯一参数是一个Closure。Closure的参数是文件每一行的内容
其内部实现肯定是Groovy打开这个文件,然后读取文件的一行,然后调用Closure...

def File targetFile = new File("build.gradle")
targetFile.eachLine {
    String line ->
        println line
}

2 直接得到文件内容

targetFile.getBytes()  <==文件内容一次性读出,返回类型为byte[]

3 使用InputStream.InputStream的SDK在 http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html

def ism =  targetFile.newInputStream()  
//操作ism,最后记得关掉  
ism.close

4 使用闭包操作inputStream,以后在Gradle里会常看到这种搞法

 targetFile.withInputStream{
 ism -> 操作ism. 不用close。Groovy会自动替你close
}
  1. 写文件
    和读文件差不多。不再啰嗦。这里给个例子,告诉大家如何copy文件。
    def srcFile = new File(源文件名)
    def targetFile = new File(目标文件名)
    targetFile.withOutputStream{
    os-> srcFile.withInputStream {
    ins->
       os << ins //利用OutputStream的<<操作符重载,完成从inputstream到OutputStream  //的输出
      }
    }
    </div>
    <!--  -->

    <div class="show-foot">
      <a class="notebook" href="/nb/2655670">
        <i class="iconfont ic-search-notebook"></i> <span>其他</span>

欢迎关注个人公众号:
个人公号

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三中方式可以修复这个问题:

  1. 不在线程之间共享该状态变量

    采用线程封闭技术,实现状态变量不在线程之间进行共享。线程封闭技术有三个实现原理:

  2. Ad-hoc线程封闭

    其实就是维护线程封闭性的职责完全由程序实现来承担,该方式的线程封闭是非常脆弱的

  3. 栈封闭

    在栈封闭中。只能通过局部变量才能访问对象,局部变量的固有属性之一就是封闭在执行环境当中,它们位于执行线程的栈中,其他线程无法访问这个栈。
    该方法其实也就是使用局部变量来实现线程的安全。

  4. ThreadLocal类

    维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口和方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是能返回当前执行线程在调用set时设置的最新值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    pulic Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
    }
    };

    public static Connection getConnection() {
    return connectionHoler.get();
    }
  5. 将状态变量修改为不可变的变量

    不可比对象一定是线程安全的。所谓不可变对象,其实就是如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一。因为不可变对象在创建之后只会有一种状态,不会发生变化,所以一定是线程安全的。要使一个对象成为不可变对象,需要满足以下三个条件:

  • 对象创建后其状态就不能修改
  • 对象的所有与都是final类型的
  • 对象被正确地创建了(在对象的创建期间,this引用没有逸出)。

    其中,如果对象从技术上来看是可变的,但其状态在对象被创建后就不会再变化,那么把这种对象成为“事实不可变对象”,这些对象不需要满足上述的三个条件,在这些对象发布之后,程序只需要将它们视作不可变的对象即可。
  1. 在访问状态变量时使用同步

    在大多数情况下,以上两种保证线程安全性的方式不足以满足我们的开发需求,这是,我们就需要在访问共享的状态变量时,使用同步机制,保证线程的安全性。

欢迎关注个人公众号:
个人公号

因为一次在项目开发中使用ArrayList的过程中,发生了ConcurrentModificationException异常,于是查询了相关资料,对发生该异常的原因记录一下。

所谓的ConcurrentModificationException翻译过来就是并发修改异常,网上大部分该异常出现的原因,都是在使用迭代器的时候发生的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.ArrayList;
import java.util.Iterator;

public class Test {

public static void main(String[] args) {
ArrayList<String> array = new ArrayList<String>();

// 创建并添加元素
array.add("hello");
array.add("world");
array.add("java");
Iterator it = array.iterator();
while (it.hasNext()) {
String s = (String) it.next();
if ("world".equals(s)) {
array.add("javaee");
}
}
}
}

在该例子中,我们使用迭代器进行迭代的过程中对集合进行了操作(不限于此处的添加操作,也可能是删除等),导致迭代器失效抛出该异常。但是在项目当中,本人并没有使用到迭代器,而是存在下面一段代码:

1
2
3
4
5
6
List<TableEntity> tableEntities = tableData.getValue().subList(1, tableData.getValue().size());
List<TableEntity> newEntitys = tableData.getValue().subList(0,1);
List<TableEntity> entities = sortTableEntity(tableEntities);
newEntitys.addAll(entities);
tableData.getValue().clear();
tableData.getValue().addAll(newEntitys);

异常在最后一步的时候抛出。可见,ConcurrentModificationException异常不仅仅是在使用迭代器的时候会出现。分析ArrayList类的subList源码我们可以发现,

1
2
3
4
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}

此处返回了一个SubList的对象,而在其构造函数内部,

1
2
3
4
5
6
7
SubList(AbstractList<E> parent,int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}

可以看到this.modCount = ArrayList.this.modCount这样一句代码。而在第一个迭代器的例子中,通过iterator()函数返回的迭代器在构造当中也使用到了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

可见,该异常的抛出与modCount有关,modCount属性是从AbstractList抽象类继承而来的。查看javadoc文档中的解释:

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
This field is used by the iterator and list iterator implementation returned by the iterator and listIterator methods. If the value of this field changes unexpectedly, the iterator (or list iterator) will throw a ConcurrentModificationException in response to the next, remove, previous, set or add operations. This provides fail-fast behavior, rather than non-deterministic behavior in the face of concurrent modification during iteration.

我们知道该参数用来记录集合被修改的次数,之所以要记录修改的次数,是因为ArrayList不是线程安全的,为了防止在使用迭代器和子序列的过程当中对原集合的修改导致迭代器及子序列的失效,故保存了修改次数的记录,在迭代器的操作及子序列的操作过程当中,会首先去检查modCount是否相等(函数checkForComodification()),如果不想等的话,则说明集合被修改了,那么为了防止后续不明确的错误发生,于是便抛出了该异常。为了防止该异常的出现,在使用迭代器进行集合的迭代是,若要对集合进行修改,需要通过迭代器提供的对集合进行操作的函数来进行。而对我代码中出现的问题,可以修改为:

1
2
3
4
5
6
List<TableEntity> tableEntities = Lists.newArrayList(tableData.getValue().subList(1, tableData.getValue().size()));
List<TableEntity> newEntitys = Lists.newArrayList(tableData.getValue().subList(0,1));
List<TableEntity> entities = sortTableEntity(tableEntities);
newEntitys.addAll(entities);
tableData.getValue().clear();
tableData.getValue().addAll(newEntitys);

这样的话,newEntitys就是一个ArrayList的对象而不是SubList的对象了。该情况也是第一次遇到,网上其他解释基本上都是第一种情况(即使用迭代器)发生。其实无论是第一种情况还是第二种情况,本质都是因为原集合的modCount被修改,导致与SubList的modCount或者是迭代器的expectedModCount不同导致的。

欢迎关注个人公众号:
个人公号

博主在学习并发编程的过程当中,使用的书籍是《Java并发编程实战》这本书,但阅读下来,只能说本书的内容是很适合学习的,但是不知道是因为原版英语图书本身的写作问题,还是译者的翻译问题,本书的第一部分——基础知识 阅读起来难以理解。书中使用了大量的并发编程领域的专业词汇,由于本书不是很好阅读,这是博主半年后第二次尝试阅读该书,终于理解了书中内容,尤其是第一部分。为了方便后续的学习,在这里先把第一部分的内容梳理一下,对几个重要的关键词汇做一些解释。

线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

不变性

书中在介绍不变性时讲的是不可变对象,但是在书中很多地方提到的却是不变性条件,其实不变性和不可变对象是两回事。所谓的不可变性条件,是指在程序执行过程或部分过程中,可始终被假定成立的条件。而不可变对象则是一种实例对象。

不可变对象

要使一个对象成为不可变对象,需要满足以下三个条件:

  • 对象创建后其状态就不能修改
  • 对象的所有与都是final类型的
  • 对象被正确地创建了(在对象的创建期间,this引用没有逸出)。

事实不可变对象

如果对象从技术上来看是可变的,但其状态在对象被创建后就不会再变化,那么把这种对象成为“事实不可变对象”。事实不可变对象不需要满足不可变对喜爱那个的前两个条件。

原子性

一段代码或者一句代码包含多个操作,这些操作要么全部执行,要么全都不执行,称为原子性。

竞态条件

在并发编程中,由于操作不具备原子性,因此由于不恰当的执行时序而出现不正确的结果。最常见的竞态条件:

  1. 先检测后执行
    竞态条件
    对于main线程,如果文件a不存在,则创建文件a,但是在判断文件a不存在之后,Task线程创建了文件a,这时候先前的判断结果已经失效,(main线程的执行依赖了一个错误的判断结果)此时文件a已经存在了,但是main线程还是会继续创建文件a,导致Task线程创建的文件a被覆盖、文件中的内容丢失等等问题。

  2. 延迟初始化(典型即为单例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class ObjFactory {  
    private Obj instance;

    public Obj getInstance(){
    if(instance == null){
    instance = new Obj();
    }
    return instance;
    }
    }

    单例模式
    线程a和线程b同时执行getInstance(),线程a看到instance为空,创建了一个新的Obj对象,此时线程b也需要判断instance是否为空,此时的instance是否为空取决于不可预测的时序:包括线程a创建Obj对象需要多长时间以及线程的调度方式,如果b检测时,instance为空,那么b也会创建一个instance对象。因此,使用单例模式必须在判断instance == null之前加锁。顺带一提,之所以在加锁前需要再判断一次instance == null,是为了防止在instance已经创建的情况下,线程无谓地获取锁导致的开销。该单例的创建方式称为双重检查锁定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ObjFactory {  
    private Obj instance;

    public Obj getInstance(){
    if(instance == null){
    synchronous(ObjFactory.class) {
    if(instance == null) {
    instance = new Obj();
    }
    }
    }
    return instance;
    }
    }

可见性

所谓的可见性,我们可以简单的理解为线程能够看到某一个变量的最新值。如果一个变量是不可见的,则线程可能会获取到一个失效值,导致程序出项意想不到的错误。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NoVisibility {
private static boolean ready;
private static int number;
}

private static class ReaderThread extends Thread {
public void run() {
while(!ready) {
Thread.yield();
}
System.out.printLn(number);
}
}

public static class main(String[] args) {
new ReaderThread().start;
number = 42;
ready = true;
}

NoVosobility可能会持续循环下去,因为读线程可能永远都看不到ready的值。一种更奇怪的现象是,NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入的number的值,产生该问题的原因的Java编译器、处理器以及运行时可能的对操作的执行顺序重排序导致的。
为了使其他线程看到ready的最新值,需要将ready变量用volatile关键字进行修饰。关于volatile关键字的底层原理机制,将在后面一篇文章做专门的介绍。

发布和逸出

“发布”一个对象的意思是指,是对象能够在当前作用域之外的代码中使用。反之,当某个不应该发布的对象被发布时,就称为“逸出”
发布一个对象的安全方式:

  1. 在静态初始化函数中初始化一个对象的引用
  2. 将对象的引用保存到volatile类型的域中或AtomicReference对象中。
  3. 将对象的引用保存到某个正确构造的对喜爱那个的final类型域中
  4. 将对象的引用保存到一个由锁保护的域中

而对象的发布方式,则取决于它的可见性:

  1. 不可变对象可通过任意方式来发布。
  2. 事实不可变对象必须通过安全方式来发布。
  3. 可变对象必须通过安全方式来发布,并且必须是线程安全的或者有某个锁来保护。

两种逃逸的情况:

1
2
3
4
5
6
7
8
9
10
11
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener {
public void onEvent(Event e) {
doSomething(e);
}
}
)
}
}

该例子当中,在ThisEscape的构造函数还没有退出时,this引用隐式地逸出到了匿名内部类中。

第二种逸出方式是,在构造函数中调用了一个可改写的实例方法(既不是私有方法,也不是终结方法)。更具体地说,就是在创建子类的过程当中,首先会调用父类的构造方法进行父类数据的构造,如果在父类的构造方法中调用了被子类重载的方法,相当于还没有构造完全的父类引用,逃逸到了子类的重构方法代码中了。

关于并发编程中的一些术语就先介绍到这里,上述的几个术语基本上就是并发编程中的重点概念,在下一节当中,将讨论如何实现线程安全类。

欢迎关注个人公众号:
个人公号

0%