Java8新特性教程

接口的默认方法

Java8允许开发者通过使用关键字 default  向接口中加入非抽象方法。这一新的特性被称之为扩展方法。下面是我们的第一个例子:
  1. interface Formula {
  2.     double calculate(int a);
  3.     default double sqrt(int a) {
  4.         return Math.sqrt(a);
  5.     }
  6. }

在抽象方法calculator之外,接口Formula还定义了一个默认方法sqrt。实现类只需要实现抽象方法calculate。默认方法sqrt可以在定义之外使用。如:

  1. Formula formula = new Formula() {
  2.     @Override
  3.     public double calculate(int a) {
  4.         return sqrt(a * 100);
  5.     }
  6. };
  7. formula.calculate(100);     // 100.0
  8. formula.sqrt(16);           // 4.0

formula被实现为一个匿名类。代码有点啰嗦:六行代码里就就只有一句简单的计算:sqrt(a*100)。我们将在下面部分看到一种用java8实现的更加简洁的办法。

Lambda表达式

让我们使用一个简单的例子来展示在java8以前是如何对字符串列表进行排序的:
  1. List<String> names = Arrays.asList(“peter”“anna”“mike”“xenia”);
  2. Collections.sort(names, new Comparator<String>() {
  3.     @Override
  4.     public int compare(String a, String b) {
  5.         return b.compareTo(a);
  6.     }
  7. });

这个静态工具方法Collections.sort接受一个列表和一个用于元素比较的比较器。你会发现自己会经常创建匿名类并把它们传递给排序方法。

为了不用整天创建这些匿名类,java8带来了一个非常简短的语法–lambda表达式:
  1. Collections.sort(names, (String a, String b) -> {
  2.     return b.compareTo(a);
  3. });

现在的代码已经变得简短和便于阅读。但是,实际上,它可以变得更加简短:

  1. Collections.sort(names, (String a, String b) -> b.compareTo(a));
对于这种一行代码体的表达式,你可以直接省略掉大括号{}和return关键字。它就变成下面这种更加简短的写法:
  1. Collections.sort(names, (a, b) -> b.compareTo(a));

java编译器能够探测到这些参数的类型,这样使得的你可以直接跳过它们。下面我们来解答为什么lambda表示式可以这样随意的使用。

功能性接口

lambda表达式是如何和java系统的类型进行对应的?每个lambda表达式都对应一个指定的类型,这个指定的类型是由接口确定的。该接口被称之为功能性接口,它必须且恰好只包含一个抽象方法声明。被指定接口类型所对应的lambda表达式刚好和这个接口的抽象方法想匹配。因为默认方法不是抽象的,因此你可以在你的功能性接口中添加多个默认方法。
我们可以将任意的接口用作lambda表示式,只要该接口仅仅包含一个抽象方法。为了确保你定义的接口达到要求,你可以在接口上添加@FunctionInterface注解。编译器可以检测到该注解并判断你的接口是否满足条件,如果 你定义的接口包含多个抽象方法时,编译器便会报错。
示例:
  1. @FunctionalInterface
  2. interface Converter<F, T> {
  3.     T convert(F from);
  4. }
  1. Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
  2. Integer converted = converter.convert(“123”);
  3. System.out.println(converted);    // 123

如果FunctionInterface注解被添加,你定义的接口将总会被检测。

方法和构造函数引用

前部分的示例在使用静态方法引用的情况下可以被进一步的简化:

  1. Converter<String, Integer> converter = Integer::valueOf;
  2. Integer converted = converter.convert(“123”);
  3. System.out.println(converted);   // 123

java8可以让你通过关键字::来传递方法和构造函数的引用。上面的示例展示了如何引用一个静态方法。我们同样也可以引用对象方法。

  1. class Something {
  2.     String startsWith(String s) {
  3.         return String.valueOf(s.charAt(0));
  4.     }
  5. }
  1. Something something = new Something();
  2. Converter<String, String> converter = something::startsWith;
  3. String converted = converter.convert(“Java”);
  4. System.out.println(converted);    // “J”

现在我们将看到关键字::如何为构造函数工作。首先我们定义一个拥有不同构造函数的bean类:

  1. class Person {
  2.     String firstName;
  3.     String lastName;
  4.     Person() {}
  5.     Person(String firstName, String lastName) {
  6.         this.firstName = firstName;
  7.         this.lastName = lastName;
  8.     }
  9. }

接下来我们定义一个用来创建类person的工厂接口:

  1. </pre><pre code_snippet_id=“265470” snippet_file_name=“blog_20140330_13_2612710” name=“code” class=“java”>interface PersonFactory<P extends Person> {
  2.     P create(String firstName, String lastName);
  3. }
不使用通常的手动实现工厂类,我们通过使用构造函数将所有的工作联合在一起:
  1. PersonFactory<Person> personFactory = Person::new;
  2. Person person = personFactory.create(“Peter”“Parker”);

我们通过Person::new创建一个指向Person构造函数的引用。java编译器自动的选择正确的构造函数来匹配PersonFactory.create的函数签名。

Lambda范围

在lambda表达式里访问外部变量和匿名类的方式是十分相似的。你可以在lambda中访问外部的final变量,访问实例字段和静态变量的方法也是如此。

访问本地变量

我们可以访问在lambda表示式之外的本地final变量:
  1. final int num = 1;
  2. Converter<Integer, String> stringConverter =
  3.         (from) -> String.valueOf(from + num);
  4. stringConverter.convert(2);     // 3

但是和匿名变量不同的是变量num不必强制的被声明为final。下面的代码依然是合法的:

  1. int num = 1;
  2. Converter<Integer, String> stringConverter =
  3.         (from) -> String.valueOf(from + num);
  4. stringConverter.convert(2);     // 3

但是实际上,变量num在编译期是被隐式的转换为fianl类型的。下面的代码是不能被成功的编译的:

  1. int num = 1;
  2. Converter<Integer, String> stringConverter =
  3.         (from) -> String.valueOf(from + num);
  4. num = 3;

在lambda表达式内部向变量num写入值同样是不允许的。

访问对象字段和静态变量

和访问本地变量相反,我们在lambda表达式里即可以读取也可以写入对象字段和静态变量。这一准则同样适用于匿名类。
  1. class Lambda4 {
  2.     static int outerStaticNum;
  3.     int outerNum;
  4.     void testScopes() {
  5.         Converter<Integer, String> stringConverter1 = (from) -> {
  6.             outerNum = 23;
  7.             return String.valueOf(from);
  8.         };
  9.         Converter<Integer, String> stringConverter2 = (from) -> {
  10.             outerStaticNum = 72;
  11.             return String.valueOf(from);
  12.         };
  13.     }
  14. }

访问接口默认方法

还记得第一部分的formula示例么?接口formula定义了一个默认方法sqrt,这个方法可以被formula的实例和匿名实例所访问。
但是这个方法不能被lambda表达式所访问。
默认方法不能被lambda表示式内部的代码访问。下面的代码不能通过编译。
  1. Formula formula = (a) -> sqrt( a * 100);

内建的功能性接口

JDK1.8包括了许多功能性接口。它们中的一些是老版本中被熟知的接口,例如Comparator和Runnable。这些已存在的接口已经通过@FunctionalInterface注解扩展为支持Lambda表达式。
同时Java8的API也包含了很多新的功能性接口简化你的开发。一些新的接口是来自非常出名的Google Guava库。即使你已经对这库十分熟悉了,你也应当留意下这些接口是如何被扩展的。

断言接口(Predicates)

Predicates是只拥有一个参数的Boolean型功能的接口。这个接口拥有多个默认方法用于构成predicates复杂的逻辑术语。
  1. Predicate<String> predicate = (s) -> s.length() > 0;
  2. predicate.test(“foo”);              // true
  3. predicate.negate().test(“foo”);     // false
  4. Predicate<Boolean> nonNull = Objects::nonNull;
  5. Predicate<Boolean> isNull = Objects::isNull;
  6. Predicate<String> isEmpty = String::isEmpty;
  7. Predicate<String> isNotEmpty = isEmpty.negate();

功能接口(Functions)

Functions接受一个参数并产生一个结果。默认方法能够用于将多个函数链接在一起。
  1. Function<String, Integer> toInteger = Integer::valueOf;
  2. Function<String, String> backToString = toInteger.andThen(String::valueOf);
  3. backToString.apply(“123”);     // “123”

供应接口(Suppliers)

Suppliers对于给定的泛型类型产生一个实例。不同于Functions,Suppliers不需要任何参数。
  1. Supplier<Person> personSupplier = Person::new;
  2. personSupplier.get();   // new Person

消费接口(Consumers)

Consumers代表在只有一个输入参数时操作被如何执行。
  1. Consumer<Person> greeter = (p) -> System.out.println(“Hello, “ + p.firstName);
  2. greeter.accept(new Person(“Luke”“Skywalker”));

比较接口(Comparators)

Comparators在老版本中就已经被熟知。Java8向该接口中添加了多种默认方法。
  1. Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
  2. Person p1 = new Person(“John”“Doe”);
  3. Person p2 = new Person(“Alice”“Wonderland”);
  4. comparator.compare(p1, p2);             // > 0
  5. comparator.reversed().compare(p1, p2);  // < 0

选项接口(Optionals)

Optionals并不是功能性接口,反而它是一种特殊的工具用来阻止NullPointerException。我们首先快速的浏览Optionals是如何工作的,因为它在下一章节是十分重要的概念。

Optional是一种可以包含null和non-null值的简单容器。考虑到方法可以返回non-null结果,偶尔也可能任何都不返回。在Java8中,你可以返回Optional而不是返回null。
  1. Optional<String> optional = Optional.of(“bam”);
  2. optional.isPresent();           // true
  3. optional.get();                 // “bam”
  4. optional.orElse(“fallback”);    // “bam”
  5. optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // “b”

流接口(Streams)

java.util.Stream代表着一串你可以在其上进行多种操作的元素。流操作既可以是连续的也可以是中断的。中断操作返回操作结果。而连续操作返回流本身,这样你就可以在该行上继续操作。流是创建在数据源上的,例如:java.util.Collection、list集合和set集合。流操作既可以顺序执行也可以并行执行。

我们首先了解下顺序的流是如何工作的。我们首先创建一个字符串链表。
  1. List<String> stringCollection = new ArrayList<>();
  2. stringCollection.add(“ddd2”);
  3. stringCollection.add(“aaa2”);
  4. stringCollection.add(“bbb1”);
  5. stringCollection.add(“aaa1”);
  6. stringCollection.add(“bbb3”);
  7. stringCollection.add(“ccc”);
  8. stringCollection.add(“bbb2”);
  9. stringCollection.add(“ddd1”);

Java8的Collections类已经被扩展了,你可以简单的调用Collection.stream()或者Collection.parallelSteam()来创建流。下面部分将介绍大部分流操作。

Filter

Filter接受一个predicate来过滤流中的所有元素。这个操作是连续的,它可以让我们在结果上继续调用另外一个流操作forEach。ForEach接受一个consumer,它被用来对过滤流中的每个元素执行操作。ForEach是一个中断操作,因此我们不能在ForEach后调用其他流操作。
  1. stringCollection
  2.     .stream()
  3.     .filter((s) -> s.startsWith(“a”))
  4.     .forEach(System.out::println);
  5. // “aaa2”, “aaa1”

Sorted

Sorted是一个连续操作,它返回流的已排序版本。如果你没有显示的指定Comparator,那么流中元素的排序规则为默认的。
  1. stringCollection
  2.     .stream()
  3.     .sorted()
  4.     .filter((s) -> s.startsWith(“a”))
  5.     .forEach(System.out::println);
  6. // “aaa1”, “aaa2”

需要注意的是sorted只创建了流的排序结果,它并没有改变集合中元素的排序位置。stringCollection中元素排序是没有改变的。

  1. System.out.println(stringCollection);
  2. // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map

连续性操作map通过指定的Function将流中的每个元素转变为另外的对象。下面的示例将每个字符串转换为大写的字符串。此外,你也可以使用map将每个元素的类型改变为其它类型。转换后流的泛型类型依赖于你传入的Function的泛型类型。
  1. stringCollection
  2.     .stream()
  3.     .map(String::toUpperCase)
  4.     .sorted((a, b) -> b.compareTo(a))
  5.     .forEach(System.out::println);
  6. // “DDD2”, “DDD1”, “CCC”, “BBB3”, “BBB2”, “AAA2”, “AAA1”

Match

各种匹配操作可以用来检测是否某种predicate和流中元素相匹配。所有的这些操作是中断的并返回一个boolean结果。
  1. boolean anyStartsWithA =
  2.     stringCollection
  3.         .stream()
  4.         .anyMatch((s) -> s.startsWith(“a”));
  5. System.out.println(anyStartsWithA);      // true
  6. boolean allStartsWithA =
  7.     stringCollection
  8.         .stream()
  9.         .allMatch((s) -> s.startsWith(“a”));
  10. System.out.println(allStartsWithA);      // false
  11. boolean noneStartsWithZ =
  12.     stringCollection
  13.         .stream()
  14.         .noneMatch((s) -> s.startsWith(“z”));
  15. System.out.println(noneStartsWithZ);      // true

Count

Count是中断型操作,它返回流中的元素数量。
  1. long startsWithB =
  2.     stringCollection
  3.         .stream()
  4.         .filter((s) -> s.startsWith(“b”))
  5.         .count();
  6. System.out.println(startsWithB);    // 3

Reduce

这个中断性操作使用指定的function对流中元素实施消减策略。此操作的返回值是一个包括所有被消减元素的Optional。
  1. Optional<String> reduced =
  2.     stringCollection
  3.         .stream()
  4.         .sorted()
  5.         .reduce((s1, s2) -> s1 + “#” + s2);
  6. reduced.ifPresent(System.out::println);
  7. // “aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2”

Parallel Streams

在前面部分我们提到流可以是顺序的也可以是并行的。顺序流的操作是在单线程上执行的,而并行流的操作是在多线程上并发执行的。
随后的例子我们展示了并行流可以多么容易的提高性能。
首先,我们创建一个包含唯一元素的大容器:
  1. int max = 1000000;
  2. List<String> values = new ArrayList<>(max);
  3. for (int i = 0; i < max; i++) {
  4.     UUID uuid = UUID.randomUUID();
  5.     values.add(uuid.toString());
  6. }

现在我们开始测试排序这些元素需要多长时间。

Sequential Sort
  1. long t0 = System.nanoTime();
  2. long count = values.stream().sorted().count();
  3. System.out.println(count);
  4. long t1 = System.nanoTime();
  5. long millis = TimeUnit.NANOSECONDS.toMillis(t1 – t0);
  6. System.out.println(String.format(“sequential sort took: %d ms”, millis));
  7. // sequential sort took: 899 ms

Parallel Sort

  1. long t0 = System.nanoTime();
  2. long count = values.parallelStream().sorted().count();
  3. System.out.println(count);
  4. long t1 = System.nanoTime();
  5. long millis = TimeUnit.NANOSECONDS.toMillis(t1 – t0);
  6. System.out.println(String.format(“parallel sort took: %d ms”, millis));
  7. // parallel sort took: 472 ms

你会观察到这两种模式的代码基本上市一致的,但是并行排序所花费的时间大约是顺序排序的一半。

Map

我们已经提到maps不支持流。然而现在maps包括了许多新的非常有用的方法用于执行通用任务。
  1. Map<Integer, String> map = new HashMap<>();
  2. for (int i = 0; i < 10; i++) {
  3.     map.putIfAbsent(i, “val” + i);
  4. }
  5. map.forEach((id, val) -> System.out.println(val));
  1. </pre>上述的代码应该很清晰了:putIfAbsent使得我们不用写是否为null值的检测语句;forEach使用consumer来对map中的每个元素进行操作。</div><div></div><div>下面的例子向我们展示使用功能性函数在map里执行代码:</div><div><pre code_snippet_id=“265470” snippet_file_name=“blog_20140330_37_9422552” name=“code” class=“java”>map.computeIfPresent(3, (num, val) -> val + num);
  2. map.get(3);             // val33
  3. map.computeIfPresent(9, (num, val) -> null);
  4. map.containsKey(9);     // false
  5. map.computeIfAbsent(23, num -> “val” + num);
  6. map.containsKey(23);    // true
  7. map.computeIfAbsent(3, num -> “bam”);
  8. map.get(3);             // val33

接下来,我们将学习如何删除给定键所对应的元素。删除操作还需要满足给定的值需要和map中的值想等:

  1. map.remove(3“val3”);
  2. map.get(3);             // val33
  3. map.remove(3“val33”);
  4. map.get(3);             // null

其他一些帮助性方法:

  1. map.getOrDefault(42“not found”);  // not found

合并map中的实体是十分容易的:

  1. map.merge(9“val9”, (value, newValue) -> value.concat(newValue));
  2. map.get(9);             // val9
  3. map.merge(9“concat”, (value, newValue) -> value.concat(newValue));
  4. map.get(9);             // val9concat

如果map不存在指定的键,那么它将把该键值对key/value加入map中。反而,如果存在,它将调用function来进行合并操作。

Date API

Java8在包java.time下面包括了一款新的date和time的API。新的Date API和Joda-Time库是相兼容的,但是它们不是一样的。下面的示例覆盖了新API中的重要部分。

Clock

Clock提供了访问当前日期和时间的方法。Clock是时区敏感的并且它可以被用来替代System.currentTimeMillis进行获取当前毫秒数。同时,时间轴上的时间点是可以用类Instant来表示的。Instants可以被用来创建遗留的java.util.Date对象。
  1. Clock clock = Clock.systemDefaultZone();
  2. long millis = clock.millis();
  3. Instant instant = clock.instant();
  4. Date legacyDate = Date.from(instant);   // legacy java.util.Date

TimeZones

TimeZones被用来表示ZoneId。它们可以通过静态工厂方法访问。TImeZones定义了时差,它在instants和本地日期时间转换上十分重要。
  1. System.out.println(ZoneId.getAvailableZoneIds());
  2. // prints all available timezone ids
  3. ZoneId zone1 = ZoneId.of(“Europe/Berlin”);
  4. ZoneId zone2 = ZoneId.of(“Brazil/East”);
  5. System.out.println(zone1.getRules());
  6. System.out.println(zone2.getRules());
  7. // ZoneRules[currentStandardOffset=+01:00]
  8. // ZoneRules[currentStandardOffset=-03:00]

LocalTime

本地时间代表了一个和时区无关的时间,e.g. 10pm or 17:30:15. 下面的示例创建了前部分展示的两个时区的本地时间。然后,我们将比较这两个时间并计算出这两个时间在小时和分钟数上的差异。
  1. LocalTime now1 = LocalTime.now(zone1);
  2. LocalTime now2 = LocalTime.now(zone2);
  3. System.out.println(now1.isBefore(now2));  // false
  4. long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
  5. long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
  6. System.out.println(hoursBetween);       // -3
  7. System.out.println(minutesBetween);     // -239

LocalTime包含了多个工厂方法用来简化创建过程,其中也包括通过字符串来创建时间:

  1. LocalTime late = LocalTime.of(235959);
  2. System.out.println(late);       // 23:59:59
  3. DateTimeFormatter germanFormatter =
  4.     DateTimeFormatter
  5.         .ofLocalizedTime(FormatStyle.SHORT)
  6.         .withLocale(Locale.GERMAN);
  7. LocalTime leetTime = LocalTime.parse(“13:37”, germanFormatter);
  8. System.out.println(leetTime);   // 13:37

LocalDate

LocalDate代表了一个可区分日期,e.g. 2014-03-11。 它是不变的同时工作原理类似于LocalTime。下面的例子描绘了通过加减年,月,日来计算出一个新的日期。需要注意的是这每个操作都返回一个新的实例。
  1. LocalDate today = LocalDate.now();
  2. LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
  3. LocalDate yesterday = tomorrow.minusDays(2);
  4. LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
  5. DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
  6. System.out.println(dayOfWeek);    // FRIDAY

从字符串解析出LocalDate和解析LocalTime一样简单:

  1. DateTimeFormatter germanFormatter =
  2.     DateTimeFormatter
  3.         .ofLocalizedDate(FormatStyle.MEDIUM)
  4.         .withLocale(Locale.GERMAN);
  5. LocalDate xmas = LocalDate.parse(“24.12.2014”, germanFormatter);
  6. System.out.println(xmas);   // 2014-12-24

LocalDateTime

LocalDateTime代表日期和时间。它将我们前部分看到的时间和日期组合进一个实例。LocalDateTime是不可变的并且它的工作原理和LocalTime和LocalDate十分相似。
我们可以从date-time中获取某些字段值:
  1. LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31235959);
  2. DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
  3. System.out.println(dayOfWeek);      // WEDNESDAY
  4. Month month = sylvester.getMonth();
  5. System.out.println(month);          // DECEMBER
  6. long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
  7. System.out.println(minuteOfDay);    // 1439

在一些额外的时区信息帮助下,它可以被转换为instant。Instants可以被容易的转换为遗留的java.util.Date类型。

  1. Instant instant = sylvester
  2.         .atZone(ZoneId.systemDefault())
  3.         .toInstant();
  4. Date legacyDate = Date.from(instant);
  5. System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

格式date-time的过程和格式date和time基本上是一样的。在使用系统自带的定义格式时,我们也可以定义我们自己的格式:

  1. DateTimeFormatter formatter =
  2.     DateTimeFormatter
  3.         .ofPattern(“MMM dd, yyyy – HH:mm”);
  4. LocalDateTime parsed = LocalDateTime.parse(“Nov 03, 2014 – 07:13”, formatter);
  5. String string = formatter.format(parsed);
  6. System.out.println(string);     // Nov 03, 2014 – 07:13

和java.text.NumberFormat不一样的是DateTimeFormatter是不可变的并且是类型安全的。

如果想了解详细的格式语法,可以阅读这里

Annotations

Java8中的Annotations是可重复。现在我们深入的学习一个例子来理解它。
首先,我们定义一个包装注解,它包含了一个实际注解的数组。
  1. @interface Hints {
  2.     Hint[] value();
  3. }
  4. @Repeatable(Hints.class)
  5. @interface Hint {
  6.     String value();
  7. }

Java8可以使同一个注解类型同时使用多次,只要我们在注解声明时使用@Repeatable。

情景1:使用容器注解
  1. @Hints({@Hint(“hint1”), @Hint(“hint2”)})
  2. class Person {}

情景2:使用可重复注解

  1. @Hint(“hint1”)
  2. @Hint(“hint2”)
  3. class Person {}

在第二种情景下,java编译器隐式的在该注解使用中加入@Hints。这种后期处理在通过反射获取注解是十分重要的。

  1. Hint hint = Person.class.getAnnotation(Hint.class);
  2. System.out.println(hint);                   // null
  3. Hints hints1 = Person.class.getAnnotation(Hints.class);
  4. System.out.println(hints1.value().length);  // 2
  5. Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
  6. System.out.println(hints2.length);          // 2

虽然我们从来没有在类Person上声明@Hints注解,但该信息还是可以通过getAnnotation(Hint.class)获得。 此外,getAnnotationsByType是一种更加便利的方法,它可以保证我们访问所有使用的@Hint注解。

此外,Java8中注解的使用范围扩展到两种新的类型:
  1. @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
  2. @interface MyAnnotation {}

总结

我的Java8语言特性教程到此就结束了。此外还有很多新的内容需要阐述。去不去了解JDK8中的这些非常棒的特性取决于你,这些特性包括有Arrays.parallelSort,StampedLock,CompletableFuture  —即使列举名字也有很多了。我已经在网站上把这些特性都列举出来了,你可以去哪里看看。
原文地址: http://winterbe.com/posts/2014/03/16/java-8-tutorial/

对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解

根据 Wiki 对 Zero-copy 的定义:

“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.

即所谓的 Zero-copy, 就是在操作数据时, 不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域. 因为少了一次内存的拷贝, 因此 CPU 的效率就得到的提升.

在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space)内核态(Kernel-space) 之间来回拷贝数据. 例如 Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间; 同样地, 内核空间对这段区域的修改也直接反映用户空间. 正因为有这样的映射关系, 我们就不需要在 用户态(User-space)内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率.

而需要注意的是, Netty 中的 Zero-copy 与上面我们所提到到 OS 层面上的 Zero-copy 不太一样, Netty的 Zero-coyp 完全是在用户态(Java 层面)的, 它的 Zero-copy 的更多的是偏向于 优化数据操作 这样的概念.

Netty 的 Zero-copy 体现在如下几个个方面:

  • Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝.
  • 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作.
  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝.
  • 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.

下面我们就来简单了解一下这几种常见的零拷贝操作.

通过 CompositeByteBuf 实现零拷贝

假设我们有一份协议数据, 它由头部和消息体组成, 而头部和消息体是分别存放在两个 ByteBuf 中的, 即:

ByteBuf header = ...
ByteBuf body = ...

我们在代码处理中, 通常希望将 header 和 body 合并为一个 ByteBuf, 方便处理, 那么通常的做法是:

ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

可以看到, 我们将 header 和 body 都拷贝到了新的 allBuf 中了, 这无形中增加了两次额外的数据拷贝操作了.

那么有没有更加高效优雅的方式实现相同的目的呢? 我们来看一下 CompositeByteBuf 是如何实现这样的需求的吧.

ByteBuf header = ...
ByteBuf body = ...

CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);

上面代码中, 我们定义了一个 CompositeByteBuf 对象, 然后调用

public CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers) {
...
}

方法将 headerbody 合并为一个逻辑上的 ByteBuf, 即:

不过需要注意的是, 虽然看起来 CompositeByteBuf 是由两个 ByteBuf 组合而成的, 不过在 CompositeByteBuf 内部, 这两个 ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体.

上面 CompositeByteBuf 代码还以一个地方值得注意的是, 我们调用 addComponents(boolean increaseWriterIndex, ByteBuf... buffers) 来添加两个 ByteBuf, 其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex.
如果我们调用的是

compositeByteBuf.addComponents(header, body);

那么其实 compositeByteBufwriteIndex 仍然是0, 因此此时我们就不可能从 compositeByteBuf 中读取到数据, 这一点希望大家要特别注意.

除了上面直接使用 CompositeByteBuf 类外, 我们还可以使用 Unpooled.wrappedBuffer 方法, 它底层封装了 CompositeByteBuf 操作, 因此使用起来更加方便:

ByteBuf header = ...
ByteBuf body = ...

ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);

通过 wrap 操作实现零拷贝

例如我们有一个 byte 数组, 我们希望将它转换为一个 ByteBuf 对象, 以便于后续的操作, 那么传统的做法是将此 byte 数组拷贝到 ByteBuf 中, 即:

byte[] bytes = ...
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);

显然这样的方式也是有一个额外的拷贝操作的, 我们可以使用 Unpooled 的相关方法, 包装这个 byte 数组, 生成一个新的 ByteBuf 实例, 而不需要进行拷贝操作. 上面的代码可以改为:

byte[] bytes = ...
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

可以看到, 我们通过 Unpooled.wrappedBuffer 方法来将 bytes 包装成为一个 UnpooledHeapByteBuf 对象, 而在包装的过程中, 是不会有拷贝操作的. 即最后我们生成的生成的 ByteBuf 对象是和 bytes 数组共用了同一个存储空间, 对 bytes 的修改也会反映到 ByteBuf 对象中.

Unpooled 工具类还提供了很多重载的 wrappedBuffer 方法:

public static ByteBuf wrappedBuffer(byte[] array)
public static ByteBuf wrappedBuffer(byte[] array, int offset, int length)

public static ByteBuf wrappedBuffer(ByteBuffer buffer)
public static ByteBuf wrappedBuffer(ByteBuf buffer)

public static ByteBuf wrappedBuffer(byte[]... arrays)
public static ByteBuf wrappedBuffer(ByteBuf... buffers)
public static ByteBuf wrappedBuffer(ByteBuffer... buffers)

public static ByteBuf wrappedBuffer(int maxNumComponents, byte[]... arrays)
public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuf... buffers)
public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuffer... buffers)

这些方法可以将一个或多个 buffer 包装为一个 ByteBuf 对象, 从而避免了拷贝操作.

通过 slice 操作实现零拷贝

slice 操作和 wrap 操作刚好相反, Unpooled.wrappedBuffer 可以将多个 ByteBuf 合并为一个, 而 slice 操作可以将一个 ByteBuf 切片 为多个共享一个存储区域的 ByteBuf 对象.
ByteBuf 提供了两个 slice 操作方法:

public ByteBuf slice();
public ByteBuf slice(int index, int length);

不带参数的 slice 方法等同于 buf.slice(buf.readerIndex(), buf.readableBytes()) 调用, 即返回 buf 中可读部分的切片. 而 slice(int index, int length) 方法相对就比较灵活了, 我们可以设置不同的参数来获取到 buf 的不同区域的切片.

下面的例子展示了 ByteBuf.slice 方法的简单用法:

ByteBuf byteBuf = ...
ByteBuf header = byteBuf.slice(0, 5);
ByteBuf body = byteBuf.slice(5, 10);

slice 方法产生 header 和 body 的过程是没有拷贝操作的, header 和 body 对象在内部其实是共享了 byteBuf 存储空间的不同部分而已. 即:

通过 FileRegion 实现零拷贝

Netty 中使用 FileRegion 实现文件传输的零拷贝, 不过在底层 FileRegion 是依赖于 Java NIO FileChannel.transfer 的零拷贝功能.

首先我们从最基础的 Java IO 开始吧. 假设我们希望实现一个文件拷贝的功能, 那么使用传统的方式, 我们有如下实现:

public static void copyFile(String srcFile, String destFile) throws Exception {
    byte[] temp = new byte[1024];
    FileInputStream in = new FileInputStream(srcFile);
    FileOutputStream out = new FileOutputStream(destFile);
    int length;
    while ((length = in.read(temp)) != -1) {
        out.write(temp, 0, length);
    }

    in.close();
    out.close();
}

上面是一个典型的读写二进制文件的代码实现了. 不用我说, 大家肯定都知道, 上面的代码中不断中源文件中读取定长数据到 temp 数组中, 然后再将 temp 中的内容写入目的文件, 这样的拷贝操作对于小文件倒是没有太大的影响, 但是如果我们需要拷贝大文件时, 频繁的内存拷贝操作就消耗大量的系统资源了.
下面我们来看一下使用 Java NIO 的 FileChannel 是如何实现零拷贝的:

public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
    RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r");
    FileChannel srcFileChannel = srcFile.getChannel();

    RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw");
    FileChannel destFileChannel = destFile.getChannel();

    long position = 0;
    long count = srcFileChannel.size();

    srcFileChannel.transferTo(position, count, destFileChannel);
}

可以看到, 使用了 FileChannel 后, 我们就可以直接将源文件的内容直接拷贝(transferTo) 到目的文件中, 而不需要额外借助一个临时 buffer, 避免了不必要的内存操作.

有了上面的一些理论知识, 我们来看一下在 Netty 中是怎么使用 FileRegion 来实现零拷贝传输一个文件的:

@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
    RandomAccessFile raf = null;
    long length = -1;
    try {
        // 1. 通过 RandomAccessFile 打开一个文件.
        raf = new RandomAccessFile(msg, "r");
        length = raf.length();
    } catch (Exception e) {
        ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');
        return;
    } finally {
        if (length < 0 && raf != null) {
            raf.close();
        }
    }

    ctx.write("OK: " + raf.length() + '\n');
    if (ctx.pipeline().get(SslHandler.class) == null) {
        // SSL not enabled - can use zero-copy file transfer.
        // 2. 调用 raf.getChannel() 获取一个 FileChannel.
        // 3. 将 FileChannel 封装成一个 DefaultFileRegion
        ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
    } else {
        // SSL enabled - cannot use zero-copy file transfer.
        ctx.write(new ChunkedFile(raf));
    }
    ctx.writeAndFlush("\n");
}

上面的代码是 Netty 的一个例子, 其源码在 netty/example/src/main/java/io/netty/example/file/FileServerHandler.java
可以看到, 第一步是通过 RandomAccessFile 打开一个文件, 然后 Netty 使用了 DefaultFileRegion 来封装一个 FileChannel 即:

new DefaultFileRegion(raf.getChannel(), 0, length)

当有了 FileRegion 后, 我们就可以直接通过它将文件的内容直接写入 Channel 中, 而不需要像传统的做法: 拷贝文件内容到临时 buffer, 然后再将 buffer 写入 Channel. 通过这样的零拷贝操作, 无疑对传输大文件很有帮助.

java多线程同步以及线程间通信详解&消费者生产者模式&死锁&Thread.join()(多线程编程之二)

本篇我们将讨论以下知识点:

1.线程同步问题的产生

什么是线程同步问题,我们先来看一段卖票系统的代码,然后再分析这个问题:
  1. public class Ticket implements Runnable
  2. {
  3.     //当前拥有的票数
  4.     private  int num = 100;
  5.     public void run()
  6.     {
  7.         while(true)
  8.         {
  9.                 if(num>0)
  10.                 {
  11.                     try{Thread.sleep(10);}catch (InterruptedException e){}
  12.                     //输出卖票信息
  13.                     System.out.println(Thread.currentThread().getName()+“…..sale….”+num–);
  14.                 }
  15.         }
  16.     }
  17. }

上面是卖票线程类,下来再来看看执行类:

  1. public class TicketDemo {
  2.     public static void main(String[] args)
  3.     {
  4.         Ticket t = new Ticket();//创建一个线程任务对象。
  5.         //创建4个线程同时卖票
  6.         Thread t1 = new Thread(t);
  7.         Thread t2 = new Thread(t);
  8.         Thread t3 = new Thread(t);
  9.         Thread t4 = new Thread(t);
  10.         //启动线程
  11.         t1.start();
  12.         t2.start();
  13.         t3.start();
  14.         t4.start();
  15.     }
  16. }

运行程序结果如下(仅截取部分数据):

从运行结果,我们就可以看出我们4个售票窗口同时卖出了1号票,这显然是不合逻辑的,其实这个问题就是我们前面所说的线程同步问题。不同的线程都对同一个数据进了操作这就容易导致数据错乱的问题,也就是线程不同步。那么这个问题该怎么解决呢?在给出解决思路之前我们先来分析一下这个问题是怎么产生的?我们声明一个线程类Ticket,在这个类中我们又声明了一个成员变量num也就是票的数量,然后我们通过run方法不断的去获取票数并输出,最后我们在外部类TicketDemo中创建了四个线程同时操作这个数据,运行后就出现我们刚才所说的线程同步问题,从这里我们可以看出产生线程同步(线程安全)问题的条件有两个:1.多个线程在操作共享的数据(num),2.操作共享数据的线程代码有多条(4条线程);既然原因知道了,那该怎么解决?
解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程时不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。 好了,思路知道了,我们就用java代码的方式来解决这个问题。
2.解决线程同步的两种典型方案
在java中有两种机制可以防止线程安全的发生,Java语言提供了一个synchronized关键字来解决这问题,同时在Java SE5.0引入了Lock锁对象的相关类,接下来我们分别介绍这两种方法
2.1通过锁(Lock)对象的方式解决线程安全问题
在给出解决代码前我们先来介绍一个知识点:Lock,锁对象。在java中锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但有的锁可以允许多个线程并发访问共享资源,比如读写锁,后面我们会分析)。在Lock接口出现之前,java程序是靠synchronized关键字(后面分析)实现锁功能的,而JAVA SE5.0之后并发包中新增了Lock接口用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁,缺点就是缺少像synchronized那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。接下来我们就来介绍Lock接口的主要API方便我们学习
方法 相关描述内容
void lock() 获取锁,调用该方法当前线程会获取锁,当获取锁后。从该方法返回
void lockInterruptibly()
throws InterruptedException
可中断获取锁和lock()方法不同的是该方法会响应中断,即在获取锁
中可以中断当前线程。例如某个线程在等待一个锁的控制权的这段时
间需要中断。
boolean tryLock() 尝试非阻塞获取锁,调用该方法后立即返回,如果能够获取锁则返回
true,否则返回false。
boolean tryLock(long time,TimeUnit unit)
throws  InterruptedException
超时获取锁,当前线程在以下3种情况返回:
1.当前线程在超时时间内获取了锁
2.当前线程在超时时间被中断
3.当前线程超时时间结束,返回false
void unlock() 释放锁
Condition newCondition() 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有
获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放
锁。
这里先介绍一下API,后面我们将结合Lock接口的实现子类ReentrantLock使用某些方法。
ReentrantLock(重入锁):
重入锁,顾名思义就是支持重新进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,也就是说在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性。这里的公平是在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平锁,反之,是不公平的。那么该如何使用呢?看范例代码:
1.同步执行的代码跟synchronized类似功能:
  1. ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁  
  2. ReentrantLock lock = new ReentrantLock(true); //公平锁  
  3. lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果  
  4. try {
  5.     //操作  
  6. finally {
  7.     lock.unlock();  //释放锁
  8. }

2.防止重复执行代码:

  1. ReentrantLock lock = new ReentrantLock();
  2. if (lock.tryLock()) {  //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果   
  3.     try {
  4.         //操作  
  5.     } finally {
  6.         lock.unlock();
  7.    }
  8. }

3.尝试等待执行的代码:

  1. ReentrantLock lock = new ReentrantLock(true); //公平锁  
  2. try {
  3.     if (lock.tryLock(5, TimeUnit.SECONDS)) {
  4.         //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行  
  5.        try {
  6.             //操作  
  7.         } finally {
  8.             lock.unlock();
  9.         }
  10.     }
  11. catch (InterruptedException e) {
  12.     e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                   
  13. }

这里有点需要特别注意的,把解锁操作放在finally代码块内这个十分重要。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。好了,ReentrantLock我们就简单介绍到这里,接下来我们通过ReentrantLock来解决前面卖票线程的线程同步(安全)问题,代码如下:

  1. import java.util.concurrent.locks.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. public class Ticket implements Runnable {
  4.     //创建锁对象
  5.     private Lock ticketLock = new ReentrantLock();
  6.     //当前拥有的票数
  7.     private int num = 100;
  8.     public void run() {
  9.         while (true) {
  10.             try {
  11.                 ticketLock.lock();//获取锁
  12.                 if (num > 0) {
  13.                     Thread.sleep(10);//输出卖票信息System.out.println(Thread.currentThread().getName()+”…..sale….”+num–); }
  14.                 } else {
  15.                     break;
  16.                 }
  17.             } catch (InterruptedException e) {
  18.                 Thread.currentThread().interrupt();//出现异常就中断
  19.             } finally {
  20.                 ticketLock.unlock();//释放锁
  21.             }
  22.         }
  23.     }
  24. }
TicketDemo类无需变化,运行结果正常(太多不贴了),线程安全问题就此解决。
2.2通过synchronied关键字的方式解决线程安全问题
在Java中内置了语言级的同步原语-synchronized,这个可以大大简化了Java中多线程同步的使用。从JAVA SE1.0开始,java中的每一个对象都有一个内部锁,如果一个方法使用synchronized关键字进行声明,那么这个对象将保护整个方法,也就是说调用该方法线程必须获得内部的对象锁。
  1. public synchronized void method{
  2.   //method body
  3. }

等价于

  1. private Lock ticketLock = new ReentrantLock();
  2. public void method{
  3.  ticketLock.lock();
  4.  try{
  5.   //…….
  6.  }finally{
  7.    ticketLock.unlock();
  8.  }
  9. }

从这里可以看出使用synchronized关键字来编写代码要简洁得多了。当然,要理解这一代码,我们必须知道每个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管那些调用wait的线程(wait()/notifyAll/notify())。同时我们必须明白一旦有一个线程通过synchronied方法获取到内部锁,该类的所有synchronied方法或者代码块都无法被其他线程访问直到当前线程释放了内部锁。刚才上面说的是同步方法,synchronized还有一种同步代码块的实现方式:

  1. Object obj = new Object();
  2. synchronized(obj){
  3.   //需要同步的代码
  4. }

其中obj是对象锁,可以是任意对象。那么我们就通过其中的一个方法来解决售票系统的线程同步问题:

  1. class Ticket implements Runnable
  2. {
  3.     private  int num = 100;
  4.     Object obj = new Object();
  5.     public void run()
  6.     {
  7.         while(true)
  8.         {
  9.             synchronized(obj)
  10.             {
  11.                 if(num>0)
  12.                 {
  13.                     try{Thread.sleep(10);}catch (InterruptedException e){}
  14.                     System.out.println(Thread.currentThread().getName()+“…..sale….”+num–);
  15.                 }
  16.             }
  17.         }
  18.     }
  19. }
嗯,同步代码块解决,运行结果也正常。到此同步问题也就解决了,当然代码同步也是要牺牲效率为前提的:
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。
同步的前提:同步中必须有多个线程并使用同一个锁。
3.线程间的通信机制
线程开始运行,拥有自己的栈空间,但是如果每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者是价值很小,如果多线程能够相互配合完成工作的话,这将带来巨大的价值,这也就是线程间的通信啦。在java中多线程间的通信使用的是等待/通知机制来实现的。
3.1synchronied关键字等待/通知机制:是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述的两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
等待/通知机制主要是用到的函数方法是notify()/notifyAll(),wait()/wait(long),wait(long,int),这些方法在上一篇文章都有说明过,这里就不重复了。当然这是针对synchronied关键字修饰的函数或代码块,因为要使用notify()/notifyAll(),wait()/wait(long),wait(long,int)这些方法的前提是对调用对象加锁,也就是说只能在同步函数或者同步代码块中使用。
3.2条件对象的等待/通知机制:所谓的条件对象也就是配合前面我们分析的Lock锁对象,通过锁对象的条件对象来实现等待/通知机制。那么条件对象是怎么创建的呢?
  1. //创建条件对象
  2. Condition conditionObj=ticketLock.newCondition();
就这样我们创建了一个条件对象。注意这里返回的对象是与该锁(ticketLock)相关的条件对象。下面是条件对象的API:
方法 函数方法对应的描述
void await() 将该线程放到条件等待池中(对应wait()方法)
void signalAll() 解除该条件等待池中所有线程的阻塞状态(对应notifyAll()方法)
void signal() 从该条件的等待池中随机地选择一个线程,解除其阻塞状态(对应notify()方法)
上述方法的过程分析:一个线程A调用了条件对象的await()方法进入等待状态,而另一个线程B调用了条件对象的signal()或者signalAll()方法,线程A收到通知后从条件对象的await()方法返回,进而执行后续操作。上述的两个线程通过条件对象来完成交互,而对象上的await()和signal()/signalAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。当然这样的操作都是必须基于对象锁的,当前线程只有获取了锁,才能调用该条件对象的await()方法,而调用后,当前线程将缩放锁。
这里有点要特别注意的是,上述两种等待/通知机制中,无论是调用了signal()/signalAll()方法还是调用了notify()/notifyAll()方法并不会立即激活一个等待线程。它们仅仅都只是解除等待线程的阻塞状态,以便这些线程可以在当前线程解锁或者退出同步方法后,通过争夺CPU执行权实现对对象的访问。到此,线程通信机制的概念分析完,我们下面通过生产者消费者模式来实现等待/通知机制。
4.生产者消费者模式
4.1单生产者单消费者模式
顾名思义,就是一个线程消费,一个线程生产。我们先来看看等待/通知机制下的生产者消费者模式:我们假设这样一个场景,我们是卖北京烤鸭店铺,我们现在只有一条生产线也只有一条消费线,也就是说只能生产线程生产完了,再通知消费线程才能去卖,如果消费线程没烤鸭了,就必须通知生产线程去生产,此时消费线程进入等待状态。在这样的场景下,我们不仅要保证共享数据(烤鸭数量)的线程安全,而且还要保证烤鸭数量在消费之前必须有烤鸭。下面我们通过java代码来实现:
北京烤鸭生产资源类KaoYaResource:
  1. public class KaoYaResource {
  2.     private String name;
  3.     private int count = 1;//烤鸭的初始数量
  4.     private boolean flag = false;//判断是否有需要线程等待的标志
  5.     /**
  6.      * 生产烤鸭
  7.      */
  8.     public synchronized void product(String name){
  9.         if(flag){
  10.             //此时有烤鸭,等待
  11.             try {
  12.                 this.wait();
  13.             } catch (InterruptedException e) {
  14.                 e.printStackTrace()
  15. ;
  16.             }
  17.         }
  18.         this.name=name+count;//设置烤鸭的名称
  19.         count++;
  20.         System.out.println(Thread.currentThread().getName()+“…生产者…”+this.name);
  21.         flag=true;//有烤鸭后改变标志
  22.         notifyAll();//通知消费线程可以消费了
  23.     }
  24.     /**
  25.      * 消费烤鸭
  26.      */
  27.     public synchronized void consume(){
  28.         if(flag){//如果没有烤鸭就等待
  29.             try{this.wait();}catch(InterruptedException e){}
  30.         }
  31.         System.out.println(Thread.currentThread().getName()+“…消费者……..”+this.name);//消费烤鸭1
  32.         flag = false;
  33.         notifyAll();//通知生产者生产烤鸭
  34.     }
  35. }
在这个类中我们有两个synchronized的同步方法,一个是生产烤鸭的,一个是消费烤鸭的,之所以需要同步是因为我们操作了共享数据count,同时为了保证生产烤鸭后才能消费也就是生产一只烤鸭后才能消费一只烤鸭,我们使用了等待/通知机制,wait()和notify()。当第一次运行生产现场时调用生产的方法,此时有一只烤鸭,即flag=false,无需等待,因此我们设置可消费的烤鸭名称然后改变flag=true,同时通知消费线程可以消费烤鸭了,即使此时生产线程再次抢到执行权,因为flag=true,所以生产线程会进入等待阻塞状态,消费线程被唤醒后就进入消费方法,消费完成后,又改变标志flag=false,通知生产线程可以生产烤鸭了………以此循环。
生产消费执行类Single_Producer_Consumer.java:
  1. public class Single_Producer_Consumer {
  2.     public static void main(String[] args)
  3.     {
  4.         KaoYaResource r = new KaoYaResource();
  5.         Producer pro = new Producer(r);
  6.         Consumer con = new Consumer(r);
  7.         //生产者线程
  8.         Thread t0 = new Thread(pro);
  9.         //消费者线程
  10.         Thread t2 = new Thread(con);
  11.         //启动线程
  12.         t0.start();
  13.         t2.start();
  14.     }
  15. }
  16. class Producer implements Runnable
  17. {
  18.     private KaoYaResource r;
  19.     Producer(KaoYaResource r)
  20.     {
  21.         this.r = r;
  22.     }
  23.     public void run()
  24.     {
  25.         while(true)
  26.         {
  27.             r.product(“北京烤鸭”);
  28.         }
  29.     }
  30. }
  31. class Consumer implements Runnable
  32. {
  33.     private KaoYaResource r;
  34.     Consumer(KaoYaResource r)
  35.     {
  36.         this.r = r;
  37.     }
  38.     public void run()
  39.     {
  40.         while(true)
  41.         {
  42.             r.consume();
  43.         }
  44.     }
  45. }

在这个类中我们创建两个线程,一个是消费者线程,一个是生产者线程,我们分别开启这两个线程用于不断的生产消费,运行结果如下:

很显然的情况就是生产一只烤鸭然后就消费一只烤鸭。运行情况完全正常,嗯,这就是单生产者单消费者模式。上面使用的是synchronized关键字的方式实现的,那么接下来我们使用对象锁的方式实现:KaoYaResourceByLock.java
  1. public class KaoyaResourceByLock {
  2.     private String name;
  3.     private int count = 1;//烤鸭的初始数量
  4.     private boolean flag = false;//判断是否有需要线程等待的标志
  5.     //创建一个锁对象
  6.     private Lock resourceLock=new ReentrantLock();
  7.     //创建条件对象
  8.     private Condition condition= resourceLock.newCondition();
  9.     /**
  10.      * 生产烤鸭
  11.      */
  12.     public  void product(String name){
  13.         resourceLock.lock();//先获取锁
  14.         try{
  15.             if(flag){
  16.                 try {
  17.                     condition.await();
  18.                 } catch (InterruptedException e) {
  19.                     e.printStackTrace();
  20.                 }
  21.             }
  22.             this.name=name+count;//设置烤鸭的名称
  23.             count++;
  24.             System.out.println(Thread.currentThread().getName()+“…生产者…”+this.name);
  25.             flag=true;//有烤鸭后改变标志
  26.             condition.signalAll();//通知消费线程可以消费了
  27.         }finally{
  28.             resourceLock.unlock();
  29.         }
  30.     }
  31.     /**
  32.      * 消费烤鸭
  33.      */
  34.     public  void consume(){
  35.         resourceLock.lock();
  36.         try{
  37.         if(!flag){//如果没有烤鸭就等待
  38.             try{condition.await();}catch(InterruptedException e){}
  39.         }
  40.         System.out.println(Thread.currentThread().getName()+“…消费者……..”+this.name);//消费烤鸭1
  41.         flag = false;
  42.         condition.signalAll();//通知生产者生产烤鸭
  43.         }finally{
  44.             resourceLock.unlock();
  45.         }
  46.     }
  47. }
代码变化不大,我们通过对象锁的方式去实现,首先要创建一个对象锁,我们这里使用的重入锁ReestrantLock类,然后通过手动设置lock()和unlock()的方式去获取锁以及释放锁。为了实现等待/通知机制,我们还必须通过锁对象去创建一个条件对象Condition,然后通过锁对象的await()和signalAll()方法去实现等待以及通知操作。Single_Producer_Consumer.java代码替换一下资源类即可,运行结果就不贴了,有兴趣自行操作即可。
4.2多生产者多消费者模式
分析完了单生产者单消费者模式,我们再来聊聊多生产者多消费者模式,也就是多条生产线程配合多条消费线程。既然这样的话我们先把上面的代码Single_Producer_Consumer.java类修改成新类,大部分代码不变,仅新增2条线程去跑,一条t1的生产  共享资源类KaoYaResource不作更改,代码如下:
  1. public class Mutil_Producer_Consumer {
  2.     public static void main(String[] args)
  3.     {
  4.         KaoYaResource r = new KaoYaResource();
  5.         Mutil_Producer pro = new Mutil_Producer(r);
  6.         Mutil_Consumer con = new Mutil_Consumer(r);
  7.         //生产者线程
  8.         Thread t0 = new Thread(pro);
  9.         Thread t1 = new Thread(pro);
  10.         //消费者线程
  11.         Thread t2 = new Thread(con);
  12.         Thread t3 = new Thread(con);
  13.         //启动线程
  14.         t0.start();
  15.         t1.start();
  16.         t2.start();
  17.         t3.start();
  18.     }
  19. class Mutil_Producer implements Runnable
  20. {
  21.     private KaoYaResource r;
  22.     Mutil_Producer(KaoYaResource r)
  23.     {
  24.         this.r = r;
  25.     }
  26.     public void run()
  27.     {
  28.         while(true)
  29.         {
  30.             r.product(“北京烤鸭”);
  31.         }
  32.     }
  33. }
  34. class Mutil_Consumer implements Runnable
  35. {
  36.     private KaoYaResource r;
  37.     Mutil_Consumer(KaoYaResource r)
  38.     {
  39.         this.r = r;
  40.     }
  41.     public void run()
  42.     {
  43.         while(true)
  44.         {
  45.             r.consume();
  46.         }
  47.     }
  48. }

就多了两条线程,我们运行代码看看,结果如下:


不对呀,我们才生产一只烤鸭,怎么就被消费了3次啊,有的烤鸭生产了也没有被消费啊?难道共享数据源没有进行线程同步?我们再看看之前的KaoYaResource.java
  1. public class KaoYaResource {
  2.     private String name;
  3.     private int count = 1;//烤鸭的初始数量
  4.     private boolean flag = false;//判断是否有需要线程等待的标志
  5.     /**
  6.      * 生产烤鸭
  7.      */
  8.     public synchronized void product(String name){
  9.         if(flag){
  10.             //此时有烤鸭,等待
  11.             try {
  12.                 this.wait();
  13.             } catch (InterruptedException e) {
  14.                 e.printStackTrace();
  15.             }
  16.         }
  17.         this.name=name+count;//设置烤鸭的名称
  18.         count++;
  19.         System.out.println(Thread.currentThread().getName()+“…生产者…”+this.name);
  20.         flag=true;//有烤鸭后改变标志
  21.         notifyAll();//通知消费线程可以消费了
  22.     }
  23.     /**
  24.      * 消费烤鸭
  25.      */
  26.     public synchronized void consume(){
  27.         if(!flag){//如果没有烤鸭就等待
  28.             try{this.wait();}catch(InterruptedException e){}
  29.         }
  30.         System.out.println(Thread.currentThread().getName()+“…消费者……..”+this.name);//消费烤鸭1
  31.         flag = false;
  32.         notifyAll();//通知生产者生产烤鸭
  33.     }
  34. }
共享数据count的获取方法都进行synchronized关键字同步了呀!那怎么还会出现数据混乱的现象啊?
分析:确实,我们对共享数据也采用了同步措施,而且也应用了等待/通知机制,但是这样的措施只在单生产者单消费者的情况下才能正确应用,但从运行结果来看,我们之前的单生产者单消费者安全处理措施就不太适合多生产者多消费者的情况了。那么问题出在哪里?可以明确的告诉大家,肯定是在资源共享类,下面我们就来分析问题是如何出现,又该如何解决?直接上图


解决后的资源代码如下只将if改为了while:

  1. public class KaoYaResource {
  2.     private String name;
  3.     private int count = 1;//烤鸭的初始数量
  4.     private boolean flag = false;//判断是否有需要线程等待的标志
  5.     /**
  6.      * 生产烤鸭
  7.      */
  8.     public synchronized void product(String name){
  9.         while(flag){
  10.             //此时有烤鸭,等待
  11.             try {
  12.                 this.wait();
  13.             } catch (InterruptedException e) {
  14.                 e.printStackTrace();
  15.             }
  16.         }
  17.         this.name=name+count;//设置烤鸭的名称
  18.         count++;
  19.         System.out.println(Thread.currentThread().getName()+“…生产者…”+this.name);
  20.         flag=true;//有烤鸭后改变标志
  21.         notifyAll();//通知消费线程可以消费了
  22.     }
  23.     /**
  24.      * 消费烤鸭
  25.      */
  26.     public synchronized void consume(){
  27.         while(!flag){//如果没有烤鸭就等待
  28.             try{this.wait();}catch(InterruptedException e){}
  29.         }
  30.         System.out.println(Thread.currentThread().getName()+“…消费者……..”+this.name);//消费烤鸭1
  31.         flag = false;
  32.         notifyAll();//通知生产者生产烤鸭
  33.     }
  34. }

运行代码,结果如下:


到此,多消费者多生产者模式也完成,不过上面用的是synchronied关键字实现的,而锁对象的解决方法也一样将之前单消费者单生产者的资源类中的if判断改为while判断即可代码就不贴了哈。不过下面我们将介绍一种更有效的锁对象解决方法,我们准备使用两组条件对象(Condition也称为监视器)来实现等待/通知机制,也就是说通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。有了前面的分析这里我们直接上代码:
  1. public class ResourceBy2Condition {
  2.     private String name;
  3.     private int count = 1;
  4.     private boolean flag = false;
  5.     //创建一个锁对象。
  6.     Lock lock = new ReentrantLock();
  7.     //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
  8.     Condition producer_con = lock.newCondition();
  9.     Condition consumer_con = lock.newCondition();
  10.     /**
  11.      * 生产
  12.      * @param name
  13.      */
  14.     public  void product(String name)
  15.     {
  16.         lock.lock();
  17.         try
  18.         {
  19.             while(flag){
  20.                 try{producer_con.await();}catch(InterruptedException e){}
  21.             }
  22.             this.name = name + count;
  23.             count++;
  24.             System.out.println(Thread.currentThread().getName()+“…生产者5.0…”+this.name);
  25.             flag = true;
  26. //          notifyAll();
  27. //          con.signalAll();
  28.             consumer_con.signal();//直接唤醒消费线程
  29.         }
  30.         finally
  31.         {
  32.             lock.unlock();
  33.         }
  34.     }
  35.     /**
  36.      * 消费
  37.      */
  38.     public  void consume()
  39.     {
  40.         lock.lock();
  41.         try
  42.         {
  43.             while(!flag){
  44.                 try{consumer_con.await();}catch(InterruptedException e){}
  45.             }
  46.             System.out.println(Thread.currentThread().getName()+“…消费者.5.0…….”+this.name);//消费烤鸭1
  47.             flag = false;
  48. //          notifyAll();
  49. //          con.signalAll();
  50.             producer_con.signal();//直接唤醒生产线程
  51.         }
  52.         finally
  53.         {
  54.             lock.unlock();
  55.         }
  56.     }
  57. }
从代码中可以看到,我们创建了producer_con 和consumer_con两个条件对象,分别用于监听生产者线程和消费者线程,在product()方法中,我们获取到锁后,
如果此时flag为true的话,也就是此时还有烤鸭未被消费,因此生产线程需要等待,所以我们调用生产线程的监控器producer_con的
await()的方法进入阻塞等待池;但如果此时的flag为false的话,就说明烤鸭已经消费完,需要生产线程去生产烤鸭,那么生产线程将进行烤
鸭生产并通过消费线程的监控器consumer_con的signal()方法去通知消费线程对烤鸭进行消费。consume()方法也是同样的道理,这里就不
过多分析了。我们可以发现这种方法比我们之前的synchronized同步方法或者是单监视器的锁对象都来得高效和方便些,之前都是使用
notifyAll()和signalAll()方法去唤醒池中的线程,然后让池中的线程又进入 竞争队列去抢占CPU资源,这样不仅唤醒了无关的线程而且又让全
部线程进入了竞争队列中,而我们最后使用两种监听器分别监听生产者线程和消费者线程,这样的方式恰好解决前面两种方式的问题所在,
我们每次唤醒都只是生产者线程或者是消费者线程而不会让两者同时唤醒,这样不就能更高效得去执行程序了吗?好了,到此多生产者多消
费者模式也分析完毕。
5.线程死锁
现在我们再来讨论一下线程死锁问题,从上面的分析,我们知道锁是个非常有用的工具,运用的场景非常多,因为它使用起来非常简单,而
且易于理解。但同时它也会带来一些不必要的麻烦,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。我们先通过一个例
子来分析,这个例子会引起死锁,使得线程t1和线程t2互相等待对方释放锁。
  1. public class DeadLockDemo {
  2.     private static String A=“A”;
  3.     private static String B=“B”;
  4.     public static void main(String[] args) {
  5.         DeadLockDemo deadLock=new DeadLockDemo();
  6.         while(true){
  7.             deadLock.deadLock();
  8.         }
  9.     }
  10.     private void deadLock(){
  11.         Thread t1=new Thread(new Runnable(){
  12.             @SuppressWarnings(“static-access”)
  13.             @Override
  14.             public void run() {
  15.                 synchronized (A) {
  16.                     try {
  17.                         Thread.currentThread().sleep(2000);
  18.                     } catch (InterruptedException e) {
  19.                         e.printStackTrace();
  20.                     }
  21.                 }
  22.                 synchronized(B){
  23.                     System.out.println(“1”);
  24.                 }
  25.             }
  26.         });
  27.         Thread t2 =new Thread(new Runnable() {
  28.             @Override
  29.             public void run() {
  30.                 synchronized (B) {
  31.                     synchronized (A) {
  32.                         System.out.println(“2”);
  33.                     }
  34.                 }
  35.             }
  36.         });
  37.         //启动线程
  38.         t1.start();
  39.         t2.start();
  40.     }
  41. }
同步嵌套是产生死锁的常见情景,从上面的代码中我们可以看出,当t1线程拿到锁A后,睡眠2秒,此时线程t2刚好拿到了B锁,接着要获取A锁,但是此时A锁正好被t1线程持有,因此只能等待t1线程释放锁A,但遗憾的是在t1线程内又要求获取到B锁,而B锁此时又被t2线程持有,到此结果就是t1线程拿到了锁A同时在等待t2线程释放锁B,而t2线程获取到了锁B也同时在等待t1线程释放锁A,彼此等待也就造成了线程死锁问题。虽然我们现实中一般不会向上面那么写出那样的代码,但是有些更为复杂的场景中,我们可能会遇到这样的问题,比如t1拿了锁之后,因为一些异常情况没有释放锁(死循环),也可能t1拿到一个数据库锁,释放锁的时候抛出了异常,没有释放等等,所以我们应该在写代码的时候多考虑死锁的情况,这样才能有效预防死锁程序的出现。下面我们介绍一下避免死锁的几个常见方法:
1.避免一个线程同时获取多个锁。
2.避免在一个资源内占用多个 资源,尽量保证每个锁只占用一个资源。
3.尝试使用定时锁,使用tryLock(timeout)来代替使用内部锁机制。
4.对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
5.避免同步嵌套的发生
6.Thread.join()
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才能从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时的方法表示,如果线程在给定的超时时间里没有终止,那么将会从该超时方法中返回。下面给出一个例子,创建10个线程,编号0~9,每个线程调用钱一个线程的join()方法,也就是线程0结束了,线程1才能从join()方法中返回,而0需要等待main线程结束。
  1. package com.zejian.test;
  2. /**
  3.  * @author zejian
  4.  * @time 2016年3月13日 下午4:10:03
  5.  * @decrition join案例
  6.  */
  7. public class JoinDemo {
  8.     public static void main(String[] args) {
  9.         Thread previous = Thread.currentThread();
  10.         for(int i=0;i<10;i++){
  11.             //每个线程拥有前一个线程的引用。需要等待前一个线程终止,才能从等待中返回
  12.             Thread thread=new Thread(new Domino(previous),String.valueOf(i));
  13.             thread.start();
  14.             previous=thread;
  15.         }
  16.         System.out.println(Thread.currentThread().getName()+” 线程结束”);
  17.     }
  18. }
  19. class Domino implements Runnable{
  20.     private Thread thread;
  21.     public Domino(Thread thread){
  22.         this.thread=thread;
  23.     }
  24.     @Override
  25.     public void run() {
  26.         try {
  27.             thread.join();
  28.         } catch (InterruptedException e) {
  29.             e.printStackTrace();
  30.         }
  31.         System.out.println(Thread.currentThread().getName()+” 线程结束”);
  32.     }
  33. }

好了,到此本篇结束。

java多线程-概念&创建启动&中断&守护线程&优先级&线程状态(多线程编程之一)

今天开始就来总结一下java多线程的基础知识点,下面是本篇的主要内容(大部分知识点参考java核心技术卷1):

1.什么是线程以及多线程与进程的区别
2.多线程的创建与启动
3.中断线程和守护线程以及线程优先级
4.线程的状态转化关系
1.什么是线程以及多线程与进程的区别
在现代操作在运行一个程序时,会为其创建一个进程。例如启动一个QQ程序,操作系统就会为其创建一个进程。而操作系统中调度的最小单位元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。因此我们可以这样理解:
进程:正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
线程:是进程中的单个顺序控制流,是一条执行路径一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。
2.多线程的创建与启动
创建多线程有两种方法,一种是继承Thread类重写run方法,另一种是实现Runnable接口重写run方法。下面我们分别给出代码示例,继承Thread类重写run方法:
  1. public class ThreadByEx extends Thread{
  2.     /**
  3.      * 重写run方法
  4.      */
  5.     @Override
  6.     public void run() {
  7.         System.out.println(“I’m a thread that extends Thread!”);
  8.     }
  9. }

实现Runnable接口重写run方法:

  1. public class ThreadByRunnable implements Runnable{
  2.     /**
  3.      * 实现run方法
  4.      */
  5.     @Override
  6.     public void run() {
  7.         System.out.println(“I’m a thread that implements Runnable !”);
  8.     }
  9. }

怎么启动线程?

  1. public class MainTest {
  2.     public static void main(String[] args) {
  3.         //继承Thread启动的方法
  4.         ThreadByEx t1=new ThreadByEx();
  5.         t1.start();//启动线程
  6.         //实现Runnable启动线程的方法
  7.         ThreadByRunnable r = new ThreadByRunnable();
  8.         Thread t2 =new Thread(r);
  9.         t2.start();//启动线程
  10.     }
  11. }

运行结果:

  1. I’m a thread that extends Thread!
  2. I’m a thread that implements Runnable !
代码相当简单,不过多解释。这里有点需要注意的是调用start()方法后并不是是立即的执行多线程的代码,而是使该线程变为可运行态,什么时候运行多线程代码是由操作系统决定的。
3.中断线程和守护线程以及线程优先级
什么是中断线程?
我们先来看看中断线程是什么?(该解释来自java核心技术一书,我对其进行稍微简化),当线程的run()方法执行方法体中的最后一条语句后,并经由执行return语句返回时,或者出现在方法中没有捕获的异常时线程将终止。在java早期版本中有一个stop方法,其他线程可以调用它终止线程,但是这个方法现在已经被弃用了,因为这个方法会造成一些线程不安全的问题。我们可以把中断理解为一个标识位的属性,它表示一个运行中的线程是否被其他线程进行了中断操作,而中断就好比其他线程对该线程打可个招呼,其他线程通过调用该线程的interrupt方法对其进行中断操作,当一个线程调用interrupt方法时,线程的中断状态(标识位)将被置位(改变),这是每个线程都具有的boolean标志,每个线程都应该不时的检查这个标志,来判断线程是否被中断。而要判断线程是否被中断,我们可以使用如下代码
  1. Thread.currentThread().isInterrupted()
  1. while(!Thread.currentThread().isInterrupted()){
  2.     do something
  3. }

但是如果此时线程处于阻塞状态(sleep或者wait),就无法检查中断状态,此时会抛出InterruptedException异常。如果每次迭代之后都调用sleep方法(或者其他可中断的方法),isInterrupted检测就没必要也没用处了,如果在中断状态被置位时调用sleep方法,它不会休眠反而会清除这一休眠状态并抛出InterruptedException。所以如果在循环中调用sleep,不要去检测中断状态,只需捕获InterruptedException。代码范例如下:

  1. public void run(){
  2.         while(more work to do ){
  3.             try {
  4.                 Thread.sleep(5000);
  5.             } catch (InterruptedException e) {
  6.                 //thread was interrupted during sleep
  7.                 e.printStackTrace();
  8.             }finally{
  9.                 //clean up , if required
  10.             }
  11.         }
同时还有点要注意的就是我们在捉中断异常时尽量按如下形式处理,不要留空白什么都不处理!
不妥的处理方式:
  1. void myTask(){
  2.     …
  3.    try{
  4.        sleep(50)
  5.       }catch(InterruptedException e){
  6.    …
  7.    }
  8. }
  1. void myTask()throw InterruptedException{
  2.     sleep(50)
  3. }

或者

  1. void myTask(){
  2.     …
  3.     try{
  4.     sleep(50)
  5.     }catch(InterruptedException e){
  6.      Thread.currentThread().interrupt();
  7.     }
  8. }
最后关于中断线程,我们这里给出中断线程的一些主要方法:
void interrupt():向线程发送中断请求,线程的中断状态将会被设置为true,如果当前线程被一个sleep调用阻塞,那么将会抛出interrupedException异常。
static boolean interrupted():测试当前线程(当前正在执行命令的这个线程)是否被中断。注意这是个静态方法,调用这个方法会产生一个副作用那就是它会将当前线程的中断状态重置为false。
boolean isInterrupted():判断线程是否被中断,这个方法的调用不会产生副作用即不改变线程的当前中断状态。
static Thread currentThread() : 返回代表当前执行线程的Thread对象。
什么是守护线程?
首先我们可以通过t.setDaemon(true)的方法将线程转化为守护线程。而守护线程的唯一作用就是为其他线程提供服务。计时线程就是一个典型的例子,它定时地发送“计时器滴答”信号告诉其他线程去执行某项任务。当只剩下守护线程时,虚拟机就退出了,因为如果只剩下守护线程,程序就没有必要执行了。另外JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。最后还有一点需要特别注意的是在java虚拟机退出时Daemon线程中的finally代码块并不一定会执行哦,代码示例:
  1. public class Demon {
  2.     public static void main(String[] args) {
  3.         Thread deamon = new Thread(new DaemonRunner(),“DaemonRunner”);
  4.         //设置为守护线程
  5.         deamon.setDaemon(true);
  6.         deamon.start();//启动线程
  7.     }
  8.     static class DaemonRunner implements Runnable{
  9.         @Override
  10.         public void run() {
  11.             try {
  12.                 Thread.sleep(500);
  13.             } catch (InterruptedException e) {
  14.                 e.printStackTrace();
  15.             }finally{
  16.                 System.out.println(“这里的代码在java虚拟机退出时并不一定会执行哦!”);
  17.             }
  18.         }
  19.     }
  20. }
因此在构建Daemon线程时,不能依靠finally代码块中的内容来确保执行关闭或清理资源的逻辑。
什么是线程优先级
在现代操作系统中基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下一次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。在java线程中,通过一个整型的成员变量Priority来控制线程优先级,每一个线程有一个优先级,默认情况下,一个线程继承它父类的优先级。可以用setPriority方法提高或降低任何一个线程优先级。可以将优先级设置在MIN_PRIORITY(在Thread类定义为1)与MAX_PRIORITY(在Thread类定义为10)之间的任何值。线程的默认优先级为NORM_PRIORITY(在Thread类定义为5)。尽量不要依赖优先级,如果确实要用,应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态,低优先级线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程可能永远不会被执行到。因此我们在设置优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高的优先级,而偏重计算(需要较多CPU时间或者运算)的线程则设置较低的优先级,这样才能确保处理器不会被长久独占。当然还有要注意就是在不同的JVM以及操作系统上线程的规划存在差异,有些操作系统甚至会忽略对线程优先级的设定,如mac os系统或者Ubuntu系统……..
4.线程的状态转化关系
(1). 新建状态(New):新创建了一个线程对象。
(2). 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
(3). 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
(4). 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

– 等待阻塞(WAITING):运行的线程执行wait()方法,JVM会把该线程放入等待池中。

– 同步阻塞(Blocked):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

– 超时阻塞(TIME_WAITING):运行的线程执行sleep(long)或join(long)方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。

(5). 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

图中的方法解析如下:

Thread.sleep():在指定时间内让当前正在执行的线程暂停执行,但不会释放”锁标志”。不推荐使用。
Thread.sleep(long):使当前线程进入阻塞状态,在指定时间内不会执行。
Object.wait()和Object.wait(long):在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的”锁标志”,从而使别的线程有机会抢占该锁。 当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。 唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常,waite()和notify()必须在synchronized函数或synchronized中进行调用。如果在non-synchronized函数或non-synchronized中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
Object.notifyAll():则从对象等待池中唤醒所有等待等待线程
Object.notify():则从对象等待池中唤醒其中一个线程。
Thread.yield()方法 暂停当前正在执行的线程对象,yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,yield()只能使同优先级或更高优先级的线程有执行的机会。
Thread.Join():把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
好了。本篇线程基础知识介绍到此结束。

关于Java Lambda的教程,简单明了

许多热门的编程语言如今都有一个叫做lambda或者闭包的语言特性,包括比较经典的函数式编程语言Lisp,Scheme,也有稍微年轻的语言比如JavaScript,Python,Ruby,Groovy,Scale,C#,甚至C++也有Lambda表达式。一些语言是运行在java虚拟机上,作为虚拟机最具代表的语言java当然也不想落后。

究竟什么是Lambda表达式?

Lambda表达式的概念来自于Lambda演算,下面是一个java lambda的简单例子,

(int x)->{ return x+1;}

简单来看lambda像一个没有名字的方法,它具有一个方法应该有的部分:参数列表int x,方法body return x+1,和方法相比lambda好像缺少了一个返回值类型、异常抛出和名字。返回值类型和异常是通过编译器在方法体中推导出来,在上面这个例子中返回值类型是int,没有抛出异常。真正缺少的就是一个名字,从这个角度来看,lambda表达式是一种匿名方法。

Lambda表达式和匿名内部类

从上面的分析可以看出lambda和java内部类的特性有点相似,匿名内部类不只是一个方法,而是一个包含一个或多个方法的类,他们的作用都是一样的,都是作为方法的参数传递,我从JDK源码中提取出来listFiles(FileFilter) 方法:

关于Java Lambda的教程,简单明了

public interface FileFilter { boolean accept(File pathname); }

fileFilter接收一个File对象返回一个boolean值,listFiles方法把Filter应用到所有的File对象接收 那些accept返回true的文件。对于listFiles方法来讲我们必须传递一个函数式接口给他,这是FileFileter的一个实现,一般我们通过匿名类来完成:

关于Java Lambda的教程,简单明了

我们现在可以用lambda来实现:

关于Java Lambda的教程,简单明了

这两种情况我们都是传递了一个函数式接口给方法就像传递对象一样,我们使用代码就像使用数据一样,使用匿名类我们实际上传递了一个对象给方法,使用lambda不再需要创建对象,我们只需要把lambda代码传递给方法。

除了传递lambda之外我们还可以传递一个方法引用,比如:

File[] files = myDir.listFiles( File::isFile );

Lambda表达式的表示

在之前的例子,我们使用lambda表达式定义了一个函数,我们可以把它作为参数传递给一个方法,方法把它当成一个对象来使用,lambda表达式有函数和对象的一些属性,看你从什么角度来看:

  • 从概念来讲,lambda表达式是一个匿名函数,它有签名和方法体但是没有名字
  • 当lambda表达式作为参数传递给方法时,接收方法把它当对象使用,在listFiles方法内部,lambda表达式是一个对象的引用,在这里lambda表达式是一种常规的对象,比如有地址和类型。

从实际的角度来分析,lambda对象是由编译期和运行时系统来创建的,这就允许编译期进行优化而使用者不需要关心具体细节,编译器从lambda表达式的上下文环境来获取lambda对象的语义类型,但是编译期并不创建那个对象而是直到运行时由虚拟机动态创建,这里说的动态创建是指调用

invokedynamic字节码指令来创建。使用动态创建可以推迟对象的创建到对象第一次被使用时,如果你只是定义了lambda表达式而从未使用,它的类型和对象都不会创建。

函数式接口

整个魔幻之处就在于类型的推导,这个类型称为目标类型,运行时系统动态创建的类型是目标类型的子类型。之前的那个例子我们看到目标类型是FileFilter,在例子中我们定义了一个lambda表达式把它传递给listFiles方法,然后listFiles方法把它作为FileFilter子类的一个对象来使用。这里看起来好像有点神奇,我们并没有声明lambda表达式实现了FileFilter接口,listFiles方法也没有表明它很愉快的接收了lambda表达式,它只是需要一个FileFilter的子类的对象,这是如何工作的?

这里面的魔术在于编译期执行了类型推导,编译器根据lambda表达式的上下文来决定需要什么类型的对象,然后编译器观察lambda表达式是否兼容需要的类型。如果Java是一种函数式编程语言的话lambda表达式最自然的类型就是某种函数式类型,用来描述函数的一种特殊类型。函数式类型仅仅描述了函数的签名比如(int,int)->boolean.但是Java不是函数式编程语言因此没有函数式类型,语言的设计者可以选择添加一种新的类型,由于他们不想给Java的类型系统引入太多的改变,因此他们尝试寻找一种办法来集成lambda表达式到语言中而不需要添加函数式类型。

结果他们使用函数式接口来代替,函数式接口是只有一个方法的接口,这样的接口在JDK里有很多,比如经典的Runnable接口,它只有一个方法void run(),还有很多其他的,比如Readable,Callable,Iterable,closeable,Flushnable,Formattable,Comparable,Comparator,或者我们前面提到的FileFilter接口。函数是接口和lambda表达式奕扬都只有一个方法,语言的设计者决定让编译器把lambda表达式转换成匹配的函数式接口。这种转换通常是自动的。比如我们前面提到的(File f) -> { return f.isFile(); },编译器知道listFiles方法的签名,因此我们需要的类型就是FileFilter,FileFilter是这样的:
public interface FileFilter { boolean accept(File pathname); }
FileFilter仅仅需要一个方法因此它是函数式接口类型,我们定义的lambda表达式有一个相匹配的签名,接收一个File对象,返回一个boolean值,不抛出检查的异常,因此编译器把lambda表达式转换成函数式接口FileFilter类型。

假如我们有下面两个函数式接口:
关于Java Lambda的教程,简单明了

我们的lambda表达式兼容两种函数式接口类型:
关于Java Lambda的教程,简单明了

当我们试图给两个变量相互赋值时编译器会报错,虽然两个变量都是同一个lambda表达式,原因很简单两个变量是不同的类型。也有可能出现编译器无法判断匹配的函数式接口类型,比如这个例子:
Object ref = (File f) -> { return f.isFile(); };
这个赋值语句的上下文没有提供足够的信息来转换,因此编译器会报错,解决这个问题最简单的方法就是添加一个类型转换:
Object ref = (FileFilter) (File f) -> { return f.isFile(); };

Lambda表达式和匿名内部类的区别

Lambda表达式出现在我们通常需要匿名内部类的地方,在很多场合他们是可以互换的。但是他们还是有几个区别:

1.语法

匿名类一般这样编写:
关于Java Lambda的教程,简单明了

而Lambda表达式有多种形式:
关于Java Lambda的教程,简单明了

2.运行时成本

匿名类相对Lambda表达式来讲多了一些成本,使用匿名类或造成新类型的创建、新类型对象的创建。运行时匿名内需要:类加载 > 内存分配、对象初始化 > 调用非静态方法。

Lambda表达式需要函数式接口的转换和最终的调用,类型推导发生在编译期,不需要运行时消耗,之前提到过,lambda对象的创建是通过字节码指令invokedynamic来完成的,减少了类型和实例的创建消耗。

3.变量绑定

匿名类可以访问外部域的final变量,如下所示关于Java Lambda的教程,简单明了

对于lambda表达式,cnt变量不需要显式声明为final的,一旦变量在lambda中使用编译期会自动把它当成是final的变量,换句话说在lambda中使用的外部域变量是隐式final的,

关于Java Lambda的教程,简单明了

从java8开始匿名内部类也不需要再显式声明final类,编译器会自动把它当成是final。

4.作用域

匿名内部类是一个类,也就是说它自己引入了一个作用域,你可以在里面定义变量,而lambda表达式没有自己的作用域。
关于Java Lambda的教程,简单明了

lambda表达式:
关于Java Lambda的教程,简单明了

不同的作用域规则对于this和super关键字有不同的效果,在匿名类中this表示匿名类对象本身的引用,super表示匿名类的父类。在lambda表达式this和super关键字意思和外部域中this和super的意思一样,this一般是包含它的那个对象,super表示包含它的类的父类。

java8的新特性以及用法简介,再不学习,代码都看不懂了

1. 介绍

本文简单介绍下java8里面有哪些新的特性,以及相关的用法。

JAVA8是意义深远的一个新版本。随着大数据的兴起,函数式编程在处理大数据上的优势开始体现。JAVA8也紧跟时代,引入了函数式语言的特性,值得关注。下面看看其有哪些新的内容。

2 接口的默认方法

Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法。JAVA可以实现多个接口,从而来对原本的类进行方法扩展。JAVA只能继承1个类,所以以前的接口没法提供这种灵活的扩展方法。现在要简单的扩展方法,也不需要使用Spring的代码织入了,直接用扩展方法即可。不过横切逻辑织入还是用Spring好。

下面看个例子:

interface Formula {  
    double calculate(int a);  
    
    //default修饰的扩展方法 
    default double sqrt(int a) {  
        return Math.sqrt(a);  
    } 
}

Formula接口在拥有calculate方法之外同时还定义了sqrt方法,实现了Formula接口的子类只需要实现一个calculate方法,默认方法sqrt将在子类上可以直接使用。

Formula formula = new Formula() {  
    @Override  
    public double calculate(int a) { 
        return sqrt(a * 100);  
    } 
}; 
formula.calculate(100); // 100.0 
formula.sqrt(16); // 4.0

新的Java 8 的这个特新在编译器实现的角度上来说更加接近Scala的trait。 在C#中也有名为扩展方法的概念,允许给已存在的类型扩展方法,和Java 8的这个在语义上有差别。

2 lambda表达式

首先看看在老版本的Java中是如何排列字符串的:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia"); 
Collections.sort(names, new Comparator<String>() {  
    @Override  
    public int compare(String a, String b) {  
         return b.compareTo(a);  
    } 
});

只需要给静态方法 Collections.sort 传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。在Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8提供了更简洁的语法,lambda表达式:

Collections.sort(names, 
     (String a, String b) -> {  return b.compareTo(a); });

看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字,但是你还可以写得更短点,Java编译器可以自动推导出参数类型,所以你可以不用再写一次类型。

Collections.sort(names, (a, b) -> b.compareTo(a));

2.1 函数式接口

每个lambda表达式对应一个函数式接口。函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。因为 默认方法 不算抽象方法,所以你也可以给你的函数式接口添加默认方法。

我们可以将lambda表达式当作任意只包含一个抽象方法的接口类型,确保你的接口一定达到这个要求,你只需要给你的接口添加 @FunctionalInterface 注解,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的。

//需要注解修饰表明是函数式接口
@FunctionalInterface
interface Converter<F, T> {  
    //必须仅包含一个抽象方法,用于lambda表达式给出具体实现。这里可以包    含扩展方法 
    T convert(F from); 
} 
// Integer.valueOf(from)是函数式接口里面抽象方法的实现。Converter<String, Integer> converter = 
      (from) -> Integer.valueOf(from); 
Integer converted = converter.convert("123"); System.out.println(converted); // 123

需要注意如果@FunctionalInterface如果没有指定,上面的代码也是对的。

译者注 将lambda表达式映射到一个单方法的接口上,这种做法在Java 8之前就有别的语言实现,比如Rhino JavaScript解释器,如果一个函数参数接收一个单方法的接口而你传递的是一个function,Rhino 解释器会自动做一个单接口的实例到function的适配器

2.2 方法与构造函数引用

Java 8 允许你使用 :: 关键字来传递方法或者构造函数引用

Converter<String, Integer> converter = Integer::valueOf; 
Integer converted = converter.convert("123"); System.out.println(converted); // 123

获取构造函数的引用的话使用 ::new,后面接new关键字即可。

2.3 访问局部变量

在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。

//不用final修饰也可以访问,但是要确保其不备修改,所以还是建议用final修饰final
int num = 1; 
Converter<Integer, String> stringConverter = 
     (from) -> String.valueOf(from + num); 
stringConverter.convert(2); // 3

2.4 访问对象字段与静态变量

和本地变量不同的是,lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的:

class Lambda4 {  
    static int outerStaticNum;  
    int outerNum;  
    void testScopes() {  
        Converter<Integer, String> stringConverter1 = 
           (from) -> { outerNum = 23;  
                return String.valueOf(from);  
           };  
        Converter<Integer, String> stringConverter2 = 
           (from) -> {  
                outerStaticNum = 72; 
                return String.valueOf(from); 
           };  
     } 
}

3. 内建函数式接口

JDK 1.8 API包含了很多内建的函数式接口,在老Java中常用到的比如Comparator或者Runnable接口,这些接口都增加了@FunctionalInterface注解以便能用在lambda上。

Java 8 API同样还提供了很多全新的函数式接口来让工作更加方便,有一些接口是来自Google Guava库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。

这些已经默认提供的内建函数式接口主要是:

  • Predicate — 传入一个参数,返回一个bool结果, 方法为boolean test(T t)
  • Consumer — 传入一个参数,无返回值,纯消费。 方法为void accept(T t)
  • Function<t,r> — 传入一个参数,返回一个结果,方法为R apply(T t)</t,r>
  • Supplier — 无参数传入,返回一个结果,方法为T get()

3.1 Predicate接口

Predicate 接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非):

//用于做一些判断Predicate<String>

predicate = (s) -> s.length() > 0; 
predicate.test("foo"); // true
predicate.negate().test("foo"); // false 
Predicate<Boolean> nonNull = Objects::nonNull; 
Predicate<Boolean> isNull = Objects::isNull; 
Predicate<String> isEmpty = String::isEmpty; 
Predicate<String> isNotEmpty = isEmpty.negate();

3.2 Function 接口

Function 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose, andThen):

Function<String, Integer> toInteger = Integer::valueOf; Function<String, String> backToString = toInteger.andThen(String::valueOf); 
backToString.apply("123"); // "123"

3.3 Supplier 接口

Supplier 接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数

Supplier<Person> personSupplier = Person::new; 
personSupplier.get(); // new Person

3.4 Consumer 接口

Consumer 接口表示执行在单个参数上的操作。

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName); 
greeter.accept(new Person("Luke", "Skywalker"));

3.5 Comparator 接口

Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法:

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); 
Person p1 = new Person("John", "Doe"); 
Person p2 = new Person("Alice", "Wonderland"); 
comparator.compare(p1, p2); // > 0 
comparator.reversed().compare(p1, p2); // < 0

3.6 Optional 接口

Optional 不是函数是接口,这是个用来防止NullPointerException异常的辅助类型,这是下一届中将要用到的重要概念,现在先简单的看看这个接口能干什么:

Optional 被定义为一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null,而在Java 8中,不推荐你返回null而是返回Optional。

Optional<String> optional = Optional.of("bam"); 
optional.isPresent(); // true 
optional.get(); // "bam" optional.orElse("fallback"); // "bam" 
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"

3.7 Stream 接口

java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set, Map不支持。Stream的操作可以串行执行或者并行执行。

首先看看Stream是怎么用,首先创建实例代码的用到的数据List:

List<String> stringCollection = new ArrayList<>(); 
stringCollection.add("ddd2"); 
stringCollection.add("aaa2"); 
stringCollection.add("bbb1"); 
stringCollection.add("aaa1"); 
stringCollection.add("bbb3"); 
stringCollection.add("ccc"); 
stringCollection.add("bbb2"); 
stringCollection.add("ddd1");

Java 8扩展了集合类,可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建一个Stream。下面几节将详细解释常用的Stream操作:

3.7.1 Filter过滤

过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。

stringCollection
   .stream()  
   .filter((s) -> s.startsWith("a"))
   .forEach(System.out::println); // "aaa2", "aaa1"

3.7.2 Sort 排序

排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。

stringCollection
   .stream()
   .sorted()
   .filter((s) -> s.startsWith("a"))  
   .forEach(System.out::println); // "aaa1", "aaa2"

需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的:

System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

3.7.3 Map 映射

中间操作map会将元素根据指定的Function接口来依次将元素转成另外的对象,下面的示例展示了将字符串转换为大写字符串。你也可以通过map来讲对象转换成其他类型,map返回的Stream类型是根据你map传递进去的函数的返回值决定的。

stringCollection
   .stream()  
   .map(String::toUpperCase)  
   .sorted((a, b) -> b.compareTo(a))  
   .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

3.7.4 Match 匹配

Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。

boolean anyStartsWithA = stringCollection  
    .stream() 
    .anyMatch((s) -> s.startsWith("a"));             System.out.println(anyStartsWithA); // true boolean 
allStartsWithA = stringCollection  
    .stream()  
    .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean
noneStartsWithZ =  stringCollection  
    .stream()  
    .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true

3.7.5 Count 计数

计数是一个最终操作,返回Stream中元素的个数,返回值类型是long。

long startsWithB = stringCollection  
    .stream()  
    .filter((s) -> s.startsWith("b"))  
    .count(); 
System.out.println(startsWithB); // 3

3.7.6 Reduce 规约

这是一个最终操作,允许通过指定的函数来讲stream中的多个元素规约为一个元素,规越后的结果是通过Optional接口表示的:

Optional<String> reduced = stringCollection
    .stream()  
    .sorted()  
    .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); //"aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

3.7.7 并行Streams

前面提到过Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。

下面的例子展示了是如何通过并行Stream来提升性能:

首先我们创建一个没有重复元素的大表:

int max = 1000000; 
List<String> values = new ArrayList<>(max); 
for (int i = 0; i < max; i++) {  
   UUID uuid = UUID.randomUUID();  
   values.add(uuid.toString()); 
}

然后我们计算一下排序这个Stream要耗时多久,

串行排序:

long t0 = System.nanoTime(); 
long count = values.stream().sorted().count(); 
System.out.println(count); 
long t1 = System.nanoTime(); 
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); 
System.out.println(String.format("sequential sort took: %d ms",  millis));

// 串行耗时: 899 ms

并行排序:

long t0 = System.nanoTime(); 
long count = values.parallelStream().sorted().count(); System.out.println(count); 
long t1 = System.nanoTime(); 
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); 
System.out.println(String.format("parallel sort took: %d ms", millis));

// 并行排序耗时: 472 ms

上面两个代码几乎是一样的,但是并行版的快了50%之多,唯一需要做的改动就是将stream()改为parallelStream()。

4. Map

前面提到过,Map类型不支持stream,不过Map提供了一些新的有用的方法来处理一些日常任务。

Map<Integer, String> map = new HashMap<>(); 
for (int i = 0; i < 10; i++) {  
   map.putIfAbsent(i, "val" + i); 
}
map.forEach((id, val) -> System.out.println(val));

以上代码很容易理解, putIfAbsent 不需要我们做额外的存在性检查,而forEach则接收一个Consumer接口来对map里的每一个键值对进行操作。

下面的例子展示了map上的其他有用的函数:

map.computeIfPresent(3,(num, val) -> val + num); 
map.get(3); // val33 
map.computeIfPresent(9, (num, val) -> null); 
map.containsKey(9); // false 
map.computeIfAbsent(23, num -> "val" + num); 
map.containsKey(23); // true 
map.computeIfAbsent(3, num -> "bam"); 
map.get(3); // val33

接下来展示如何在Map里删除一个键值全都匹配的项:

map.remove(3, "val3"); 
map.get(3); // val33 
map.remove(3, "val33"); 
map.get(3); // null

另外一个有用的方法:

map.getOrDefault(42, "not found"); // not found

对Map的元素做合并也变得很容易了:

map.merge(9,"val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 
map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); 
map.get(9); // val9concat

Merge做的事情是如果键名不存在则插入,否则则对原键对应的值做合并操作并重新插入到map中。

5. Date API

Java 8 在包java.time下包含了一组全新的时间日期API。新的日期API和开源的Joda-Time库差不多,但又不完全一样,下面的例子展示了这组新API里最重要的一些部分:

5.1 Clock 时钟

Clock类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用Instant类来表示,Instant类也可以用来创建老的java.util.Date对象。

Clock clock = Clock.systemDefaultZone(); 
long millis = clock.millis(); 
Instant instant = clock.instant(); 
Date legacyDate = Date.from(instant); // legacy java.util.Date

5.2 Timezones 时区

在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到。 时区定义了到UTS时间的时间差,在Instant时间点对象到本地日期对象之间转换的时候是极其重要的。

System.out.println(ZoneId.getAvailableZoneIds());
 // prints all available timezone ids ZoneId zone1 = 
ZoneId.of("Europe/Berlin"); 
ZoneId zone2 = ZoneId.of("Brazil/East"); 
System.out.println(zone1.getRules()); 
System.out.println(zone2.getRules()); // 
ZoneRules[currentStandardOffset=+01:00] // 
ZoneRules[currentStandardOffset=-03:00]

5.3 LocalTime 本地时间

LocalTime 定义了一个没有时区信息的时间,例如 晚上10点,或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差:

LocalTime now1 = LocalTime.now(zone1); 
LocalTime now2 = LocalTime.now(zone2); 
System.out.println(now1.isBefore(now2)); // false 
long hoursBetween = ChronoUnit.HOURS.between(now1, now2); 
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2); 
System.out.println(hoursBetween); // -3 
System.out.println(minutesBetween); // -239

LocalTime 提供了多种工厂方法来简化对象的创建,包括解析时间字符串。

LocalTime late = LocalTime.of(23, 59, 59); 
System.out.println(late); // 23:59:59 
DateTimeFormatter germanFormatter =  
    DateTimeFormatter
    .ofLocalizedTime(FormatStyle.SHORT)  
    .withLocale(Locale.GERMAN); 
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter); 
System.out.println(leetTime); // 13:37

5.4 LocalDate 本地日期

LocalDate 表示了一个确切的日期,比如 2014-03-11。该对象值是不可变的,用起来和LocalTime基本一致。下面的例子展示了如何给Date对象加减天/月/年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例。

LocalDate today = LocalDate.now(); 
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); 
LocalDate yesterday = tomorrow.minusDays(2); 
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4); DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();

System.out.println(dayOfWeek); // FRIDAY

从字符串解析一个LocalDate类型和解析LocalTime一样简单:

DateTimeFormatter germanFormatter = DateTimeFormatter  
   .ofLocalizedDate(FormatStyle.MEDIUM)  
   .withLocale(Locale.GERMAN); 
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter); 
System.out.println(xmas); // 2014-12-24

5.5 LocalDateTime 本地日期时间

LocalDateTime 同时表示了时间和日期,相当于前两节内容合并到一个对象上了。LocalDateTime和LocalTime还有LocalDate一样,都是不可变的。LocalDateTime提供了一些能访问具体字段的方法。

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59); 
DayOfWeek dayOfWeek = sylvester.getDayOfWeek(); 
System.out.println(dayOfWeek); // WEDNESDAY Month month = 
sylvester.getMonth(); 
System.out.println(month); // DECEMBER long 
minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY); 
System.out.println(minuteOfDay); // 1439

只要附加上时区信息,就可以将其转换为一个时间点Instant对象,Instant时间点对象可以很容易的转换为老式的java.util.Date。

Instant instant = sylvester  
   .atZone(ZoneId.systemDefault())  
   .toInstant(); 
Date legacyDate = Date.from(instant); 
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014

格式化LocalDateTime和格式化时间和日期一样的,除了使用预定义好的格式外,我们也可以自己定义格式:

DateTimeFormatter formatter = DateTimeFormatter  
   .ofPattern("MMM dd, yyyy - HH:mm"); 
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", 
formatter); 
String string = formatter.format(parsed); 
System.out.println(string); // Nov 03, 2014 - 07:13

和java.text.NumberFormat不一样的是新版的DateTimeFormatter是不可变的,所以它是线程安全的。

6. Annotation 注解

6.1 多重注解

在Java 8中支持多重注解了,先看个例子来理解一下是什么意思。

首先定义一个包装类Hints注解用来放置一组具体的Hint注解:

@interface Hints {  Hint[] value(); } 
@Repeatable(Hints.class) 
@interface Hint {  String value(); }

Java 8允许我们把同一个类型的注解使用多次,只需要给该注解标注一下@Repeatable即可。

例子:

  1. 使用包装类当容器来存多个注解(老方法) java @Hints({@Hint("hint1"), @Hint("hint2")}) class Person {}
  1. 使用多重注解(新方法) java @Hint("hint1") @Hint("hint2") class Person {} 第二个例子里java编译器会隐性的帮你定义好@Hints注解,了解这一点有助于你用反射来获取这些信息:
    java
    Hint hint = Person.class.getAnnotation(Hint.class);
    System.out.println(hint); // null
    Hints hints1 =
    Person.class.getAnnotation(Hints.class);
    System.out.println(hints1.value().length); // 2
    Hint[] hints2 =
    Person.class.getAnnotationsByType(Hint.class);
    System.out.println(hints2.length); // 2即便我们没有在Person类上定义@Hints注解,我们还是可以通过 getAnnotation(Hints.class) 来获取 @Hints注解,更加方便的方法是使用 getAnnotationsByType 可以直接获取到所有的@Hint注解。

6.2 新的target

另外Java 8的注解还增加到两种新的target上了:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @interface MyAnnotation {}

7. more

JDK 1.8里还有很多很有用的东西,比如Arrays.parallelSort, StampedLock和CompletableFuture等等。

https://www.kancloud.cn/wizardforcel/modern-java-zh/141721

 

Java NIO 基础知识

前言

前言部分是科普,读者可自行选择是否阅读这部分内容。

为什么我们需要关心 NIO?我想很多业务猿都会有这个疑问。

我在工作的前两年对这个问题也很不解,因为那个时候我认为自己已经非常熟悉 IO 操作了,读写文件什么的都非常溜了,IO 包无非就是 File、RandomAccessFile、字节流、字符流这些,感觉没什么好纠结的。最混乱的当属 InputStream/OutputStream 一大堆的类不知道谁是谁,不过了解了装饰者模式以后,也都轻松破解了。

在 Java 领域,一般性的文件操作确实只需要和 java.io 包打交道就可以了,尤其对于写业务代码的程序员来说。不过,当你写了两三年代码后,你的业务代码可能已经写得很溜了,蒙着眼睛也能写增删改查了。这个时候,也许你会想要开始了解更多的底层内容,包括并发、JVM、分布式系统、各个开源框架源码实现等,处于这个阶段的程序员会开始认识到 NIO 的用处,因为系统间通讯无处不在。

可能很多人不知道 Netty 或 Mina 有什么用?和 Tomcat 有什么区别?为什么我用 HTTP 请求就可以解决应用间调用的问题却要使用 Netty?

当然,这些问题的答案很简单,就是为了提升性能。那意思是 Tomcat 性能不好?当然不是,它们的使用场景就不一样。当初我也不知道 Nginx 摆在 Tomcat 前面有什么用,也是经过实践慢慢领悟到了那么些意思。

Nginx 是 web 服务器,Tomcat/Jetty 是应用服务器,Netty 是通讯工具。

也许你现在还不知道 NIO 有什么用,但是一定不要放弃学习它。

缓冲区操作

缓冲区是 NIO 操作的核心,本质上 NIO 操作就是缓冲区操作。

写操作是将缓冲区的数据排干,如将数据从缓冲区持久化到磁盘中。

读操作是将数据填充到缓冲区中,以便应用程序后续使用数据。

当然,我们这里说的缓冲区是指用户空间的缓冲区。

Java NIO 基础知识

.

简单分析下上图。应用程序发出读操作后,内核向磁盘控制器发送命令,要求磁盘返回相应数据,磁盘控制器通过 DMA 直接将数据发送到内核缓冲区。一旦内核缓冲区满了,内核即把数据拷贝到请求数据的进程指定的缓冲区中。

DMA: Direct Memory Access

Wikipedia:直接内存访问是计算机科学中的一种内存访问技术。它允许某些电脑内部的硬件子系统(电脑外设),可以独立地直接读写系统内存,而不需中央处理器(CPU)介入处理 。在同等程度的处理器负担下,DMA 是一种快速的数据传送方式。很多硬件的系统会使用 DMA,包含硬盘控制器、绘图显卡、网卡和声卡。

也就是说,磁盘控制器可以在不用 CPU 的帮助下就将数据从磁盘写到内存中,毕竟让 CPU 等待 IO 操作完成是一种浪费

很容易看出来,数据先到内核,然后再从内核复制到用户空间缓冲区的做法并不高效,下面简单说说为什么需要这么设计。

  • 首先,用户空间运行的代码是不可以直接访问硬件的,需要由内核空间来负责和硬件通讯,内核空间由操作系统控制。
  • 其次,磁盘存储的是固定大小的数据块,磁盘按照扇区来组织数据,而用户进程请求的一般都是任意大小的数据块,所以需要由内核来负责协调,内核会负责组装、拆解数据。

内核空间会对数据进行缓存和预读取,所以,如果用户进程需要的数据刚好在内核空间中,直接拷贝过来就可以了。如果内核空间没有用户进程需要的数据的话,需要挂起用户进程,等待数据准备好。

虚拟内存

这个概念大家都懂,这里就继续啰嗦一下了,虚拟内存是计算机系统内存管理的一种技术。前面说的缓存区操作看似简单,但是具体到底层细节,还是蛮复杂的。

下面的描述,我尽量保证准确,但是不会展开得太具体,因为虚拟内存还是蛮复杂的,要完全介绍清楚,恐怕需要很大的篇幅,如果读者对这方面的内容感兴趣的话,建议读者寻找更加专业全面的介绍资料,如《深入理解计算机系统》。

物理内存被组织成一个很大的数组,每个单元是一个字节大小,然后每个字节都有一个唯一的物理地址,这应该很好理解。

虚拟内存是对物理内存的抽象,它使得应用程序认为它自己拥有连续可用的内存(一个连续完整的地址空间),而实际上,应用程序得到的全部内存其实是一个假象,它通常会被分隔成多个物理内存碎片(后面说的页),还有部分暂时存储在外部磁盘存储器上,在需要时进行换入换出。

举个例子,在 32 位系统中,每个应用程序能访问到的内存是 4G(32 位系统的最大寻址空间 2^32),这里的 4G 就是虚拟内存,每个程序都以为自己拥有连续的 4G 空间的内存,即使我们的计算机只有 2G 的物理内存。也就是说,对于机器上同时运行的多个应用程序,每个程序都以为自己能得到连续的 4G 的内存。这中间就是使用了虚拟内存。

我们从概念上看,虚拟内存也被组织成一个很大的数组,每个单元也是一个字节大小,每个字节都有唯一的虚拟地址。它被存储于磁盘上,物理内存是它的缓存。

物理内存作为虚拟内存的缓存,当然不是以字节为单位进行组织的,那样效率太低了,它们之间是以页(page)进行缓存的。虚拟内存被分割为一个个虚拟页,物理内存也被分割为一个个物理页,这两个页的大小应该是一致的,通常是 4KB – 2MB。

举个例子,看下图:

Java NIO 基础知识

.

进程 1 现在有 8 个虚拟页,其中有 2 个虚拟页缓存在主存中,6 个还在磁盘上,需要的时候再读入主存中;进程 2 有 7 个虚拟页,其中 4 个缓存在主存中,3 个还在磁盘上。

在 CPU 读取内存数据的时候,给出的是虚拟地址,将一个虚拟地址转换为物理地址的任务我们称之为地址翻译。在主存中的查询表存放了虚拟地址到物理地址的映射关系,表的内容由操作系统维护。CPU 需要访问内存时,CPU 上有一个叫做内存管理单元的硬件会先去查询真实的物理地址,然后再到指定的物理地址读取数据。

上面说的那个查询表,我们称之为页表,虚拟内存系统通过页表来判断一个虚拟页是否已经缓存在了主存中。如果是,页表会负责到物理页的映射;如果不命中,也就是我们经常会见到的概念缺页,对应的英文是 page fault,系统首先判断这个虚拟页存放在磁盘的哪个位置,然后在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到内存中,替换这个牺牲页。

在磁盘和内存之间传送页的活动叫做交换(swapping)或者页面调度(paging)。

下面,简单介绍下虚拟内存带来的好处。

SRAM缓存:表示位于 CPU 和主存之间的 L1、L2 和 L3 高速缓存。

DRAM缓存:表示虚拟内存系统的缓存,缓存虚拟页到主存中。

物理内存访问速度比高速缓存要慢 10 倍左右,而磁盘要比物理内存慢大约 100000 倍。所以,DRAM 的缓存不命中比 SRAM 缓存不命中代价要大得多,因为 DRAM 缓存一旦不命中,就需要到磁盘加载虚拟页。而 SRAM 缓存不命中,通常由 DRAM 的主存来服务。而从磁盘的一个扇区读取第一个字节的时间开销比起读这个扇区中连续的字节要慢大约 100000 倍。

了解 Kafka 的读者应该知道,消息在磁盘中的顺序存储对于 Kafka 的性能至关重要。

结论就是,IO 的性能主要是由 DRAM 的缓存是否命中决定的。

内存映射文件

英文名是 Memory Mapped Files,相信大家也都听过这个概念,在许多对 IO 性能要求比较高的 java 应用中会使用到,它是操作系统提供的支持,后面我们在介绍 NIO Buffer 的时候会碰到的 MappedByteBuffer 就是用来支持这一特性的。

是什么:

我们可以认为内存映射文件是一类特殊的文件,我们的 Java 程序可以直接从内存中读取到文件的内容。它是通过将整个文件或文件的部分内容映射到内存页中实现的,操作系统会负责加载需要的页,所以它的速度是非常快的。

优势:

  • 一旦我们将数据写入到了内存映射文件,即使我们的 JVM 挂掉了,操作系统依然会帮助我们将这部分内存数据持久化到磁盘上。当然了,如果是断电的话,还是有可能会丢失数据的。
  • 另外,它比较适合于处理大文件,因为操作系统只会在我们需要的页不在内存中时才会去加载页数据,而用其处理大量的小文件反而可能会造成频繁的缺页。
  • 另一个重要的优势就是内存共享。我们可以在多个进程中同时使用同一个内存映射文件,也算是一种进程间协作的方式吧。想像下进程间的数据通讯平时我们一般采用 Socket 来请求,而内存共享至少可以带来 10 倍以上的性能提升。

我们还没有接触到 NIO 的 Buffer,下面就简单地示意一下:

import
 java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import 
java.nio.channels.FileChannel;public class MemoryMappedFileInJava {  
private static int count = 10485760; //10 MB public static void 
main(String[] args) throws Exception { RandomAccessFile memoryMappedFile
 = new RandomAccessFile("largeFile.txt", "rw"); // 将文件映射到内存中,map 方法 
MappedByteBuffer out = 
memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 
count); // 这一步的写操作其实是写到内存中,并不直接操作文件 for (int i = 0; i < count; i++) {
 out.put((byte) 'A'); } System.out.println("Writing to Memory Mapped 
File is completed"); // 这一步的读操作读的是内存 for (int i = 0; i < 10 ; i++) { 
System.out.print((char) out.get(i)); } System.out.println("Reading from 
Memory Mapped File is completed"); }}

我们需要注意的一点就是,用于加载内存映射文件的内存是堆外内存。

参考资料:Why use Memory Mapped File or MapppedByteBuffer in Java

分散/聚集 IO

scatter/gather IO,个人认为这个看上去很酷炫,实践中比较难使用到。

分散/聚集 IO(另一种说法是 vectored I/O 也就是向量 IO)是一种可以在单次操作中对多个缓冲区进行输入输出的方法,可以把多个缓冲区的数据写到单个数据流,也可以把单个数据流读到多个缓冲区中。

Java NIO 基础知识

.

Java NIO 基础知识

.

这个功能是操作系统提供的支持,Java NIO 包中已经给我们提供了操作接口 。这种操作可以提高一定的性能,因为一次操作相当于多次的线性操作,同时这也带来了原子性的支持,因为如果用多线程来操作的话,可能存在对同一文件的操作竞争。

非阻塞 IO

相信读者在很多地方都看到过说 NIO 其实不是代表 New IO,而是 Non-Blocking IO,我们这里不纠结这个。我想之所以会有这个说法,是因为在 Java 1.4 第一次推出 NIO 的时候,提供了 Non-Blocking IO 的支持。

在理解非阻塞 IO 前,我们首先要明白,它的对立面 阻塞模式为什么不好。

比如说 InputStream.read 这个方法,一旦某个线程调用这个方法,那么就将一直阻塞在这里,直到数据传输完毕,返回 -1,或者由于其他错误抛出了异常。

我们再拿 web 服务器来说,阻塞模式的话,每个网络连接进来,我们都需要开启一个线程来读取请求数据,然后到后端进行处理,处理结束后将数据写回网络连接,这整个流程需要一个独立的线程来做这件事。那就意味着,一旦请求数量多了以后,需要创建大量的线程,大量的线程必然带来创建线程、切换线程的开销,更重要的是,要给每个线程都分配一部分内存,会使得内存迅速被消耗殆尽。我们说多线程是性能利器,但是这就是过多的线程导致系统完全消化不了了。

通常,我们可以将 IO 分为两类:面向数据块(block-oriented)的 IO 和面向流(stream-oriented)的 IO。比如文件的读写就是面向数据块的,读取键盘输入或往网络中写入数据就是面向流的。

注意,这节混着用了流和通道这两个词,提出来这点是希望不会对读者产生困扰。

面向流的 IO 往往是比较慢的,如网络速度比较慢、需要一直等待用户新的输入等。

这个时候,我们可以用一个线程来处理多个流,让这个线程负责一直轮询这些流的状态,当有的流有数据到来后,进行相应处理,也可以将数据交给其他子线程来处理,这个线程继续轮询。

问题来了,不断地轮询也会带来资源浪费呀,尤其是当一个线程需要轮询很多的数据流的时候。

现代操作系统提供了一个叫做 readiness selection 的功能,我们让操作系统来监控一个集合中的所有的通道,当有的通道数据准备好了以后,就可以直接到这个通道获取数据。当然,操作系统不会通知我们,但是我们去问操作系统的时候,它会知道告诉我们通道 N 已经准备好了,而不需要自己去轮询(后面我们会看到,还要自己轮询的 select 和 poll)。

后面我们在介绍 Java NIO 的时候会说到 Selector,对应类 java.nio.channels.Selector,这个就是 java 对 readiness selection 的支持。这样一来,我们的一个线程就可以更加高效地管理多个通道了。

Java NIO 基础知识

.

上面这张图我想大家也都可能看过,就是用一个 Selector 来管理多个 Channel,实现了一个线程管理多个连接。说到底,其实就是解决了我们前面说的阻塞模式下线程创建过多的问题。

在 Java 中,继承自 SelectableChannel 的子类就是实现了非阻塞 IO 的,我们可以看到主要有 socket IO 中的 DatagramChannel 和 SocketChannel,而 FileChannel 并没有继承它。所以,文件 IO 是不支持非阻塞模式的。

在系统实现上,POSIX 提供了 select 和 poll 两种方式。它们两个最大的区别在于持有句柄的数量上,select 最多只支持到 FD_SETSIZE(一般常见的是 1024),显然很多场景都会超过这个数量。而 poll 我们想创建多少就创建多少。它们都有一个共同的缺点,那就是当有任务完成后,我们只能知道有几个任务完成了,而不知道具体是哪几个句柄,所以还需要进行一次扫描。

正是由于 select 和 poll 的不足,所以催生了以下几个实现。BSD& OS X 中的 kqueue,Solaris 中的 /dev/poll,还有 Linux 中的 epoll。

Windows 没有提供额外的实现,只能使用 select。

在不同的操作系统上,JDK 分别选择相应的系统支持的非阻塞实现方式。

异步 IO

我们知道 Java 1.4 引入了 New IO,从 Java 7 开始,就不再是 New IO 了,而是 More New IO 来临了,我们也称之为 NIO2。

Java7 在 NIO 上带来的最大的变化应该就属引入了 Asynchronous IO(异步 IO)。本来吧,异步 IO 早就提上日程了,可是大佬们没有时间完成,所以才一直拖到了 java 7 的。废话不多说,简单来看看异步 IO 是什么。

要说异步 IO 是什么,当然还得从 Non-Blocking IO 没有解决的问题入手。非阻塞 IO 很好用,它解决了阻塞式 IO 的等待问题,但是它的缺点是需要我们去轮询才能得到结果。

而异步 IO 可以解决这个问题,线程只需要初始化一下,提供一个回调方法,然后就可以干其他的事情了。当数据准备好以后,系统会负责调用回调方法。

异步 IO 最主要的特点就是回调,其实回调在我们日常的代码中也是非常常见的。

最简单的方法就是设计一个线程池,池中的线程负责完成一个个阻塞式的操作,一旦一个操作完成,那么就调用回调方法。比如 web 服务器中,我们前面已经说过不能每来一个请求就新开一个线程,我们可以设计一个线程池,在线程池外用一个线程来接收请求,然后将要完成的任务交给线程池中的线程并提供一个回调方法,这样这个线程就可以去干其他的事情了,如继续处理其他的请求。等任务完成后,池中的线程就可以调用回调方法进行通知了。

另外一种方式就是自己不设计线程池,让操作系统帮我们实现。流程也是基本一样的,提供给操作系统回调方法,然后就可以干其他事情了,等操作完成后,操作系统会负责回调。这种方式的缺点就是依赖于操作系统的具体实现,不过也有它的一些优势。

首先,我们自己设计处理任务的线程池的话,我们需要掌握好线程池的大小,不能太大,也不能太小,这往往需要凭我们的经验;其次,让操作系统来做这件事情的话,操作系统可以在一些场景中帮助我们优化性能,如文件 IO 过程中帮助更快找到需要的数据。

操作系统对异步 IO 的实现也有很多种方式,主要有以下 3 中:

  1. Linux AIO:由 Linux 内核提供支持
  2. POSIX AIO:Linux,Mac OS X(现在该叫 Mac OS 了),BSD,solaris 等都支持,在 Linux 中是通过 glibc 来提供支持的。
  3. Windows:提供了一个叫做 completion ports 的机制。

这篇文章 asynchronous disk I/O 的作者表示,在类 unix 的几个系统实现中,限制太多,实现的质量太差,还不如自己用线程池进行管理异步操作。

而 Windows 系统下提供的异步 IO 的实现方式有点不一样。它首先让线程池中的线程去自旋调用 GetQueuedCompletionStatus.aspx) 方法,判断是否就绪。然后,让任务跑起来,但是需要提供特定的参数来告诉执行任务的线程,让线程执行完成后将结果通知到线程池中。一旦任务完成,操作系统会将线程池中阻塞在 GetQueuedCompletionStatus 方法的线程唤醒,让其进行后续的结果处理。

Windows 智能地唤醒那些执行 GetQueuedCompletionStatus 方法的线程,以让线程池中活跃的线程数始终保持在合理的水平。这样就不至于创建太多的线程,降低线程切换的开销。

Java 7 在异步 IO 的实现上,如果是 Linux 或者其他类 Unix 系统上,是采用自建线程池实现的,如果是 Windows 系统上,是采用系统提供的 completion ports 来实现的。

所以,在非阻塞 IO 和异步 IO 之间,我们应该怎么选择呢?

如果是文件 IO,我们没得选,只能选择异步 IO。

如果是 Socket IO,在类 unix 系统下我们应该选择使用非阻塞 IO,Netty 是基于非阻塞模式的;在 Windows 中我们应该使用异步 IO。

当然了,Java 的存在就是为了实现平台无关化,所以,其实不需要我们选择,了解这些权当让自己涨点知识吧。

总结

和其他几篇文章一样,也没什么好总结的,要说的都在文中了,希望读者能学到点东西吧。

如果哪里说得不对了,我想也是正常的,我这些年写的都是 Java,对于底层了解得愈发的少了,所以如果读者发现有什么不合理的内容,非常希望读者可以提出来。

lambda 表达式和闭包

区分lambda表达式和闭包

熟悉的Javascript或者Ruby的同学,可能对另一个名词:闭包更加熟悉。因为一般闭包的示例代码,长得跟lambda差不多,导致我也在以前很长一段时间对这两个概念傻傻分不清楚。其实呢,这两个概念是完全不同维度的东西。

闭包是个什么东西呢?我觉得Ruby之父松本行弘在《代码的未来》一书中解释的最好:闭包就是把函数以及变量包起来,使得变量的生存周期延长。闭包跟面向对象是一棵树上的两条枝,实现的功能是等价的。

这样说可能不够直观,我们还是用代码说话吧。其实Java在很早的版本就支持闭包了,只是因为应用场景太少,这个概念一直没得到推广。在Java6里,我们可以这样写:

public static Supplier<Integer> testClosure(){

final int i = 1;

return new Supplier<Integer>() {

@Override

public Integer get() {

return i;

}

};

}

public interface Supplier<T> {

T get();

}

看出问题了么?这里i是函数testClosure的内部变量,但是最终返回里的匿名对象里,仍然返回了i。我们知道,函数的局部变量,其作用域仅限于函数内部,在函数结束时,就应该是不可见状态,而闭包则将i的生存周期延长了,并且使得变量可以被外部函数所引用。这就是闭包了。这里,其实我们的lambda表达式还没有出现呢!

而支持lambda表达式的语言,一般也会附带着支持闭包了,因为lambda总归在函数内部,与函数局部变量属于同一语句块,如果不让它引用局部变量,不会让人很别扭么?例如Python的lambda定义我觉得是最符合λ算子的形式的,我们可以这样定义lambda:

#!/usr/bin/python

y = 1

f=lambda x: x + y

print f(2)

y = 3

print f(2)

输出:

3

5

这里y其实是外部变量。

Java中闭包带来的问题

在Java的经典著作《Effective Java》、《Java Concurrency in Practice》里,大神们都提到:匿名函数里的变量引用,也叫做变量引用泄露,会导致线程安全问题,因此在Java8之前,如果在匿名类内部引用函数局部变量,必须将其声明为final,即不可变对象。(Python和Javascript从一开始就是为单线程而生的语言,一般也不会考虑这样的问题,所以它的外部变量是可以任意修改的)。

在Java8里,有了一些改动,现在我们可以这样写lambda或者匿名类了:

public static Supplier<Integer> testClosure() {

int i = 1;

return () -> {

return i;

};

}

这里我们不用写final了!但是,Java大神们说的引用泄露怎么办呢?其实呢,本质没有变,只是Java8这里加了一个语法糖:在lambda表达式以及匿名类内部,如果引用某局部变量,则直接将其视为final。我们直接看一段代码吧:

public static Supplier<Integer> testClosure() {

int i = 1;

i++;

return () -> {

return i; //这里会出现编译错误

};

}

明白了么?其实这里我们仅仅是省去了变量的final定义,这里i会强制被理解成final类型。很搞笑的是编译错误出现在lambda表达式内部引用i的地方,而不是改变变量值的i++…这也是Java的lambda的一个被人诟病的地方。我只能说,强制闭包里变量必须为final,出于严谨性我还可以接受,但是这个语法糖有点酸酸的感觉,还不如强制写final呢…