本文主要讲解一下在 JVM 中如何保存 Java 对象以及 Java 对象指针压缩相关的东西。
上图是 JVM 规范中定义的体系结构(这个只是定义的规范,实际的 JVM 实现中可能与这个结构会有差异),这里我们主要看下运行时数据区(runtime data areas)的内容,以下摘自 The Java® Virtual Machine Specification Java SE 8 Edition
堆和方法区是所有类共享的,其中堆主要存储对象实体,方法区存储的信息比较多,主要包括下面几类:
我们知道一个Java对象包含两部分内容,字段和方法,每个对象的字段值都可能不同,但是所用的方法都是一样的,如果每个对象都保存一套方法定义,显然会浪费很多的空间。所以方法定义相关的都放到了方法区,对象只保存自己的实例数据和指向方法定义的指针。下图是对象保存的一种方式,也是 Hotspot 虚拟机采用的方式,对象在堆中只保存实例的数据,同时会有一个指针指向方法区中的一个方法表(和 c++ 中的 Virtual method table 类似)。方法表保存两个部分:指向类数据的指针和执行各个方法的指针。这里将类数据和方法分开存储,是为了更加快速的找到方法。每个类都会对应一个方法表,这种实现方式会稍微浪费一些内存,但是会获得更好的性能。
我们知道对象是有继承关系的,如果子类没有覆写父类的方法,那么子类会指向父类的中的方法。
上面主要是 Java 虚拟机规范中定义的规范,每种虚拟机实现的方式可能不太相同,这里我们主要看下 HotSpot 虚拟机的实现,后面的内容都是基于 HotSpot 虚拟机。
在 Java8 中,HotSpot VM 移除了永生代(PermGen),添加了元数据空间(Metaspace),元空间不使用虚拟机内存,而是使用本地内存。元空间主要和方法区对应,存储类的元数据和常量池(String常量的实例存在堆中)等信息。
图片摘自 http://java-latte.blogspot.com/2014/03/metaspace-in-java-8.html
在 JVM 中 Java 对象使用 OOP(Ordinary Object Pointer) 来表示,格式如下图所示。
OOP 主要包含两个部分:对象头和实例数据。对象头主要包含四个部分:
对象头后面就是实例数据,可能是基本数据,也可能是指向其他对象的引用。如果实例数据的大小不是 8 的倍数,那么也会插入一些填充的数据来对齐。对于继承的情况,会先存放父类的实例数据,然后再存放子类的实例数据,如下图所以:
Mark word、Klass word 以及对象的引用大小和 JVM 位数相关,32位 JVM 是 4 字节大小,64 位 JVM 是 8 字节大小。对于引用来说,4字节来寻址的话的最多可以表示 232,也就是做大只能支持 4GB 的内存,一般来说4GB 的内存是不大够用的,所以我们常用的是 64 位的 JVM,但是使用 64 位 JVM 带来的一个问题就是引用从 4 个字节变成了 8 个字节,也就是会多占一倍的空间,这样会导致更加频繁的 GC 周期,导致性能变差。
我们使用压缩的 OOP 来实现在64位的 JVM 上使用32位大小的引用来寻址,这个方式主要是基于 Java 对象是 8 字节对齐,即后三位全部为 0,也就是在当前的对象引用中后三位实际上是没有用到的。基于上面的逻辑,我们就可以做一下优化,将当前32位值的表示为第 4-35 位的值,也就是实际的值相当于左移了三位,如下图所示。这样我们就有35位来寻址,内存最大就可以支持到 32GB。
开启了压缩之后,堆中 OOP 里的下列字段会被压缩:
下面是 Integer 对象在不同情况下占的内存大小,因为 Java 是 8 字节对齐,所以在64位 VM 上未开启压缩时,Integer 还要加上一个 32bit 填充,即总的大小是 192 bit。
图片摘自 https://www.javacodegeeks.com/2016/05/compressedoops-introduction-compressed-references-java.html
我们可以在启动 Java 程序时使用 -XX:+UseCompressedOops
来开启压缩,Java7之后,如果最大内存小于32G,会自动开启 OOP 压缩。如果想在超过 32G 内存的情况下使用压缩,可以通过指定Java 对象对齐的字节数来实现 -XX:ObjectAlignmentInBytes
,该值必须在 8 到 256 之间,并且是 2 的指数倍。假设指定为 16,那么就可以使用 64G 的内存,但是由于对齐造成的内存浪费也会更多。
另外在 Java11 中添加的 ZGC 垃圾回收器必须使用 64 位的指针,所以它不支持压缩的OOP。
Lambda(λ) 表达式是一种在 被调用的位置 或者 作为参数传递给函数的位置 定义匿名函数对象 的简便方法。下面是关于 Lambda 表达式的几个点:
下面是一个示例1
2
3
4
5
6
7
8
9
10
11
12
13
interface Calculator {
int cal(int a, int b);
}
public class HelloWorld {
public static void main(String[] args) {
Calculator c = (a, b) -> a + b;
System.out.println(c.cal(1, 2));
c = (a, b) -> a * b;
System.out.println(c.cal(1, 2));
}
}
Lambda 表达式的基本形式如下所示:1
(argument list) -> code
下面是一个例子:
如上所示: Lambda 表达式包含三个部分:
(Apple a1, Apple a2)
a1.getWeight().compareTo(a2.getWeight())
,该 Lambda 主体会返回 compareTo 的结果。Lambda 函数的主体可以是表达式(expression)或者语句(statement),所以 Lambda 函数返回值有下面两种情况:
关于语句和表达式的区别,可以参考 这篇文章,这里简单说一下:假设有一条语句 int c = a + b;
,那么表达式就是指 c = a + b
,即不包含 int
和 ;
,每个表达式都会有一个计算值(void 也算一种特殊的计算值)。
所以细分一下,Lambda 表达式有两种形式:1
(parameters) -> expression
和(使用大括号)1
(parameters) -> {statements}
下面是 Lambda 表达式的几个例子:
使用场景 | 使用示例 |
---|---|
boolean 表达式 | (List<String> list) -> list.isEmpty() |
创建对象 | () -> new Apple(10) |
Consuming from an object | (Apple a) -> { System.out.println(a.getWeight()); } |
Select/extract from an object | (String s) -> s.length() |
合并两个值 | (int a, int b) -> a * b |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
我们可以在函数式接口 (Functional interface)中使用 Lambda 表达式。简单来说,函数式接口就是只定义一个抽象方法的接口(接口中可以包含额外的 default 方法)。例如 Comparator 和 Runnable 都是函数式接口:1
2
3
4
5
6
7public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
我们一般在接口定义中加上 @FunctionalInterface
注解来声明该接口是一个函数式接口。例如下面的形式:1
2
3
4
public interface Function<T, R> {
R apply(T t);
}
当一个接口通过 FunctionalInterface
被声明为函数式接口时,编译器将会检查接口的合法性,如果接口不合法,会报编译错误。
现在考虑一个问题,Lambda 表达式是如何匹配函数式接口的呢?假设我们有一个如下定义的函数式接口:1
2
3
4
interface Calculator {
int cal(int a, int b);
}
下面是使用 lambda 表达式以及匿名类来创建 Calculator 对象的示例代码。在下面的代码中对象 c 和 c2 的实现是等价的。1
2
3
4
5
6
7
8
9
10public void demo() {
Calculator c = (int a, int b) -> a + b;
Calculator c2 = new Calculator() {
public int cal(int a, int b) {
return a + b;
}
};
}
从上面的例子中,我们可以看到 Lambda 表达式 是和函数式接口中的 抽象方法 进行匹配的,其中 Lambda 表达式中参数匹配 cal 方法的参数,Lambda body 的内容作为抽象方法的具体实现,Lambda body 的计算值作为方法的返回值。这也是为什么要求函数式接口只能有一个抽象方法的原因。
函数式接口中抽象方法的签名(signature)描述了 Lambda 表达式的签名,因为 Lambda 表达式并没有名字,所以这里的签名只关注三个方面:方法参数 、返回值 以及 异常声明。我们将抽象方法所描述的 Lambda 形式称为函数描述符(function descriptor)。在 Calculator 类中,cal 方法对应的函数描述符为 (int, int) -> int
,即接受两个 int 类型作为参数,表达式的计算值为 int 类型。所以下面的 Lambda 表达式都是合法的:1
2
3(int a, int b) -> a
(int a, int b) -> a + b
(int a, int b) -> 0
如果 Lambda 表达式抛出一个可检查异常,那么对应的抽象方法所声明的 throws 语句也要与之匹配。看下面的一个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface ThrowExceptionInterface {
void run(int a, int b);
}
public class LambdaTest {
public void throwException() {
// 这里编译时会报 Unhandled Exception:java.io.Exception
ThrowExceptionInterface t = (int a, int b) -> {
throw new IOException();
};
}
}
其实也很好理解,Lambda body 中的内容会作为抽象方法的具体实现,在方法中抛出了异常但是方法声明中却没有相关的异常声明,编译器肯定要报错的。
另外还有一个特殊的 void 兼容规则。如果抽象方法的返回值为 void,即对应的函数描述符为 (T) -> void
,那么对于 body 为 语句表达式(statement expression) 的 Lambda 表达式,只要求参数列表匹配即可。看下面的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface VoidInterface {
void run(int a);
}
public class LambdaTest {
public static void voidTest() {
List<String> list = new ArrayList<>();
// 这里 a++ 返回一个 int,但是和 void 兼容
VoidInterface v = (int a) -> a++;
// 下面的代码会报错,因为 a-1 不是一个语句表达式
v = (int a) -> a-1;
}
}
这里说一下语句表达式:
The term “statement expression” or “expression statement” refers to expressions that are also allowed to be used as a statement.
语法表达式有下面四类:
Lambda 表达式本身并不包含它是实现哪个函数式接口的信息,编译器会根据 Lambda 表达式所处的上下文(context)环境来推断 Lambda 表达式的目标类型(target type),例如对于下面的代码:1
Calculator c = (int a, int b) -> a + b;
Lambda 表达式会赋值给 Calculator 对象,那么该 Lambda 表达式对应的目标类型就是 Calculator 接口,该接口中的 cal 方法对应的函数描述符为 (int, int) -> int
,这个和 (int a, int b) -> a + b
可以匹配,这样就完成了类型检查。下图是一个完整的例子:
在上面我们提到编译器会根据上下文环境推断出与 Lambda 表达式对应的函数式接口,这意味着编译器同样可以根据接口中抽象方法的函数函数描述符推断出 Lambda 表达式的签名,这样编译器就可以知道 Lambda 表达式的参数类型,这样就可以省略 Lambda 表达式中的参数类型,1
2
3Calculator c = (a, b) -> a + b;
// 当只有一个参数时,可以省略掉 ()
VoidInterface v = a -> a++;
在 Java 8 中定义了一些函数式接口,位于 java.util.function
包下,下面是这些接口的总览: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+--- BiConsumer.java
+--- BiFunction.java
+--- BinaryOperator.java
+--- BiPredicate.java
+--- BooleanSupplier.java
+--- Consumer.java
+--- DoubleBinaryOperator.java
+--- DoubleConsumer.java
+--- DoubleFunction.java
+--- DoublePredicate.java
+--- DoubleSupplier.java
+--- DoubleToIntFunction.java
+--- DoubleToLongFunction.java
+--- DoubleUnaryOperator.java
+--- Function.java
+--- IntBinaryOperator.java
+--- IntConsumer.java
+--- IntFunction.java
+--- IntPredicate.java
+--- IntSupplier.java
+--- IntToDoubleFunction.java
+--- IntToLongFunction.java
+--- IntUnaryOperator.java
+--- LongBinaryOperator.java
+--- LongConsumer.java
+--- LongFunction.java
+--- LongPredicate.java
+--- LongSupplier.java
+--- LongToDoubleFunction.java
+--- LongToIntFunction.java
+--- LongUnaryOperator.java
+--- ObjDoubleConsumer.java
+--- ObjIntConsumer.java
+--- ObjLongConsumer.java
+--- Predicate.java
+--- Supplier.java
+--- ToDoubleBiFunction.java
+--- ToDoubleFunction.java
+--- ToIntBiFunction.java
+--- ToIntFunction.java
+--- ToLongBiFunction.java
+--- ToLongFunction.java
+--- UnaryOperator.java
用来测试对象是否满足某种条件。该接口定义了一个 test 方法,接受一个泛型对象(T),并返回测试结果(boolean),函数描述符为 T -> boolean
。下面是一个使用示例:1
2
3
4
5
6
7
8public <T> boolean judge(T t, Predicate<T> p) {
return p.test(t);
}
public void testPredicate() {
String text = "111";
System.out.println(judge(text, s -> s != null));
}
下面是 Predicate 接口的实现: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/**
* Represents a predicate (boolean-valued function) of one argument.
*/
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return true if the input argument matches the predicate,
* otherwise false
*/
boolean test(T t);
/**
* Returns a composed predicate that represents a short-circuiting logical
* AND of this predicate and another. When evaluating the composed
* predicate, if this predicate is false, then the other
* predicate is not evaluated.
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/**
* Returns a predicate that represents the logical negation of this
* predicate.
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/**
* Returns a composed predicate that represents a short-circuiting logical
* OR of this predicate and another. When evaluating the composed
* predicate, if this predicate is true, then the other
* predicate is not evaluated.
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
/**
* Returns a predicate that tests if two arguments are equal according
* to {@link Objects#equals(Object, Object)}.
*/
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
我们看到在 Predicate 类中,除了 test 方法,还定义了三个 default 方法,and
, or
和 negate
,它们分别对应逻辑运算中的与(&&)、或(||)、非(!)操作。通过这三个方法,我们可以构造更复杂的 predicate 表达式:1
2
3
4
5
6
7
8public void testPredicate() {
String text = "111";
Predicate<String> a = s - > s != null;
Predicate<String> b = s - > s.length() > 3;
System.out.println(judge(text, a.and(b)));
System.out.println(judge(text, a.negate()));
System.out.println(judge(text, a.or(b)));
}
对应的输出结果为:1
2
3false
false
true
另外 and 和 or 方法是按照在表达式链中的位置,从左向右确定优先级的。因此 a.or(b).and(c)
可以看作 (a || b) && c
。
BiPredicate 针对两个参数对象(T, U)进行测试,函数描述符为 (T, U) -> boolean
。下面是该接口的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* Represents a predicate (boolean-valued function) of two arguments. This is
* the two-arity specialization of {@link Predicate}.
*/
public interface BiPredicate<T, U> {
boolean test(T t, U u);
default BiPredicate<T, U> and(BiPredicate<? super T, ? super U> other) {
Objects.requireNonNull(other);
return (T t, U u) -> test(t, u) && other.test(t, u);
}
default BiPredicate<T, U> negate() {
return (T t, U u) -> !test(t, u);
}
default BiPredicate<T, U> or(BiPredicate<? super T, ? super U> other) {
Objects.requireNonNull(other);
return (T t, U u) -> test(t, u) || other.test(t, u);
}
}
下面是一个使用示例:1
2
3
4
5public void testBiPredicate() {
BiPredicate<Integer, Integer> b = (x, y) -> x > 0 && y > 3;
boolean r = b.test(1, 4);
System.out.println(r);
}
Consumer(消费者),针对对象进行某种操作(消费对象)。该接口定义了一个 accept 方法,会将该方法作用于目标对象,函数描述符为 T -> void
。下面是使用示例:1
2
3
4
5
6
7
8
9public <T> void consume(T t, Consumer<T> c) {
c.accept(t);
}
public void testConsume() {
String text = "1234";
consume(text, s -> System.out.println(s.substring(2)));
}
下面是 Consumer 类的代码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/**
* Represents an operation that accepts a single input argument and returns no
* result. Unlike most other functional interfaces, Consumer is expected
* to operate via side-effects.
*/
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
/**
* Returns a composed Consumer that performs, in sequence, this
* operation followed by the after operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the after operation will not be performed.
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
Consumer 中定义了一个 andThen 的 default 方法,通过该方法我们可以对目标对象进行链式(chain)处理,下面是一个示例:1
2
3
4
5
6
7
8public void testConsume() {
StringBuilder builder = new StringBuilder();
Consumer <StringBuilder> a = s -> s.append("abcd");
Consumer <StringBuilder> b = s -> s.reverse();
Consumer <StringBuilder> c = s -> s.append("1234");
consume(builder, a.andThen(b).andThen(c));
System.out.println(builder.toString());
}
输出结果为:1
dcba1234
BiConsumer 针对两个对象(T, U)进行操作,对应的函数描述符为 (T, U) -> void
。下面是该接口的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* Represents an operation that accepts two input arguments and returns no
* result. This is the two-arity specialization of Consumer.
* Unlike most other functional interfaces, BiConsumer is expected
* to operate via side-effects.
*/
public interface BiConsumer<T, U> {
void accept(T t, U u);
default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
Objects.requireNonNull(after);
return (l, r) -> {
accept(l, r);
after.accept(l, r);
};
}
}
下面是一个例子:1
2
3
4public void testBiConsumer() {
BiConsumer<String, String> b = (x, y) -> System.out.println(x + y);
b.accept("111", "222");
}
Supplier(供应商),返回一个泛型对象(生产对象)。该接口中定义了一个 get 方法,没有方法参数,返回值是一个泛型对象,函数描述符为 () -> T
。下面是一个使用示例1
2
3
4
5
6
7
8public <T> T supplier(Supplier<T> s) {
return s.get();
}
public void testSupplier() {
String text = supplier(() -> "1111");
System.out.println(text);
}
下面是 Supplier 接口的定义:1
2
3
4
5
6
7
8
9
10
11/**
* Represents a supplier of results.
*/
public interface Supplier<T> {
/**
* Gets a result.
*/
T get();
}
Function 接口就相当于 y=f(x)
中的函数 f,接收一个 x(argument)返回计算值 y(result)。该接口定义了一个 apply 方法,接收一个 T 类型的对象,返回一个 R 类型的结果,函数描述符为 T -> R
。下面是一个使用示例:1
2
3
4
5
6
7
8
9
10public <T, R> R func(T t, Function<T, R> f) {
return f.apply(t);
}
public void testFunction() {
String text = "1234";
int i = func(text, t -> Integer.parseInt(t));
// 输出 1235
System.out.println(i + 1);
}
下面是 Function 接口的定义: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/**
* Represents a function that accepts one argument and produces a result.
*/
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*/
R apply(T t);
/**
* Returns a composed function that first applies the before
* function to its input, and then applies this function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*/
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
/**
* Returns a composed function that first applies this function to
* its input, and then applies the after function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
/**
* Returns a function that always returns its input argument.
*/
static <T> Function<T, T> identity() {
return t -> t;
}
}
在 Function 接口中定义了两个 default 方法:compose
和 andThen
可以进行链式的调用,假设有两个函数 f(x) 和 g(x):1
2f.compose(g) => f(g(x))
f.andThen(g) => g(f(x))
下图是一个详细的解释
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11public void testFunction() {
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
int i = func(1, f.andThen(g));
// 输出 4
System.out.println(i);
i = func(1, f.compose(g));
// 输出 3
System.out.println(i);
}
UnaryOperator 是一种特殊的 Function,表示操作数和返回值是同一种类型,函数描述符为 T -> T
。下面是该接口的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* Represents an operation on a single operand that produces a result of the
* same type as its operand. This is a specialization of {@code Function} for
* the case where the operand and result are of the same type.
*/
public interface UnaryOperator<T> extends Function<T, T> {
/**
* Returns a unary operator that always returns its input argument.
*/
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
下面是一个使用示例:1
2
3
4public void testUnaryOperator() {
UnaryOperator<Integer> u = x -> x + 1;
System.out.println(u.apply(1));
}
BiFunction 接收两个参数(T, U),返回一个结果(R),类似于 z=f(x, y),对应的函数描述符为 (T, U) -> R
。下面是该接口的具体实现: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/**
* Represents a function that accepts two arguments and produces a result.
* This is the two-arity specialization of Function.
*/
public interface BiFunction<T, U, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
/**
* Returns a composed function that first applies this function to
* its input, and then applies the {@code after} function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*/
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t, U u) -> after.apply(apply(t, u));
}
}
下面是一个使用示例:1
2
3
4
5public void testBiFunction() {
BiFunction<Integer, Double, String> b = (i, d) -> String.valueOf(i + d);
String r = b.apply(1, 2.5);
System.out.println(r);
}
BinaryOperator 是一种特殊的 BiFunction,表示接收的参数和返回的结果都是同一种类型 T,函数描述符为 (T, T) -> T
。下面是该接口的定义: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/**
* Represents an operation upon two operands of the same type, producing a result
* of the same type as the operands. This is a specialization of
* BiFunction for the case where the operands and the result are all of
* the same type.
*/
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
/**
* Returns a BinaryOperator which returns the lesser of two elements
* according to the specified Comparator.
*/
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
/**
* Returns a BinaryOperator which returns the greater of two elements
* according to the specified Comparator.
*/
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void testBinaryOperator() {
BinaryOperator<Integer> b = (x, y) -> x + y;
int z = b.apply(1, 3);
System.out.println(z);
BinaryOperator<Integer> min = BinaryOperator.minBy((x, y) -> x - y);
// 输出 1
z = min.apply(1, 3);
System.out.println(z);
// 输出 3
BinaryOperator<Integer> max = BinaryOperator.maxBy((x, y) -> x - y);
z = max.apply(1, 3);
System.out.println(z);
}
在上面提到的接口中,都是接受泛型参数,我们知道泛型参数只能是引用类型,也就是说对于 int 这样的基本类型,我们要首先装箱(boxing)成 Integer 类型,在使用的时候再拆箱(unboxing)成 int。虽然 Java 提供了自动装箱机制,但是在性能方面是要付出代价的。所以对于上述的函数式接口,Java 8 提供了针对基本类型的版本,以此来避免输入输出是基本类型时的自动装箱操作。以 Predicate 为例,假设我们要检测一个 int 是否满足某个条件,我们可以使用 IntPredicate :1
2
3
4
5public void testIntPredicate() {
IntPredicate ip = x -> x > 3;
boolean r = ip.test(4);
System.out.println(r);
}
下面是 IntPredicate 的定义,我们可以看到它将泛型 T 改为了基本类型 int。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* Represents a predicate (boolean-valued function) of one {@code int}-valued
* argument. This is the {@code int}-consuming primitive type specialization of
* {@link Predicate}.
*/
public interface IntPredicate {
boolean test(int value);
default IntPredicate and(IntPredicate other) {
Objects.requireNonNull(other);
return (value) -> test(value) && other.test(value);
}
default IntPredicate negate() {
return (value) -> !test(value);
}
default IntPredicate or(IntPredicate other) {
Objects.requireNonNull(other);
return (value) -> test(value) || other.test(value);
}
}
下表列出了 Java 8 中的函数式接口以及其对应的基本类型版本:
函数式接口 | 函数描述符 | 基本类型版本 |
---|---|---|
Predicate<T> | T -> boolean | IntPredicate, LongPredicate, DoublePredicate |
BiPredicate<T> | (L, R) -> boolean | |
Consumer<T> | T -> void | IntConsumer, LongConsumer, DoubleConsumer |
BiConsumer<T, U> | (T, U) -> void | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
Supplier<T> | () -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
Function<T, R> | T -> R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
UnaryOperator<T> | T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BiFunction<T, U, R> | (T, U) -> R | ToIntBiFunction<T, U> , ToLongBiFunction<T, U> , ToDoubleBiFunction<T, U> |
BinaryOperator<T> | (T, T) -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
ORM,即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。下面是一个示例。通过使用 ORM,我们只需要操作 Author 和 Blog 对象,而不用操作相关的数据库表。这里主要介绍一下 Django ORM 的相关使用。
使用 ORM 最大的优点就是快速开发,让我们将更多的精力放在业务上而不是数据库上,下面是 ORM 的几个优点
ORM 的最令人诟病的地方就是性能问题,不过现在已经提高了很多,下面是 ORM 的几个缺点
一般来说 ORM 足以满足我们的需求,如果对性能要求特别高或者查询十分复杂,可以考虑使用原生 SQL 和 ORM 共用的方式
在 Django 框架中集成了 ORM 模块,我们来看下具体的使用,部分内容会给出基于 MySQL 的 SQL 语句。
在创建完 Model 对象之后,Django 会自动为其关联一个 Manager 对象,该对象是 Model 进行数据库操作的接口。默认的 Manager 对象名称为 objects,下面是使用 Manager 进行增删改查的一个示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def save_blog():
# 使用 get 检索数据时,如果数据不存在,会报 DoesNotExist 错误
# 可以使用 Blog.objects.all().filter(id=1).first() 方法
author = Author.objects.get(id=1)
blog = Blog(title='blog2', content='blog2', author=author)
blog.save()
def update_blog():
blog = Blog.objects.all().get(id=2)
blog.title = 'change_title'
blog.save()
def delete_blog():
blog = Blog.objects.all().filter(id=2).first()
if blog is not None:
blog.delete()
def fetch_blog():
blogs = Blog.objects.all()
for blog in blogs:
print blog
我们可以自定义 Manager 的名称,如下所示:1
2
3
4
5
6
7from django.db import models
class Person(models.Model):
#...
people = models.Manager()
Person.people.all()
同时我们也可以定义自己的 Manager,为 Manager 加上一些额外的功能,下面的示例会为 Author 添加上所写 Blog 数量信息: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
33class AuthorManager(models.Manager):
def with_blog_counts(self, **kwargs):
from django.db import connection
cursor = connection.cursor()
condition = ''
if kwargs.has_key('id'):
condition = 'a.id = %s and ' % kwargs['id']
query = '''
SELECT a.id, a.name, COUNT(b.id)
FROM orm_author a LEFT JOIN orm_blog b
ON a.id = b.author_id
WHERE %s TRUE
GROUP BY a.id
''' % condition
cursor.execute(query)
result_list = []
for row in cursor.fetchall():
author = self.model(id=row[0], name=row[1])
author.blog_count = row[2]
result_list.append(author)
return result_list
class Author(models.Model):
name = models.CharField(max_length=50)
objects = AuthorManager()
authors = Author.objects.with_blog_counts(id=2)
for author in authors:
print author.name, author.blog_count
另外我们也可以为 Model 指定多个 Manager1
2
3class Author(models.Model):
manager = models.Manager()
manager2 = AuthorManager()
从数据库中查询出来的结果一般是一个集合,这个集合称为 QuerySet。QuerySet 有两种来源:通过 Manager 的方法获取、通过 QuerySet 自身的方法获得。Manager 的查询方法和 QuerySet 的方法大部分同名、同意(Manager的就是基于 QuerySet 的实现的),例如 filter, exclude等,但两者也有不同的方法,例如 Manager 的 create、get_or_create,QuerySet 的 delete 等。
下面是 QuerySet (也是 Manager的)的几个基本的查询方法
all() - 获得数据库中所有实例的一个 QuerySet
1 | Blog.objects.all() |
filter(**kwargs) - 返回满足查询条件的 QuerySet
1 | Blog.objects.filter(title='blog2') |
exclude(**kwargs) - 获得不满足查询条件的 QuerySet
1 | Blog.objects.exclude(title='blog2') |
get(**kwargs) — 从数据库中获得一个匹配的结果(一个实例),如果没有匹配结果或者匹配结果大于一个都会报错
在前面的 filter、exclude 和 get 方法中,我们需要传入参数作为选择条件: title='blog2'
,这个就是字段查询。字段查询的格式如下1
field__lookuptype=value # 中间是两个下划线
lookuptype 的类型有下面几种
title='blog2'
就相当于 title__exact='blog2'
a LIKE BINARY '%b%'
a LIKE '%b%'
a LIKE 'b'
a LIKE BINARY 'b%'
a LIKE 'b%'
a LIKE BINARY '%b'
a LIKE '%b'
我们还可以进行关联查询,下面的例子是查询所有 author name 为 zjk 的 blog,1
2
3
4
5
6blogs = Blog.objects.filter(author__name='zjk')
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` INNER JOIN `orm_author` ON (`orm_blog`.`author_id` = `orm_author`.`id`)
# WHERE `orm_author`.`name` = 'zjk'
有时候我们并不需要获取查询集的全部数据,而只需要一个子集,一个常见的场景就是进行分页查询。使用 Python 的切片语法可以限制 QuerySet 的实例数量,ORM 会将翻译成 SQL 的 LIMIT 和 OFFSET 子句,下面是几个例子:
放回 QuerySet 的前 5 个元素
1 | blogs = Blog.objects.all()[:5] |
返回 QuerySet 的第 6-10 个元素
1 | blogs = Blog.objects.all()[5:10] |
使用切片的 step 参数,下面代码返回第 1、3、5、7、9 个元素
1 | blogs = Blog.objects.all()[:10:2] |
如果只要访问一个元素,可以直接用索引来访问:
1 | blog = Blog.objects.all()[2] |
QuerySet 是惰性加载的,创建查询集不会访问数据库,只有查询集需要求值时,才会真正运行这个查询。在下面的例子中只有执行 print q
才会真正的去查询数据库。1
2
3
4
5
6
7
8
9q = Blog.objects.filter(title='blog2')
q = q.filter(content='blog2')
q = q.exclude(id=3)
# 执行下面的语句才会真正访问数据库
print q
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content` FROM `orm_blog`
# WHERE (`orm_blog`.`title` = 'blog2' AND `orm_blog`.`content` = 'blog2' AND NOT (`orm_blog`.`id` = 3)) LIMIT 21
关联对象也是惰性加载,只有用到了关联对象的值才会访问数据库1
2
3
4
5
6
7
8
9
10blog = Blog.objects.filter(id=3).first()
print blog.title
# 只有执行下面的语句才会访问数据库获取 author 的值,也就是执行第二条 SQL
print blog.author.name
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`id` = 3 ORDER BY `orm_blog`.`id` ASC LIMIT 1
#
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author` WHERE `orm_author`.`id` = 1
一般来说只要用到了 QuerySet 以及里面对象的信息,就会访问数据库。下面是文档中给出的几种会对查询集求值的情况:
每个 QuerySet 都包含一个缓存来最小化对数据库的访问,下面是一个示例:1
2
3
4
5
6
7
8# 下面代码会访问两次数据库
print [blog.title for blog in Blog.objects.all()]
print [blog.content for blog in Blog.objects.all()]
# 下面代码只会访问一次数据库
blogs = Blog.objects.all()
print [blog.title for blog in blogs]
print [blog.content for blog in blogs]
在一个新的 QuerySet 中,缓存为空。当首次对 QuerySet 的所有实例进行求值时,会将查询结果保存到 QuerySet 的缓冲中。当再访问该 QuerySet 时,会直接从缓冲中取数据。下面是一个示意图:
如果只对 QuerySet 的部分实例(query_set[5], query_set[0:10])进行求值,首先会到 QuerySet 的缓冲中查找是否已经缓存了这些实例,如果有就使用缓存值,如果没有就查询数据库,但是不会将查询结果保存到缓冲中。如下图所示:
如果 QuerySet 数量很大不希望被缓存,遍历时使用 iterator 方法:1
2
3blogs = Blog.objects.all()
for blog in blogs.iterator():
print blog.title
在讲关联查询之前,首先看一下下面的一个示例。我们前面提到,关联实例是惰性加载的,因此对于下面的代码,每次 for 循环都要访问一次数据库,会严重影响性能。因此我们需要一次将 blog 以及 author 的信息全部取出来,这就是我们马上要讲的关联查询。1
2
3
4
5
6
7
8
9for blog in Blog.objects.all():
print blog.title, blog.author.name
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content` FROM `orm_blog`
#
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author` WHERE `orm_author`.`id` = 1
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author` WHERE `orm_author`.`id` = 1.
# . . . . . .
关联查询就是在查询当前实例的同时,把其关联的实例数据也一块取出来。在下图中 orm_blog 通过一个外键和 orm_author 关联。关联大体上可以分为两种:
因此 Django ORM 中的关联查询也分两中 select_related(单关联实例) 和 prefetch_related(多关联实例)
select_related 用来处理单关联实例的情况,适用于 ForeignKey 和 OneToOneField。在查询时,会对关联的表进行 join 操作,取出全部的信息,下面是一个示例:1
2
3
4
5
6
7blog = Blog.objects.select_related().filter(id=3).first()
print blog.id, blog.author.name
# SQL
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`, `orm_author`.`id`, `orm_author`.`name`
# FROM `orm_blog` INNER JOIN `orm_author` ON (`orm_blog`.`author_id` = `orm_author`.`id`)
# WHERE `orm_blog`.`id` = 3 ORDER BY `orm_blog`.`id` ASC LIMIT 1
下面是一个示意图:
select_related 会沿着外键递归查询,例如上图中取表 1 的实例时,会沿着外键将表 3 的数据一块取出来。我们可以传入 depth 参数来指定递归的深度。
如果需要清除 QuerySet 上以前的 select_related 添加的关联字段,可以传入 None 做参数
prefetch_related 主要适用于 OneTwoMany 和 ManyToManyField。和 select_related 类似,prefetch_related 在查询时会同时取出关联实例的值。与 select_related 不同的是 prefetch_related 不使用 JOIN 方式来查询数据库,而是分别查每个表,最后使用 Python 来实现 JOIN 操作。下面是一个示例:1
2
3
4
5
6
7
8
9
10author = Author.objects.prefetch_related('blog_set').filter(name='zjk').first()
for blog in author.blog_set.all():
print blog
# SQL:
# SELECT `orm_author`.`id`, `orm_author`.`name` FROM `orm_author`
# WHERE `orm_author`.`name` = 'zjk' ORDER BY `orm_author`.`id` ASC LIMIT 1
#
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`author_id` IN (1)
下面是一个示意图:
如果查询出关联对象的 QuerySet 之后,再对该 QuerySet 执行查询条件,会使该 QuerySet 失效(也就是需要再次访问数据库)。如果在查询关联对象时需要使用查询条件,可以使用 Prefetch 对象,下面是一个示例:
1 | from django.db.models import Prefetch |
在前面所讲的 filter 和 exclude 方法,对于传入的查询条件都是执行的 AND 操作,如果我们需要对查询条件执行 OR 操作,例如查询 blog 表中 title=‘blog1’ 或者 title=‘blog2’ 的实例,就需要用到 Q 查询。Q 查询支持使用 |、&、~ 操作符,分别对象查询条件的 OR、AND 和 NOT 操作。下面是一个示例:1
2
3
4
5
6
7
8
9from django.db.models import Q
blogs = Blog.objects.filter(Q(id=10) | Q(title='blog2'))
for blog in blogs:
print blog
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE (`orm_blog`.`id` = 10 OR `orm_blog`.`title` = ‘blog2')
F 查询主要用来处理表中字段之间的比较,例如查询 blog 表中 title=conent 的记录。同时 F 查询还支持计算(加减乘除)。下面是一个示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from django.db.models import F
blogs = Blog.objects.filter(title=F('content'))
for blog in blogs:
print blog
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`title` = (`orm_blog`.`content`)
blogs = Blog.objects.filter(title=F('content')+2)
for blog in blogs:
print blog
# SQL:
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`
# FROM `orm_blog` WHERE `orm_blog`.`title` = ((`orm_blog`.`content` + 2))
有些时候我们不需要获取实例中所有的数据,而只需要获得几个字段的数据即可,使用 values 和 values_list 可以指定检索的字段。values 会返回一个 dict 数组,而 values_list 会返回 list 数组。下面是一个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14blogs = Blog.objects.filter(id=5).values('title')
print blogs
# <QuerySet [{u'title': u’blog2'}]>
blogs = Blog.objects.filter(id=5).values_list('title')
print blogs
# <QuerySet [(u’blog2',)]>
blogs = Blog.objects.filter(id=5).values_list('title', flat=True)
print blogs
# <QuerySet [u’blog2']>
通过 aggregate 和 annotate 可以使用 SQL 的聚合函数,例如 SUM、COUNT、MIN 等。aggregate: 针对所有记录调用聚合函数,返回一个 dict 对象,下面是使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from django.db.models import Min
from django.db.models import Sum
result = Blog.objects.aggregate(Min('id'))
print result
# {u'id__min': 3L}
# SELECT MIN(`orm_blog`.`id`) AS `id__min` FROM `orm_blog`
# 自定义属性名
result = Blog.objects.aggregate(total=Sum('id'))
print result
# {'total': Decimal(‘657')}
# SELECT SUM(`orm_blog`.`id`) AS `total` FROM `orm_blog
annotate 先使用 groupby 分组,然后对于每组再调用聚合函数,返回 QuerySet 对象。 annotate 默认按照 id 进行分组,如果需要按其他字段分组,要结合 values /values_list 方法。下面是使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24#认按照 id 进行分组
blogs = Blog.objects.annotate(Count('title'))
for blog in blogs:
print blog.title__count
# SELECT `orm_blog`.`id`, `orm_blog`.`author_id`, `orm_blog`.`title`, `orm_blog`.`content`,
# COUNT(`orm_blog`.`title`) AS `title__count` FROM `orm_blog` GROUP BY `orm_blog`.`id` ORDER BY NULL
# 使用 values 方法,会按照 values 中传入的属性分组
blogs = Blog.objects.values('title').annotate(Count('title'))
for blog in blogs:
print blog['title__count']
# SELECT `orm_blog`.`title`, COUNT(`orm_blog`.`title`) AS `title__count` FROM `orm_blog`
# GROUP BY `orm_blog`.`title` ORDER BY NULL
blogs = Blog.objects.values('title', 'content').annotate(Count('title'))
for blog in blogs:
print blog['title__count']
# SELECT `orm_blog`.`title`, `orm_blog`.`content`, COUNT(`orm_blog`.`title`) AS `title__count`
# FROM `orm_blog` GROUP BY `orm_blog`.`title`, `orm_blog`.`content` ORDER BY NULL
下图是 aggregate 和 annotate 的比较:
如何一些查询比较复杂可以考虑使用 extra 方法。extra 能在 ORM 生成的 SQL 子句中注入 SQL 代码,语法格式如下:1
2# 至少保证一个参数不为空
extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)
select:在 select 子句中插入 SQL 代码
1 | blogs = Blog.objects.extra(select={ |
select_params: 设置 select 参数
1 | blogs = Blog.objects.extra( |
where: 在 where 子句中插入 SQL 代码
1 | blogs = Blog.objects.extra( |
params: 为 where 设置参数
1 | blogs = Blog.objects.extra( |
tables: 在 FROM 子句中插入 table 名称
1 | blogs = Blog.objects.extra( |
order_by:在 order_by 子句中插入排序字段
1 | # - 表示倒序 |
使用 Manager 的 raw 方法可以用于原始的 SQL 查询,并返回 Model 的实例:1
2
3blogs = Blog.objects.raw('select * from orm_blog')
for blog in blogs:
print blog.id , blog.title
如果 SQL 中没有获取某个字段,那么会惰性加载该字段1
2
3
4
5
6
7
8
9
10# 没有取 title,在后面使用时会访问数据库
blogs = Blog.objects.raw('select id from orm_blog')
for blog in blogs:
print blog.id
print blog.title
# select id from orm_blog
# SELECT `orm_blog`.`id`, `orm_blog`.`title` FROM `orm_blog` WHERE `orm_blog`.`id` = 3
# SELECT `orm_blog`.`id`, `orm_blog`.`title` FROM `orm_blog` WHERE `orm_blog`.`id` = 4
#. . . .
如果只需要判断实例是否存在,使用 exists 更高效
1 | blogs = Blog.objects.filter(id=5) |
如果只需要得到实例的数量,使用 count 函数
1 | blogs = Blog.objects.filter(id=5) |
一直没有找到一个合适的展示个人项目的模板,所以自己动手使用 Vue 写了一个。该模板基于 Markdown 文件进行配置,只需要按一定规则编写 Markdown 文件,然后使用一个 在线工具 转为 JSON 文件即可。下面是该项目的在线地址和源码。本文主要记录一下项目中用到的相关知识。
程序最终的效果如下图所示:
整个项目只包含两个组件:项目介绍 和 侧边导航,逻辑比较简单,十分适合入门。
这里我们使用 Gulp 和 Webpack 用作项目构建工具。初次使用 Gulp 和 Webpack 可能不太适应,因为它们的配置可能让你看的一头雾水。不过不用担心,这两个毕竟只是一个工具,在初始时没有必要特别的了解它们的工作原理,只要能运行起来就可以。等到使用了一段时间之后,自然而然的就知道该如何配置了。这里主要记录一下项目中使用的配置,如果想要系统的学习如何使用这两个工具,可以参考下面的文章:
Gulp 和 Webpack 集成一个比较简单的方式就是将 Webpack 作为 Gulp 的一个 task,如下面的形式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var gulp = require("gulp");
var webpack = require("webpack");
gulp.task("webpack", function (callback) {
//webpack配置文件
var config = {
watch: true,
entry: {
index: __dirname + '/src/js/index.js'
},
output: {
path: __dirname + '/dist/js',
filename: '[name].js'
}
//........
};
webpack(config, function (err, stats) {
console.log(stats.toString());
});
});
gulp.task('default', [ 'webpack']);
下面我们分别介绍一下 gulp 和 webpack 的配置
Gulp 中主要配置了两个任务:webpack 和 browserSync,这里主要说一下 browserSync。browserSync 主要用来自动刷新浏览器。首先我们配置需要监听的文件,当这些文件发生改变后,调用 browserSync 使浏览器自动刷新页面。下面是具体的配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23var gulp = require("gulp");
var browserSync = require('browser-sync');
// 添加 browserSync 任务
gulp.task('browserSync', function () {
browserSync({
server: {
baseDir: '.'
},
port: 80
})
});
// 配置需要监听的文件,当这些文件发生变化之后
// 将调用 browserSync.reload 使浏览器自动刷新
gulp.task("watch", function () {
gulp.watch("./**/*.html", browserSync.reload);
gulp.watch("dist/**/*.js", browserSync.reload);
gulp.watch("dist/**/*.css", browserSync.reload);
});
// 添加到默认任务
gulp.task('default', ['browserSync', 'watch', 'webpack']);
我们使用 webpack 进行资源打包的工作,就是说将各种资源(css、js、图片等)交给 Webpack 进行管理,它会将资源整合压缩,我们在页面中只需引用压缩之后的文件即可。webpack 的基础配置文件如下所示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
42gulp.task("webpack", function (callback) {
//webpack配置文件
var config = {
// true 表示 监听文件的变化
watch: true,
// 加载的插件项
plugins: [
new ExtractTextPlugin("../css/[name].css")
],
// 入口文件配置
entry: {
index: __dirname + '/src/js/index.js'
},
// 输出文件配置
output: {
path: __dirname + '/dist/js',
filename: '[name].js'
},
module: {
// 加载器配置,它告诉 Webpack 每一种文件需要采用什么加载器来处理,
// 只有配置好了加载器才能处理相关的文件。
// test 用来测试是什么文件,loader 表示对应的加载器
loaders: [
{test: /\.vue$/, loader: 'vue-loader'}
]
},
resolve: {
// 模块别名定义,方便后续直接引用别名,无须多写长长的地址
// 例如下面的示例,使用时只需要写 import Vue from "vue"
alias: {
vue: path.join(__dirname, "/node_modules/vue/dist/vue.min.js")
},
// 自动扩展文件后缀名,在引入文件时只需写文件名,而不用写后缀
extensions: ['.js', '.json', '.less', '.vue']
}
};
webpack(config, function (err, stats) {
console.log(stats.toString());
});
});
webpack 的相关配置说明可以参考前面的给出的文章,下面说一下使用 webpack 2 遇到的坑:
extract-text-webpack-plugin 会将 css 样式打包成一个独立的 css 文件,而不是直接将样式打包到 js 文件中。下面是使用方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
plugins: [new ExtractTextPlugin("../css/[name].css")],
module: {
loaders: [{
test: /\.css$/,
loader: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
},
{
test: /\.less$/,
loader: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader!less-loader"
})
}
},
}
这里需要注意的地方就是,extract-text-webpack-plugin 在 webpack 1 和 webapck 2 中的安装方式不同,需要根据使用的 webpack 版本来安装:1
2
3
4# for webpack 1
npm install --save-dev extract-text-webpack-plugin
# for webpack 2
npm install --save-dev extract-text-webpack-plugin@beta
使用 UglifyJsPlugin 插件可以压缩 css 和 js 文件,但是一开始时总是无法压缩文件,后来查阅了一下资料,大概是因为下面几个原因:
uglifyjs-webpack-plugin 依赖于 uglify-js,而 uglify-js 默认不支持 ES6 语法,所以需要安装支持 ES6 语法的 uglify-js
1 | npm install mishoo/UglifyJS2#harmony --save |
webpack 2 中,UglifyJsPlugin 默认不压缩 loaders,如果要启动 loaders 压缩,需要加入下面的配置:
1 | plugins: [ |
如果按上面的修改了还是不能压缩文件,可以试着将 node_modules 删除,然后重新安装依赖。
本部分主要记录一下程序中用到的 Vue 语法,如果想要系统的学习一下 Vue.js,可以参考下面的文章:
我们首先来看一个最简单的 Vue 示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue Demo</title>
</head>
<body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
{{ message }}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
</script>
</body>
</html>
每个 Vue 应用都会创建一个 Vue 的根实例,在根实例中需要传入 html 标签的 id,用来告诉 Vue 该标签中的内容需要被 Vue 来解析。上面是一个简单的数据绑定的示例,在运行实 {{ message }} 会被解析为 “Hello Vue!”。
本节参考自 Vue 中文文档,略有修改
在写 Vue 应用之前,我们要熟悉一下 Vue 的基本语法,主要包括数据绑定、事件处理、条件、循环等,下面我们依次看下相关的知识。
Vue.js 使用了基于 HTML 的模版语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。下面是 Vue.js 数据绑定的相关语法:
文本
数据绑定最常见的形式就是使用 “Muestache” 语法(双大括号),如下面的形式:
1 | <span>Message: {{ msg }} </span> |
Muestache 标签会被解析为对应对象上的 msg 属性值。当 msg 属性发生改变之后,Muestache 标签处解析的内容也会随着更新。
通过使用 v-once
指令,我们可以执行一次性解析,即数据改变时,解析的内容不会随着更新。需要注意的是 v-once
会影响该节点上的所有数据绑定
1 | <span v-once>This will never change: {{ msg }}</span> |
Raw HTML
不论属性值是什么内容,Muestache 标签里的内容都会被解析为纯文本。如果希望将绑定的值解析为 HTML 格式,就需要使用 v-html
指令:
1 | <div v-html="variable"></div> |
属性值
Mustache 语法不能用在 HTML 的属性中,如果想为属性绑定变量,需要使用 v-bind
指令:
1 | <div v-bind:id="dynamicId"></div> |
假设 dynamicId=1
,那么上面代码就会被解析为
1 | <div id="1"></div> |
另外 v-bind
指令可以被缩写为 :
,所以我们在程序中经常看到的是下面的语法形式:
1 | <div :id="dynamicId"></div> |
表达式
对于所有的数据绑定, Vue.js 都提供了完全的 JavaScript 表达式支持,如下面的形式:
1 | // 加法 |
通过使用 v-on
指令可以监听 DOM 事件来触发 JS 处理函数,下面是一个完整的示例: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
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue Demo</title>
</head>
<body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<button v-on:click="increase">增加 1</button>
<p>这个按钮被点击了 {{ counter }} 次。</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
counter: 0
},
methods: {
increase: function() {
this.counter++;
}
}
})
</script>
</body>
</html>
通常情况下,v-on
会被简写为 @
,所以我们在程序中一般是看到下面的形式1
2
3<button @click="increase">增加 1</button>
<!-- 等价于 -->
<button v-on:click="increase">增加 1</button>
通过 v-if 指令我们可以根据某些条件来决定是否渲染内容,如下面的形式1
<h1 v-if="ok">Yes</h1>
我们通常将 v-if 和 v-else 结合起来使用,如下所示:1
2
3
4
5
6<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
在 Vue 2.1.0 中新增了一个 v-else-if 指令,可以进行链式判断:1
2
3
4
5
6
7
8
9
10
11
12<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
通过 v-for
指令,我们可以根据一组数据进行迭代渲染,下面是一个基本示例:1
2
3
4
5<ul id="example-1">
<li v-for="item in items">
{{ item.message }}
</li>
</ul>
1 | var example1 = new Vue({ |
上面是一个简单的对数组迭代的示例,我们还可以针对对象进行迭代,如果只使用一个参数,就是针对对象的属性值进行迭代:1
2
3
4
5<ul id="repeat-object" class="demo">
<li v-for="value in object">
{{ value }}
</li>
</ul>
如果传入第二个参数,就是针对对象的属性值以及属性名进行迭代,注意这里二个参数表示的是属性名,也就是 key1
2
3<div v-for="(value, key) in object">
{{ key }} : {{ value }}
</div>
如果再传入第三个参数,第三个参数就表示索引1
2
3<div v-for="(value, key, index) in object">
{{ index }}. {{ key }} : {{ value }}
</div>
组件是 Vue.js 最强大的功能之一。组件可以扩展 HTML元素,封装可重用的代码。在我们的程序中包含两个组件:project 组件和 sidebar 组件,如下图所示。这里我们主要介绍单文件组件的使用,即将组件用到 html、js 和 css 都写在一个文件里,每个组件自成一个系统。
单文件组件一般使用 “.vue” 作为后缀名,一般的文件结构如下所示:
project.vue
1 | <template> |
export 将模块输出,default 表明使用文件名作为模块输出名,这就类似于将模块在系统中注册一下,然后其他模块才可用使用 import 引用该模块。
然后我们需要在主文件中注册该组件:
index.js
1 | import project from '../components/project/project.vue' |
当注册完成之后,就可以 html 中使用该组件了
index.html
1 | <project></project> |
Vue 的要给组件会经历 创建 -> 编译 -> 挂载 -> 卸载 -> 销毁 等一系列事件,这些事件发生的前后都会触发一个相关的钩子(hook)函数,通过这些钩子函数,我们可以在事件发生的前后做一些操作,下面先看下官方给出的一个 Vue 对象的生命周期图,其中红框内标出的就是对应的钩子函数
下面是关于这些钩子函数的解释:
hook | 描述 |
---|---|
beforeCreate | 组件实例刚被创建,组件属性计算之前 |
created | 组件实例创建完成,属性已绑定,但是 DOM 还未生成, $el 属性还不存在 |
beforeMount | 模板编译/挂载之前 |
mounted | 模板编译/挂载之后 |
mounted | 模板编译/挂载之后(不保证组件已在 document 中) |
beforeUpdate | 组件更新之前 |
updated | 组件更新之后 |
activated | for keep-alive ,组件被激活时调用 |
deactivated | for keep-alive ,组件被移除时调用 |
beforeDestory | 组件销毁前调用 |
destoryed | 组件销毁后调用 |
下面是钩子函数的使用方法:1
2
3
4
5
6
7export default {
created: function() {
console.log("component created");
},
data {},
methods: {}
}
父子组件通信可以使用 props down 和 events up 来描述,父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息,下面示意图:
通过使用 props,父组件可以把数据传递给子组件,这种传递是单向的,当父组件的属性发生变化时,会传递给子组件,但是不会反过来。下面是一个示例
comp.vue
1 | <template> |
index.html
1 | <div id="app"> |
在上面的流程中,父组件首先将要传递的数据绑定到子组件的属性上,然后子组件在 props 中声明与绑定属性相同的变量名,就可以使用该变量了,需要注意的一点是如果变量采用驼峰的命名方式,在绑定属性时,就要将驼峰格式改为 -
连接的形式,如果上面所示 shortMsg
-> short-msg
。
如果子组件需要把信息传递给父组件,可以使用自定义事件:
下面是一个示例:
comp.vue
1 | <script> |
index.html
1 | <div id="app"> |
在上面的代码中,父组件通过 v-on
绑定了 child_chagne 事件,当 child_chagne 事件被触发时候就会调用 childChange 方法。在子组件中可以通过 $emit
触发 child_change 事件。这里需要注意的是事件名不用采用驼峰命名,也不要用 -
字符,可以使用下划线 _
连接单词。
Event Bus 通信模式是一种更加通用的通信方式,它既可以用于父子组件也可以用于非父子组件。它的原理就是使用一个空的 Vue 实例作为中央事件总线,通过自定义事件的监听和触发,来完成通信功能,下面是一个示意图:
下面我们来看一个具体的实例:
首先定义一个空的 Vue 实例,作为事件总线
EventBus.js
1 | import Vue from 'vue' |
在组件一中针对某个事件进行监听
comp1.vue
1 | <script> |
在组件二中触发相应事件完成通信
comp2.vue
1 | <script> |
本节摘自 ECMAScript 6 入门
与 ES5 相比,ES6 提供了更加完善的功能和语法,程序中我们使用部分 ES6 语法,这里做一个简单的记录,如果想要系统的学习 ES6,可以参考下面的文章:
ES6 新增了 let 命令,用于声明变量。使用 let 声明的变量具有块级作用域,所以在声明变量时,应该使用 let,而不是 var。1
2
3
4
5
6
7{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for…of循环,作为遍历所有数据结构的统一的方法1
2
3
4
5const arr = ['red', 'green', 'blue'];
for(let v of arr) {
console.log(v); // red green blue
}
ES6 引入了 Set 和 Map 结构。下面是两者的具体介绍
属性
属性 | 描述 |
---|---|
Set.prototype.size | 返回Set实例的成员总数。 |
方法
方法名 | 描述 |
---|---|
add(value) | 添加某个值,返回Set结构本身。 |
delete(value) | 删除某个值,返回一个布尔值,表示删除是否成功。 |
has(value) | 返回一个布尔值,表示该值是否为Set的成员。 |
clear() | 清除所有成员,没有返回值。 |
keys() | 返回键名的遍历器 |
values() | 返回键值的遍历器 |
entries() | 返回键值对的遍历器 |
forEach() | 使用回调函数遍历每个成员 |
使用示例:1
2
3
4
5
6
7const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
属性
属性 | 描述 |
---|---|
Map.prototype.size | 返回 Map 实例的成员总数。 |
方法
方法名 | 描述 |
---|---|
set(key, value) | set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。 |
get(key) | 读取 key 对应的键值,如果找不到 key,返回 undefined。 |
has(key) | 返回一个布尔值,表示某个键是否在当前 Map 对象之中。 |
delete(key) | 删除某个键,返回true。如果删除失败,返回false。 |
clear() | 清除所有成员,没有返回值。 |
keys() | 返回键名的遍历器 |
values() | 返回键值的遍历器 |
entries() | 返回所有成员的遍历器 |
forEach() | 遍历 Map 的所有成员。 |
使用示例:1
2
3
4
5
6
7
8
9const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
线程池是并发中一项常用的优化方法,通过对线程复用,减少线程的创建,降低资源消耗,提高程序响应速度。在 Java 中我们一般通过 Exectuors 提供的工厂方法来创建线程池,但是线程池的最终实现类是 ThreadPoolExecutor,下面我们详细分析一下 ThreadPoolExecutor 的实现。
我们首先看下线程池的基本使用。在下面的代码中我们创建一个固定大小的线程池,该线程池中最多包含 5 个线程,当任务数量超过线程的数量时,就将任务添加到任务队列,等线程空闲之后再从任务队列中获取任务。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
44import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by Jikai Zhang on 2017/4/17.
*/
public class ThreadPoolDemo {
static class WorkThread implements Runnable {
private String command;
public WorkThread(String command) {
this.command = command;
}
public void run() {
System.out.println("Thread-" + Thread.currentThread().getId() + " start. Command=" + command);
processCommand();
System.out.println("Thread-" + Thread.currentThread().getId() + " end.");
}
private void processCommand() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable work = new WorkThread("" + i);
executor.execute(work);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finish all threads.");
}
}
在分析线程池的具体实现之前,我们首先看下线程池具体的工作流程,只有先熟悉了流程,才能更好的理解线程池的实现。线程池一般都会关联一个任务队列,用来缓存任务,当线程执行完一个任务之后,会从任务队列中取下一个任务。ThreadPoolExecutor 中使用阻塞队列作为任务队列,当任务队列为空时,就会阻塞请求任务的线程。下面是 ThreadPoolExecutor 整体的图示:
图片来自 Java 并发编程的艺术
下面我们着重看下 ThreadPoolExecutor 添加任务和关闭线程池的流程。下图是 ThreadPoolExecutor 添加任务的流程:
我们首先看下添加任务的具体流程:
在 ThreadPoolExecutor 中通过两个量来控制线程池的大小:corePoolSize 和 maximumPoolSize。corePoolSize 表示正常状态下线程池中应该持有的存活线程数量,maximumPoolSize 表示线程池可以持有的最大线程数量。当线程池中的线程数量不超过 corePoolSize 时,位于线程池中的线程被看作 core 线程,默认情况下,线程池不对 core 线程进行超时控制,也就是 core 线程会一直存活在线程池中,直到线程池被关闭(这里忽略线程异常关闭的情况)。当线程池中的线程数量超过 corePoolSize 时,额外的线程被看作非 core 线程,线程池会对这部分线程进行超时控制,当线程空闲一段时间之后会销毁该线程。非 core 线程主要用来处理某段时间并发任务特别多的情况,即之前的线程配置无法及时处理那么多的任务量,需要额外的线程来帮助。而当这批任务处理完成之后,额外的线程就有些多余了(线程越多占的资源越多),因此需要及时销毁。
ThreadPoolExecutor 定义线程数量上限是 2^29 - 1 = 536870911
(后面会讲到为什么是这个数),同时用户可以自定义最大线程数量,ThreadPoolExecutor 处理时会选两者之间的较小值。当线程池的线程数量等于 maximumPoolSize 时,说明线程池也已经饱和了,此时对于新来的任务就要执行 reject 策略,JDK 中定义了四种拒绝策略:
下面再看一下线程池的关闭。线程池的关闭分为两种:平缓关闭(shutdown)和立即关闭(shutdownNow)。当调用 shutdown 方法之后,线程池不再接受新的任务,但是仍然会将任务队列中已有的任务执行完毕。而调用 shutdownNow 方法之后,线程池不仅不再接受新的任务,也不会再执行任务队列中剩余的任务,同时会通过中断的方式尝试停止正在执行任务的线程(我们知道对于中断,线程可能响应也可能不响应,所以不能保证一定停止线程)。
下面我们从源码的角度分析一下 ThreadPoolExecutor 的实现。
ThreadPoolExecutor 中每个线程都关联一个 Worker 对象,而 ThreadPool 里实际上保存的就是线程关联的 Worker 对象。 Worker 类对线程进行包装,它除了保存关联线程的信息,还保存一些其他的信息,如线程创建时分配的首任务,线程已完成的任务数量。Worker 实现了 Runnable 接口,创建线程时往 Thread 类传的参数就是该对象,所以线程创建后会执行 Worker 的 run 方法。同时 Worker 类还继承了 AbstractQueuedSynchronizer,使自身成为一个不可重入的互斥锁(以下称为 Worker 锁,注意 Worker 锁是不可重入的,也就是说该锁只能被一个线程获取一次),因此每个线程实际上也关联了一个互斥锁。当线程执行任务时,需要首先获得关联的 Worker 锁,执行完任务之后再释放该锁。Worker 锁的主要作用是为了平缓关闭线程池时,判断线程是否空闲(根据能否获得 Worker 锁),后续会详细讲解。下面是 Worker 类的实现,我们只保留了一些必要的内容:
1 | private final class Worker extends AbstractQueuedSynchronizer implements Runnable { |
我们看到在 Worker 的构造函数中将 state 设为了 -1,注释里给出的解释是:禁止中断直到执行了 runWorker 方法。其实这里包含了两个问题:1.为什么要等到执行了 runWorker 方法 2.怎样禁止中断。对于第一个问题,我们知道中断是针对运行的线程,当线程创建之后只有调用了 start 方法,线程才真正运行,而 start 方法的调用是在 runWorker 方法中的,也就是有只有执行了 runWorker 方法,线程才真正启动。对于第二个问题,这个主要是针对 shutdown 和 shutdownNow 方法的。在 shutdown 方法中,中断线程之前会首先尝试获取线程的 Worker 锁,只有获得了 Worker 锁才对线程进行中断。而获得 Worker 锁的前提是 Worker 的锁的状态变量 state 为 0,当 state 设为 -1 之后,任何线程都无法获得该锁,那么也就无法对线程执行中断操作。而在 shutdownNow 方法中,会调用 Worker 的 interruptIfStarted 方法来中断线程,而 interruptIfStarted 方法只有在 state >= 0 时才会中断线程,所以将 state 设为 -1 可以防止线程被提前中断。当执行 runWorker 方法时,会为传入 Worker 对象执行 unlock 操作(也就是将 state 加 1),使 Worker 对象的 state 变为 0,这样就使线程处于可被中断的状态了。
在 ThreadPoolExecutor 中定义了一个 AtomicInteger 类型的变量 ctl,用来保存线程池的状态和线程数量信息。下面是该变量的定义:1
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl 使用低 29 位保存线程的数量(这就是线程池最大线程数量为 2^29-1
的原因),高 3 位保存线程池的状态。为了提取出这两个信息,ThreadPoolExecutor 定义了一个低 29 位全为 1 的变量 CAPACITY,通过和 CAPACITY 进行 & 运算可以获得线程的数量,通过和 ~CAPACITY 进行 & 运算可以获得线程池的状态,下面是程序中的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 存储线程数量的 bit 位数,这里是 29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 用于提取线程池的运行状态以及线程数量,低 29 位全为 1,高 3 位为0
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 获得线程池的运行状态
private static int runStateOf(int c) {
return c & ~CAPACITY;
}
// 获得线程的数量
private static int workerCountOf(int c) {
return c & CAPACITY;
}
ThreadPoolExecutor 中为线程池定义了五种状态:
下面是 JDK 中关于这 5 个变量的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14// 11100000000000000000000000000000 -536870912
private static final int RUNNING = -1 << COUNT_BITS;
// 00000000000000000000000000000000 0
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 00100000000000000000000000000000 536870912
private static final int STOP = 1 << COUNT_BITS;
// 01000000000000000000000000000000 1073741824
private static final int TIDYING = 2 << COUNT_BITS;
// 01100000000000000000000000000000 1610612736
private static final int TERMINATED = 3 << COUNT_BITS;
下面是各状态之间的转换:
通过 execute 或者 submit 方法都可以向线程池中添加一个任务,submit 会返回一个 Future 对象来获取线程的返回值,下面是 submit 方法的实现:1
2
3
4
5
6public Future <?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture <Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
我们看到 submit 中只是将 Runnable 对象包装了一下,最终还是调用了 execute 方法。下面我们看下 execute 方法的实现: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
42public void execute(Runnable command) {
// command 不能为 null
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 线程数量少于 corePoolSize,会创建一个新的线程执行该任务
if (workerCountOf(c) < corePoolSize) {
// true 表示当前添加的线程为核心线程
if (addWorker(command, true))
return;
c = ctl.get();
}
// 线程数量大于等于 corePoolSize,首先尝试将任务添加到任务队列
// workQueue.offer 会将任务添加到队列尾部
if (isRunning(c) && workQueue.offer(command)) {
// 重新检查状态
int recheck = ctl.get();
// 如果发现当前线程池不是处于 Running 状态,就移除之前的任务
// 移除任务过程有锁保护
if (!isRunning(recheck) && remove(command)) {
reject(command);
} else if (workerCountOf(recheck) == 0) {
// workerCountOf 用来统计当前的工作线程数量,程序执行到这里,有下面两种可能:
// 1. 当前线程池处于 Running 状态,但是工作线程数量为 0,
// 需要创建新的线程
// 2. 移除任务失败,但是工作线程数量为 0,
// 需要创建新的线程来完成移除失败的任务
//
// 因为前面对任务做了判断,所以正常情况下向 addWorker 里传入的任务
// 不可能为 null,这里传入 null 是告诉 addWorker 需要创建新的线程,
// 在 addWorker 里对 null 有专门的处理逻辑
addWorker(null, false);
}
// 下面的 else 说明线程池不是 Running 状态或者任务队列满了,
} else if (!addWorker(command, false)) {
// 这里说明线程池不是 Running 状态或者线程池饱和了
reject(command);
}
}
在前面我们提到了线程池添加任务的流程,这里再重述一下
addWorker 方法会创建并启动线程,当线程池不处于 Running 状态并且传入的任务不为 null,addWorker 就无法成功创建线程。下面看下它的具体实现: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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81private boolean addWorker(Runnable firstTask, boolean core) {
// retry 类似于 goto,continue retry 跳转到 retry 定义,
// 而 break retry 跳出 retry
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 我们在下面详细讲解该条件
if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
// 线程数量大于系统规定的最大线程数或者大于 corePoolSize/maximumPoolSize
// 表明线程池中无法添加新的线程,这里 wc >= CAPACITY 为了防止 corePoolSize
// 或者 maximumPoolSize 大于CAPACITY
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) {
return false;
}
// 使用 CAS 方式将线程数量增加,如果成功就跳出 retry
if (compareAndIncrementWorkerCount(c)) {
break retry;
}
c = ctl.get(); // Re-read ctl
// 如果线程池运行状态发生了改变就从 retry(外层循环)处重新开始,
if (runStateOf(c) != rs)
continue retry;
// 程序执行到这里说 CAS 没有成功,那么就再次执行 CAS
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 创建 work
w = new Worker(firstTask);
final Thread t = w.thread;
// t != null 说明线程创建成功了
if (t != null) {
// 程序用一个 HashSet 存储线程,而 HashSet 不是线程的安全的,
// 所以将线程加入 HashSet 的过程需要加锁。
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// 1. rs < SHUTDOWN 说明程序在运行状态
// 2. rs == SHUTDOWN 说明当前线程处于平缓关闭状态,而 firstTask == null
// 说明当前创建的线程是为了处理任务队列中剩余的任务(故意传入 null)
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
// 线程是存活状态说明线程提前开始了。
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 启动线程
t.start();
workerStarted = true;
}
}
} finally {
if (!workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
这里我们着重看下返回 false 的条件:1
2
3if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
// 等价于
if(rs >= SHUTDOWN && (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty()))
我们依次看下上面的条件:
当创建了线程并成功启动之后,会执行 Worker 的 run 方法,而该方法最终调用了 ThreadPoolExecutor 的 runWorker 方法,并且将自身作为参数传进去了,下面是 runWorker 方法的实现: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
53final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 这里将 Worker 中的 state 设为 0,以便其他线程可以获得锁
// 从而可以中断当前线程
w.unlock(); // allow interrupts
// 用来标记线程是正常退出循环还是异常退出
boolean completedAbruptly = true;
try {
// 如果任务不为空,说明是刚创建线程,如果任务为空,则从队列中取任务
// 如果队列没有任务,线程就会阻塞在这里
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
wt.interrupt();
try {
// 任务执行之前做一些处理,空函数,需要用户定义处理逻辑
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x;
throw x;
} catch (Error x) {
thrown = x;
throw x;
} catch (Throwable x) {
thrown = x;
// 因为 runnable 方法不能抛出 checkedException ,所以这里
// 将异常包装成 Error 抛出
throw new Error(x);
} finally {
// 任务执行完之后做一些处理,默认空函数
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
在上面的代码中,第一个 if 判断的逻辑有点难理解,我们将它拿出分析一下。1
2
3
4
5
6
7private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
&& !wt.isInterrupted())
wt.interrupt();
这段 if 代码块的功能有两个:
上面的 if 代码中括号比较多,我们先将其分为两个大条件:
我们先看第二个条件:!wt.isInterrupted(),该条件说明当前线程没有被中断,只有在线程没有被中断的前提下,才有可能对线程执行中断操作。然后我们将第一个大条件再进行拆分,可以分为下面两个条件:
我们先看第一个条件,该条件说明线程处于 STOP 以及之后的状态,线程应该被中断。如果该条件不成立,说明当前线程不应该被中断,那么会调用 Thread.interrupted() 方法,该方法会首返回线程的中断状态,然后重置线程中断状态(设为 false),如果中断状态本来就为 false,那么就可以就可以跳出 if 代码块了,但是如果中断状态是 true,说明线程被中断过了,此时我们就要判断线程的中断是不是由 shutdownNow 方法(并发调用,该方法会中断线程池的线程,并修改线程池状态为 STOP,后面会讲到)造成的,所以我们需要再检查一下线程的状态,如果发现当前线程池已经变为 STOP 或者之后的状态,说明确实是由 shutdownNow 方法造成的,需要重新对线程进行中断,如果不是那就不需要再中断线程了。
我们看到在 runWorker 里会一直循环调用 getTask 来获取任务,下面来看下 getTask 的实现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/**
* getTask 返回 null,说明当前线程需要被回收了
*/
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// rs >= SHUTDOWN 说明当前线程池至少处于待关闭状态,不再接受新的任务
// 1. rs >= STOP: 说明不需要在再处理任务了(即便有任务)
// 2. workQueue.isEmpty(): 说明任务队列中剩余的任务已经处理完了
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
// timed 用于判断是否需要对线程进行超时控制
// 1. allowCoreThreadTimeOut: 为 true 说明可以对 core 线程进行超时控制
// 2. wc > corePoolSize: 说明线程池中有非 core 线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 1. wc > maximumPoolSize || (timed && timedOut)
// 线程数量大于 maximumPoolSize 值了 或者 允许超时控制并且超时了
// 2. wc > 1 || workQueue.isEmpty()
// 线程中活动线程的数量大于 1 或者 任务队列为空(不需要在留线程执行剩余的任务了)
// 如果上面 1 和 2 都成立,就使用 CAS 将线程数量减 1 并返回 null 回收当前线程
// 如果 CAS 失败了就重试
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 如果允许超时控制,则执行 poll 方法,该方法响应超时,当 keepAliveTime 时间内
// 仍然没有获取到任务,就返回 null。take 方法不响应超时操作,当获取不到任务时会一直等待。
// 另外不管 poll 还是 take 方法都会响应中断,如果没有新的任务添加到队列中
// 会直接抛出 InterruptedException
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
if (r != null)
return r;
// 执行到这里说明超时了
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
当 getTask 返回 null 的时候说明线程需要被回收了,我们总结一下在 getTask 中返回 null 的情况:
我们将 runWorker 和 getTask 结合起来看,整个流程就比较明朗了:
上面两个方法是整个线程池中比较核心的部分,在这两个方法中,完成了任务获取与阻塞线程的工作。下面是线程 提交 -> 处理任务 -> 回收
的流程图:
下面我们再看下 processWorkerExit 方法,该方法主要用来完成线程的回收工作: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
38private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果 completedAbruptly 为 true,说明线程是由于抛出异常而跳出循环的,
// 没有正确执行 getTask 中减少线程数量的逻辑,所以这里要将线程数量减一
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 更新已完成的任务数量,并移除工作线程
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
// 尝试终止线程池
tryTerminate();
int c = ctl.get();
// 如果线程状态是 SHUTDOWN 或者 RUNNING,需要保证线程中的最少线程数量
// 1. 如果线程是由于抛出异常而结束的,直接添加一个线程
// 2. 如果线程是正常结束的
// * 如果允许对 core 线程进行超时控制,并且任务队列中有任务
// 则保证线程数量大于等于 1
// * 如果不允许对 core 进行超时控制,则保证线程数量大于等于 corePoolSize
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && !workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}
我们看到 processWorkerExit 中调用了 tryTerminate 方法,该方法主要用来终止线程池。如果线程池满足终止条件,首先将线程池状态设为 TIDYING,然后执行 terminated 方法,最后将线程池状态设为 TERMINATED。在 shutdown 和 shutdownNow 方法中也会调用该方法 。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
40final void tryTerminate() {
for (;;) {
int c = ctl.get();
// 如果出现下面三种情况,就不执行终止线程池的逻辑,直接返回
// 1. 当前线程池处于 RUNNING 状态,不能停止
// 2. 当前线程池状态为 TIDYING 或者 TERMINATED,不需要停止
// 3. 当前线程池状态为 SHUTDOWN 并且任务队列不为空
if (isRunning(c) || runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && !workQueue.isEmpty()))
return;
// 判断工作线程的数量是否为 0
if (workerCountOf(c) != 0) { // Eligible to terminate
// 如果工作线程数量不为 0,就尝试中断正在线程池中的空闲线程
// ONLY_ONE 说明只尝试中断线程池中第一个线程(不管线程空不空闲)
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 将线程状态设为 TIDYING,如果设置不成功说明线程池的状态发生了变化,需要重试
// 这里线程池状态从 TIDYING 到 TERMINATED 状态转换是原子的
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// 执行 terminated 方法(默认空方法)
terminated();
} finally {
// 将线程状态设为 TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
在 tryTerminate 方法中, 如果满足下面两个条件,就将线程池状态设为 TERMINATED:
如果线程池处于 SHUTDOWN 或者 STOP 状态,但是工作线程不为空,那么 tryTerminate 会尝试去中断线程池中的一个线程,这样做主要是为了防止 shutdown 的中断信号丢失(我们在 shutdown 方法处再详细讨论)。下面看下 interruptIdleWorkers 方法,该方法主要中断 空闲 线程。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w: workers) {
Thread t = w.thread;
// 首先看当前线程是否已经中断,如果没有中断,就看线程是否处于空闲状态
// 如果能获得线程关联的 Worker 锁,说明线程处于空闲状态,可以中断
// 否则说明线程不能中断
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {} finally {
w.unlock();
}
}
// 如果 onlyOne 为 true,只尝试中断第一个线程
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
通过 shutdown 和 shutdownNow 我们可以关闭线程池,关于两者的区别在前面已经提到了,这里不再赘述。我们首先看下 shutdown 方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 检查当前线程是否有关闭线程池的权限
checkShutdownAccess();
// 将线程池状态设为 SHUTDOWN
advanceRunState(SHUTDOWN);
// 中断线程,这里最终调用 interruptIdleWorkers(false);
interruptIdleWorkers();
// hook 方法,默认为空,让用户在线程池关闭时可以做一些操作
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
在前面我们知道 interruptIdleWorkers 会先检查线程是否是空闲状态,如果发现线程不是空闲状态,才会中断线程。而这时中断线程的主要目的是让在任务队列中阻塞的线程醒过来。考虑下面的情况,如果执行 interruptIdleWorkers 时,线程正在运行,所以没有被中断,但是线程执行完任务之后,任务队列恰好为空,线程就会处于阻塞状态,而此时 shutdown 已经执行完 interruptIdleWorkers 操作了(即线程错过了 shutdown 的中断信号),如果没有额外操作,线程会一直处于阻塞状态。所以为了防止这种情况,在 tryTerminate() 中也增加了 interruptIdleWorkers 操作,主要就是为了弥补 shutdown 中丢失的信号。
最后我们再看下 shutdownNow 方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public List < Runnable > shutdownNow() {
List < Runnable > tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 检查线程是否具有关闭线程池的权限
checkShutdownAccess();
// 更改线程状态
advanceRunState(STOP);
// 中断线程
interruptWorkers();
// 清除任务队列,并将任务返回
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
然后我们看下 interruptWorkers 方法:1
2
3
4
5
6
7
8
9
10
11private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 不管线程是否空闲都执行中断
for (Worker w: workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
从上面的代码中我们可以看到在 interruptWorkers 方法中,只要线程开始了,就对线程执行中断,所以 shutdownNow 的中断信号不会丢失。最后我们再看下 drainQueue 方法,该方法主要作用是清空任务队列,并将队列中剩余的任务返回。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private List <Runnable> drainQueue() {
BlockingQueue <Runnable> q = workQueue;
ArrayList <Runnable> taskList = new ArrayList < Runnable > ();
// 该方法会将阻塞队列中的所有项添加到 taskList 中
// 然后清空任务队列,该方法是线程安全的
q.drainTo(taskList);
if (!q.isEmpty()) {
// 将 List 转换为 数组,传入的 Runnable[0] 用来说明是转为 Runnable 数组
for (Runnable r: q.toArray(new Runnable[0])) {
if (q.remove(r))
taskList.add(r);
}
}
return taskList;
}
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
通过这些方法,可以对线程池进行监控,在ThreadPoolExecutor类中提供了几个空方法,如beforeExecute方法,afterExecute方法和terminated方法,可以扩展这些方法在执行前或执行后增加一些新的操作,例如统计线程池的执行任务的时间等,可以继承自ThreadPoolExecutor来进行扩展。
队列同步器 AbstractQueuedSynchronizer(以下简称 AQS),是用来构建锁或者其他同步组件的基础框架。它使用一个 int 成员变量来表示同步状态,通过 CAS 操作对同步状态进行修改,确保状态的改变是安全的。通过内置的 FIFO (First In First Out)队列来完成资源获取线程的排队工作。更多关于 Java 多线程的文章可以转到 这里
在介绍 AQS 的使用之前,需要首先说明一点,AQS 同步和 synchronized 关键字同步(以下简称 synchronized 同步)是采用的两种不同的机制。首先看下 synchronized 同步,synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码需要关联到一个监视对象,当线程执行 monitorenter 指令时,需要首先获得获得监视对象的锁,这里监视对象锁就是进入同步块的凭证,只有获得了凭证才可以进入同步块,当线程离开同步块时,会执行 monitorexit 指令,释放对象锁。
在 AQS 同步中,使用一个 int 类型的变量 state 来表示当前同步块的状态。以独占式同步(一次只能有一个线程进入同步块)为例,state 的有效值有两个 0 和 1,其中 0 表示当前同步块中没有线程,1 表示同步块中已经有线程在执行。当线程要进入同步块时,需要首先判断 state 的值是否为 0,假设为 0,会尝试将 state 修改为 1,只有修改成功了之后,线程才可以进入同步块。注意上面提到的两个条件:
当线程离开同步块时,会修改 state 的值,将其设为 0,并唤醒等待的线程。所以在 AQS 同步中,我们说线程获得了锁,实际上是指线程成功修改了状态变量 state,而线程释放了锁,是指线程将状态变量置为了可修改的状态(在独占式同步中就是置为了 0),让其他线程可以再次尝试修改状态变量。在下面的表述中,我们说线程获得和释放了锁,就是上述含义, 这与 synchronized 同步中说的获得和释放锁的含义不同,需要区别理解。
本节摘自 Java 并发编程的艺术
AQS 的设计是基于模板方法的,使用者需要继承 AQS 并重写指定的方法。在后续的流程中,AQS 提供的模板方法会调用重写的方法。一般来说,我们需要重写的方法主要有下面 5 个:
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int) | 独占式获取锁,实现该方法需要查询当前状态并判断同步状态是否和预期值相同,然后使用 CAS 操作设置同步状态 |
protected boolean tryRelease(int) | 独占式释放锁,实际也是修改同步变量 |
protected int tryAcquireShared(int) | 共享式获取锁,返回大于等于 0 的值,表示获取锁成功,反之获取失败 |
protected boolean tryReleaseShared(int) | 共享式释放锁 |
protected boolean isHeldExclusively() | 判断调用该方法的线程是否持有互斥锁 |
在自定义的同步组件中,我们一般会调用 AQS 提供的模板方法。AQS 提供的模板方法基本上分为 3 类: 独占式获取与释放锁、共享式获取与释放锁以及查询同步队列中的等待线程情况。下面是相关的模板方法:
方法名称 | 描述 |
---|---|
void acquire(int) | 独占式获取锁,如果当前线程成功获取锁,那么方法就返回,否则会将当前线程放入同步队列等待。该方法会调用重写的 tryAcquire(int arg) 方法判断是否可以获得锁 |
void acquireInterruptibly(int) | 和 acquire(int) 相同,但是该方法响应中断,当线程在同步队列中等待时,如果线程被中断,会抛出 InterruptedException 异常并返回。 |
boolean tryAcquireNanos(int, long) | 在 acquireInterruptibly(int) 基础上添加了超时控制,同时支持中断和超时,当在指定时间内没有获得锁时,会返回 false,获取到了返回 true |
void acquireShared(int) | 共享式获得锁,如果成功获得锁就返回,否则将当前线程放入同步队列等待,与独占式获取锁的不同是,同一时刻可以有多个线程获得共享锁,该方法调用 tryAcquireShared(int) |
acquireSharedInterruptibly(int) | 与 acquireShared(int) 相同,该方法响应中断 |
tryAcquireSharedNanos(int, long) | 在 acquireSharedInterruptibly(int) 基础上添加了超时控制 |
boolean release(int) | 独占式释放锁,该方法会在释放锁后,将同步队列中第一个等待节点唤醒 |
boolean releaseShared(int) | 共享式释放锁 |
Collection | 获得同步队列中等待的线程集合 |
自定义组件通过使用同步器提供的模板方法来实现自己的同步语义。下面我们通过两个示例,看下如何借助于 AQS 来实现锁的同步语义。我们首先实现一个独占锁(排它锁),独占锁就是说在某个时刻内,只能有一个线程持有独占锁,只有持有锁的线程释放了独占锁,其他线程才可以获取独占锁。下面是具体实现: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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/*
* Created by Jikai Zhang on 2017/4/6.
* <p>
* 自定义独占锁
*/
public class Mutex implements Lock {
// 通过继承 AQS,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 当前线程是否被独占
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获得锁
protected boolean tryAcquire(int arg) {
// 只有当 state 的值为 0,并且线程成功将 state 值修改为 1 之后,线程才可以获得独占锁
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
// state 为 0 说明当前同步块中没有锁了,无需释放
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
// 将独占的线程设为 null
setExclusiveOwnerThread(null);
// 将状态变量的值设为 0,以便其他线程可以成功修改状态变量从而获得锁
setState(0);
return true;
}
Condition newCondition() {
return new ConditionObject();
}
}
// 将操作代理到 Sync 上
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public static void withoutMutex() throws InterruptedException {
System.out.println("Without mutex: ");
int threadCount = 2;
final Thread threads[] = new Thread[threadCount];
for (int i = 0; i < threads.length; i++) {
final int index = i;
threads[i] = new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 100000; j++) {
if (j % 20000 == 0) {
System.out.println("Thread-" + index + ": j =" + j);
}
}
}
});
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
}
public static void withMutex() {
System.out.println("With mutex: ");
final Mutex mutex = new Mutex();
int threadCount = 2;
final Thread threads[] = new Thread[threadCount];
for (int i = 0; i < threads.length; i++) {
final int index = i;
threads[i] = new Thread(new Runnable() {
public void run() {
mutex.lock();
try {
for (int j = 0; j < 100000; j++) {
if (j % 20000 == 0) {
System.out.println("Thread-" + index + ": j =" + j);
}
}
} finally {
mutex.unlock();
}
}
});
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}
public static void main(String[] args) throws InterruptedException {
withoutMutex();
System.out.println();
withMutex();
}
}
程序的运行结果如下面所示。我们看到使用了 Mutex 之后,线程 0 和线程 1 不会再交替执行,而是当一个线程执行完,另外一个线程再执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23Without mutex:
Thread-0: j =0
Thread-1: j =0
Thread-0: j =20000
Thread-1: j =20000
Thread-0: j =40000
Thread-1: j =40000
Thread-0: j =60000
Thread-1: j =60000
Thread-1: j =80000
Thread-0: j =80000
With mutex:
Thread-0: j =0
Thread-0: j =20000
Thread-0: j =40000
Thread-0: j =60000
Thread-0: j =80000
Thread-1: j =0
Thread-1: j =20000
Thread-1: j =40000
Thread-1: j =60000
Thread-1: j =80000
下面在看一个共享锁的示例。在该示例中,我们定义两个共享资源,即同一时间内允许两个线程同时执行。我们将同步变量的初始状态 state 设为 2,当一个线程获取了共享锁之后,将 state 减 1,线程释放了共享锁后,将 state 加 1。状态的合法范围是 0、1 和 2,其中 0 表示已经资源已经用光了,此时线程再要获得共享锁就需要进入同步序列等待。下面是具体实现: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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* Created by Jikai Zhang on 2017/4/9.
* <p>
* 自定义共享锁
*/
public class TwinsLock implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
public Sync(int resourceCount) {
if (resourceCount <= 0) {
throw new IllegalArgumentException("resourceCount must be larger than zero.");
}
// 设置可以共享的资源总数
setState(resourceCount);
}
protected int tryAcquireShared(int reduceCount) {
// 使用尝试获得资源,如果成功修改了状态变量(获得了资源)
// 或者资源的总量小于 0(没有资源了),则返回。
for (; ; ) {
int lastCount = getState();
int newCount = lastCount - reduceCount;
if (newCount < 0 || compareAndSetState(lastCount, newCount)) {
return newCount;
}
}
}
protected boolean tryReleaseShared(int returnCount) {
// 释放共享资源,因为可能有多个线程同时执行,所以需要使用 CAS 操作来修改资源总数。
for (; ; ) {
int lastCount = getState();
int newCount = lastCount + returnCount;
if (compareAndSetState(lastCount, newCount)) {
return true;
}
}
}
}
// 定义两个共享资源,说明同一时间内可以有两个线程同时运行
private final Sync sync = new Sync(2);
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.tryAcquireShared(1) >= 0;
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
public static void main(String[] args) {
final Lock lock = new TwinsLock();
int threadCounts = 10;
Thread threads[] = new Thread[threadCounts];
for (int i = 0; i < threadCounts; i++) {
final int index = i;
threads[i] = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
for (int i = 0; i < threadCounts; i++) {
threads[i].start();
}
}
}
运行程序,我们会发现程序每次都会同时打印两条语句,如下面的形式,证明同时有两个线程在执行。1
2
3
4
5
6
7
8Thread-0
Thread-1
Thread-3
Thread-2
Thread-8
Thread-4
Thread-3
Thread-6
CAS(Compare and Swap),比较并交换,通过利用底层硬件平台的特性,实现原子性操作。CAS 操作涉及到3个操作数,内存值 V,旧的期望值 A,需要修改的新值 B。当且仅当预期值 A 和 内存值 V 相同时,才将内存值 V 修改为 B,否则什么都不做。CAS 操作类似于执行了下面流程1
2
3if(oldValue == memory[valueAddress]) {
memory[valueAddress] = newValue;
}
在上面的流程中,其实涉及到了两个操作,比较以及替换,为了确保程序正确,需要确保这两个操作的原子性(也就是说确保这两个操作同时进行,中间不会有其他线程干扰)。现在的 CPU 中,提供了相关的底层 CAS 指令,即 CPU 底层指令确保了比较和交换两个操作作为一个原子操作进行(其实在这一点上还是有排他锁的. 只是比起用synchronized, 这里的排他时间要短的多.),Java 中的 CAS 函数是借助于底层的 CAS 指令来实现的。更多关于 CPU 底层实现的原理可以参考 这篇文章。我们来看下 Java 中对于 CAS 函数的定义:
1 | /** |
上面三个函数定义在 sun.misc.Unsafe 类中,使用该类可以进行一些底层的操作,例如直接操作原生内存,更多关于 Unsafe 类的文章可以参考 这篇。以 compareAndSwapInt 为例,我们看下如何使用 CAS 函数: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
45import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* Created by Jikai Zhang on 2017/4/8.
*/
public class CASIntTest {
private volatile int count = 0;
private static final Unsafe unsafe = getUnsafe();
private static final long offset;
// 获得 count 属性在 CASIntTest 中的偏移量(内存地址偏移)
static {
try {
offset = unsafe.objectFieldOffset(CASIntTest.class.getDeclaredField("count"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
// 通过反射的方式获得 Unsafe 类
public static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return unsafe;
}
public void increment() {
int previous = count;
unsafe.compareAndSwapInt(this, offset, previous, previous + 1);
}
public static void main(String[] args) {
CASIntTest casIntTest = new CASIntTest();
casIntTest.increment();
System.out.println(casIntTest.count);
}
}
在 CASIntTest 类中,我们定义一个 count 变量,其中 increment 方法是将 count 的值加 1。下面是 increase 方法的代码:1
2int previous = count;
unsafe.compareAndSwapInt(this, offset, previous, previous + 1);
在没有线程竞争的条件下,该代码执行的结果是将 count 变量的值加 1(多个线程竞争可能会有线程执行失败),但是在 compareAndSwapInt 函数中,我们并没有传入 count 变量,那么函数是如何修改的 count 变量值?其实我们往 compareAndSwapInt 函数中传入了 count 变量在堆内存中的地址,函数直接修改了 count 变量所在内存区域。count 属性在堆内存中的地址是由 CASIntTest 实例的起始内存地址和 count 属性相对于起始内存的偏移量决定的。其中对象属性在对象中的偏移量通过 objectFieldOffset
函数获得,函数原型如下所示。该函数接受一个 Filed 类型的参数,返回该 Filed 属性在对象中的偏移量。
1 | /** |
下面我们再看一下 compareAndSwapInt 的函数原型。我们知道 CAS 操作需要知道 3 个信息:内存中的值,期望的旧值以及要修改的新值。通过前面的分析,我们知道通过 o 和 offset 我们可以确定属性在内存中的地址,也就是知道了属性在内存中的值。expected 对应期望的旧址,而 x 就是要修改的新值。
1 | public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); |
compareAndSwapInt 函数首先比较一下 expected 是否和内存中的值相同,如果不同证明其他线程修改了属性值,那么就不会执行更新操作,但是程序如果就此返回了,似乎不太符合我们的期望,我们是希望程序可以执行更新操作的,如果其他线程先进行了更新,那么就在更新后的值的基础上进行修改,所以我们一般使用循环配合 CAS 函数,使程序在更新操作完成之后再返回,如下所示:1
2
3
4long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
下面是使用 CAS 函数实现计数器的一个实例: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
65
66
67
68
69
70
71
72import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* Created by Jikai Zhang on 2017/4/8.
*/
public class CASCounter {
// 通过反射的方式获得 Unsafe 类
public static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return unsafe;
}
private volatile long counter = 0;
private static final long offset;
private static final Unsafe unsafe = getUnsafe();
static {
try {
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}
public long getCounter() {
return counter;
}
private static long intCounter = 0;
public static void main(String[] args) throws InterruptedException {
int threadCount = 10;
Thread threads[] = new Thread[threadCount];
final CASCounter casCounter = new CASCounter();
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10000; i++) {
casCounter.increment();
intCounter++;
}
}
});
threads[i].start();
}
for(int i = 0; i < threadCount; i++) {
threads[i].join();
}
System.out.printf("CASCounter is %d \nintCounter is %d\n", casCounter.getCounter(), intCounter);
}
}
在 AQS 中,对原始的 CAS 函数封装了一下,省去了获得变量地址的步骤,如下面的形式:1
2
3
4
5
6
7
8
9
10
11
12
13private static final long waitStatusOffset;
static {
try {
waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
} catch (Exception ex) {
throw new Error(ex);
}
}
private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
}
AQS 依赖内部的同步队列(一个 FIFO的双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把队列中第一个等待节点线程唤醒(下图中的 Node1),使其再次尝试获取同步状态。同步队列的结构如下所示:
图片来自 http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer
Head 节点本身不保存等待线程的信息,它通过 next 变量指向第一个保存线程等待信息的节点(Node1)。当线程被唤醒之后,会删除 Head 节点,而唤醒线程所在的节点会设置为 Head 节点(Node1 被唤醒之后,Node1会被置为 Head 节点)。下面我们看下 JDK 中同步队列的实现。
首先看在节点所对应的 Node 类: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
65
66static final class Node {
/**
* 标志是独占式模式还是共享模式
*/
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/**
* 线程等待状态的有效值
*/
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/**
* 线程状态,合法值为上面 4 个值中的一个
*/
volatile int waitStatus;
/**
* 当前节点的前置节点
*/
volatile Node prev;
/**
* 当前节点的后置节点
*/
volatile Node next;
/**
* 当前节点所关联的线程
*/
volatile Thread thread;
/**
* 指向下一个在某个条件上等待的节点,或者指向 SHARE 节点,表明当前处于共享模式
*/
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
在 Node 类中定义了四种等待状态:
我们首先看下独占锁的获取和释放过程
独占锁获取
下面是获取独占锁的流程图:
我们通过 acquire 方法来获取独占锁,下面是方法定义1
2
3
4
5
6
7
8
9public final void acquire(int arg) {
// 首先尝试获取锁,如果获取失败,会先调用 addWaiter 方法创建节点并追加到队列尾部
// 然后调用 acquireQueued 阻塞或者循环尝试获取锁
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
// 在 acquireQueued 中,如果线程是因为中断而退出的阻塞状态会返回 true
// 这里的 selfInterrupt 主要是为了恢复线程的中断状态
selfInterrupt();
}
}
acquire 会首先调用 tryAcquire 方法来获得锁,该方法需要我们来实现,这个在前面已经提过了。如果没有获取锁,会调用 addWaiter 方法创建一个和当前线程关联的节点追加到同步队列的尾部,我们调用 addWaiter 时传入的是 Node.EXCLUSIVE,表明当前是独占模式。下面是 addWaiter 的具体实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// tail 指向同步队列的尾节点
Node pred = tail;
// Try the fast path of enq; backup to full enq on failure
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWaiter 方法会首先调用 if 方法,来判断能否成功将节点添加到队列尾部,如果添加失败,再调用 enq 方法(使用循环不断重试)进行添加,下面是 enq 方法的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 同步队列采用的懒初始化(lazily initialized)的方式,
// 初始时 head 和 tail 都会被设置为 null,当一次被访问时
// 才会创建 head 对象,并把尾指针指向 head。
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter 仅仅是将节点加到了同步队列的末尾,并没有阻塞线程,线程阻塞的操作是在 acquireQueued 方法中完成的,下面是 acquireQueued 的实现: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
28final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果当前节点的前继节点是 head,就使用自旋(循环)的方式不断请求锁
if (p == head && tryAcquire(arg)) {
// 成功获得锁,将当前节点置为 head 节点,同时删除原 head 节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire 检查是否可以挂起线程,
// 如果可以挂起进程,会调用 parkAndCheckInterrupt 挂起线程,
// 如果 parkAndCheckInterrupt 返回 true,表明当前线程是因为中断而退出挂起状态的,
// 所以要将 interrupted 设为 true,表明当前线程被中断过
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued 会首先检查当前节点的前继节点是否为 head,如果为 head,将使用自旋的方式不断的请求锁,如果不是 head,则调用 shouldParkAfterFailedAcquire 查看是否应该挂起当前节点关联的线程,下面是 shouldParkAfterFailedAcquire 的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 当前节点的前继节点的等待状态
int ws = pred.waitStatus;
// 如果前继节点的等待状态为 SIGNAL 我们就可以将当前节点对应的线程挂起
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// ws 大于 0,表明当前线程的前继节点处于 CANCELED 的状态,
// 所以我们需要从当前节点开始往前查找,直到找到第一个不为
// CAECELED 状态的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire 会检查前继节点的等待状态,如果前继节点状态为 SIGNAL,则可以将当前节点关联的线程挂起,如果不是 SIGNAL,会做一些其他的操作,在当前循环中不会挂起线程。如果确定了可以挂起线程,就调用 parkAndCheckInterrupt 方法对线程进行阻塞:1
2
3
4
5
6
7
8
9
10private final boolean parkAndCheckInterrupt() {
// 挂起当前线程
LockSupport.park(this);
// 可以通过调用 interrupt 方法使线程退出 park 状态,
// 为了使线程在后面的循环中还可以响应中断,会重置线程的中断状态。
// 这里使用 interrupted 会先返回线程当前的中断状态,然后将中断状态重置为 false,
// 线程的中断状态会返回给上层调用函数,在线程获得锁后,
// 如果发现线程曾被中断过,会将中断状态重新设为 true
return Thread.interrupted();
}
独占锁释放
下面是释放独占锁的流程:
通过 release 方法,我们可以释放互斥锁。下面是 release 方法的实现:1
2
3
4
5
6
7
8
9
10public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// waitStatus 为 0,证明是初始化的空队列或者后继结点已经被唤醒了
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
在独占模式下释放锁时,是没有其他线程竞争的,所以处理会简单一些。首先尝试释放锁,如果失败就直接返回(失败不是因为多线程竞争,而是线程本身就不拥有锁)。如果成功的话,会检查 h 的状态,然后调用 unparkSuccessor 方法来唤醒后续线程。下面是 unparkSuccessor 的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 将 head 节点的状态置为 0,表明当前节点的后续节点已经被唤醒了,
// 不需要再次唤醒,修改 ws 状态主要作用于 release 的判断
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
在 unparkSuccessor 方法中,如果发现头节点的后继结点为 null 或者处于 CANCELED 状态,会从尾部往前找(在节点存在的前提下,这样一定能找到)离头节点最近的需要唤醒的节点,然后唤醒该节点。
独占锁的流程和原理比较容易理解,因为只有一个锁,但是共享锁的处理就相对复杂一些了。在独占锁中,只有在释放锁之后,才能唤醒等待的线程,而在共享模式中,获取锁和释放锁之后,都有可能唤醒等待的线程。如果想要理清共享锁的工作过程,必须将共享锁的获取和释放结合起来看。这里我们先看一下共享锁的释放过程,只有明白了释放过程做了哪些工作,才能更好的理解获取锁的过程。
共享锁释放
下面是释放共享锁的流程:
通过 releaseShared 方法会释放共享锁,下面是具体的实现:1
2
3
4
5
6
7public final boolean releaseShared(int releases) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
releases 是要释放的共享资源数量,其中 tryReleaseShared 的方法由我们自己重写,该方法的主要功能就是修改共享资源的数量(state + releases),因为可能会有多个线程同时释放资源,所以实现的时候,一般采用循环加 CAS 操作的方式,如下面的形式:1
2
3
4
5
6
7
8
9
10protected boolean tryReleaseShared(int releases) {
// 释放共享资源,因为可能有多个线程同时执行,所以需要使用 CAS 操作来修改资源总数。
for (;;) {
int lastCount = getState();
int newCount = lastCount + releases;
if (compareAndSetState(lastCount, newCount)) {
return true;
}
}
}
当共享资源数量修改了之后,会调用 doReleaseShared 方法,该方法主要唤醒同步队列中的第一个等待节点(head.next),下面是具体实现: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
43private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
// head = null 说明没有初始化,head = tail 说明同步队列中没有等待节点
if (h != null && h != tail) {
// 查看当前节点的等待状态
int ws = h.waitStatus;
// 我们在前面说过,SIGNAL说明有后续节点需要唤醒
if (ws == Node.SIGNAL) {
/*
* 将当前节点的值设为 0,表明已经唤醒了后继节点
* 可能会有多个线程同时执行到这一步,所以使用 CAS 保证只有一个线程能修改成功,
* 从而执行 unparkSuccessor,其他的线程会执行 continue 操作
*/
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
/*
* ws 等于 0,说明无需唤醒后继结点(后续节点已经被唤醒或者当前节点没有被阻塞的后继结点),
* 也就是这一次的调用其实并没有执行唤醒后继结点的操作。就类似于我只需要一张优惠券,
* 但是我的两个朋友,他们分别给我了一张,因此我就剩余了一张。然后我就将这张剩余的优惠券
* 送(传播)给其他人使用,因此这里将节点置为可传播的状态(PROPAGATE)
*/
continue; // loop on failed CAS
}
}
if (h == head) // loop if head changed
break;
}
}
从上面的实现中,doReleaseShared 的主要作用是用来唤醒阻塞的节点并且一次只唤醒一个,让该节点关联的线程去重新竞争锁,它既不修改同步队列,也不修改共享资源。
当多个线程同时释放资源时,可以确保两件事:
所以释放锁做的主要工作还是修改共享资源的数量。而有了多个共享资源后,如何确保同步队列中的多个节点可以获取锁,是由获取锁的逻辑完成的。下面看下共享锁的获取。
共享锁的获取
下面是获取共享锁的流程
通过 acquireShared 方法,我们可以申请共享锁,下面是具体的实现:1
2
3
4
5public final void acquireShared(int arg) {
// 如果返回结果小于 0,证明没有获取到共享资源
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
如果没有获取到共享资源,就会执行 doAcquireShared 方法,下面是该方法的具体实现: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
27private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
从上面的代码中可以看到,只有前置节点为 head 的节点才有可能去竞争锁,这点和独占模式的处理是一样的,所以即便唤醒了多个线程,也只有一个线程能进入竞争锁的逻辑,其余线程会再次进入 park 状态,当线程获取到共享锁之后,会执行 setHeadAndPropagate 方法,下面是具体的实现: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
40private void setHeadAndPropagate(Node node, long propagate) {
// 备份一下头节点
Node h = head; // Record old head for check below
/*
* 移除头节点,并将当前节点置为头节点
* 当执行完这一步之后,其实队列的头节点已经发生改变,
* 其他被唤醒的线程就有机会去获取锁,从而并发的执行该方法,
* 所以上面备份头节点,以便下面的代码可以正确运行
*/
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
/*
* 判断是否需要唤醒后继结点,propagate > 0 说明共享资源有剩余,
* h.waitStatus < 0,表明当前节点状态可能为 SIGNAL,CONDITION,PROPAGATE
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 只有 s 不处于独占模式时,才去唤醒后继结点
if (s == null || s.isShared())
doReleaseShared();
}
}
判断后继结点是否需要唤醒的条件是十分宽松的,也就是一定包含必要的唤醒,但是也有可能会包含不必要的唤醒。从前面我们可以知道 doReleaseShared 函数的主要作用是唤醒后继结点,它既不修改共享资源,也不修改同步队列,所以即便有不必要的唤醒也是不影响程序正确性的。如果没有共享资源,节点会再次进入等待状态。
到了这里,脉络就比较清晰了,当一个节点获取到共享锁之后,它除了将自身设为 head 节点之外,还会判断一下是否满足唤醒后继结点的条件,如果满足,就唤醒后继结点,后继结点获取到锁之后,会重复这个过程,直到判断条件不成立。就类似于考试时从第一排往最后一排传卷子,第一排先留下一份,然后将剩余的传给后一排,后一排会重复这个过程。如果传到某一排卷子没了,那么位于这排的人就要等待,直到老师又给了他新的卷子。
在获取锁时还可以设置响应中断,独占锁和共享锁的处理逻辑类似,这里我们以独占锁为例。使用 acquireInterruptibly 方法,在获取独占锁时可以响应中断,下面是具体的实现: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
29public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
// 这里会抛出异常
throw new InterruptedException();
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
从上面的代码中我们可以看出,acquireInterruptibly 和 acquire 的逻辑类似,只是在下面的代码处有所不同:当线程因为中断而退出阻塞状态时,会直接抛出 InterruptedException 异常。1
2
3
4if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
// 这里会抛出异常
throw new InterruptedException();
}
我们知道,不管是抛出异常还是方法返回,程序都会执行 finally 代码,而 failed 肯定为 true,所以抛出异常之后会执行 cancelAcquire 方法,cancelAcquire 方法主要将节点从同步队列中移除。下面是具体的实现: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
43private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// 跳过前面的已经取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 保存下 pred 的后继结点,以便 CAS 操作使用
// 因为可能存在已经取消的节点,所以 pred.next 不一等于 node
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
// 将节点状态设为 CANCELED
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
从上面的代码可以看出,节点的删除分为三种情况:
1 | Node pred = node.prev; |
超时是在中断的基础上加了一层时间的判断,这里我们还是以独占锁为例。 tryAcquireNanos 支持获取锁的超时处理,下面是具体实现:1
2
3
4
5public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
当获取锁失败之后,会执行 doAcquireNanos 方法,下面是具体实现: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
35private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0 L)
return false;
// 线程最晚结束时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 判断是否超时,如果超时就返回
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0 L)
return false;
// 这里如果设定了一个阈值,如果超时的时间比阈值小,就认为
// 当前线程没必要阻塞,再执行几次 for 循环估计就超时了
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
当线程超时返回时,还是会执行 cancelAcquire 方法,cancelAcquire 的逻辑已经在前面说过了,这里不再赘述。
ThreadLocal 主要用来提供线程局部变量,也就是变量只对当前线程可见,本文主要记录一下对于 ThreadLocal 的理解。更多关于 Java 多线程的文章可以转到 这里。
在多线程环境下,之所以会有并发问题,就是因为不同的线程会同时访问同一个共享变量,例如下面的形式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
46public class MultiThreadDemo {
public static class Number {
private int value = 0;
public void increase() throws InterruptedException {
value = 10;
Thread.sleep(10);
System.out.println("increase value: " + value);
}
public void decrease() throws InterruptedException {
value = -10;
Thread.sleep(10);
System.out.println("decrease value: " + value);
}
}
public static void main(String[] args) throws InterruptedException {
final Number number = new Number();
Thread increaseThread = new Thread(new Runnable() {
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread decreaseThread = new Thread(new Runnable() {
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
increaseThread.start();
decreaseThread.start();
}
}
在上面的代码中,increase 线程和 decrease 线程会操作同一个 number 中 value,那么输出的结果是不可预测的,因为当前线程修改变量之后但是还没输出的时候,变量有可能被另外一个线程修改,下面是一种可能的情况:1
2increase value: 10
decrease value: 10
一种解决方法是在 increase()
和 decrease()
方法上加上 synchronized 关键字进行同步,这种做法其实是将 value 的 赋值 和 打印 包装成了一个原子操作,也就是说两者要么同时进行,要不都不进行,中间不会有额外的操作。我们换个角度考虑问题,如果 value 只属于 increase 线程或者 decrease 线程,而不是被两个线程共享,那么也不会出现竞争问题。一种比较常见的形式就是局部(local)变量(这里排除局部变量引用指向共享对象的情况),如下所示:1
2
3
4
5public void increase() throws InterruptedException {
int value = 10;
Thread.sleep(10);
System.out.println("increase value: " + value);
}
不论 value 值如何改变,都不会影响到其他线程,因为在每次调用 increase 方法时,都会创建一个 value 变量,该变量只对当前调用 increase 方法的线程可见。借助于这种思想,我们可以对每个线程创建一个共享变量的副本,该副本只对当前线程可见(可以认为是线程私有的变量),那么修改该副本变量时就不会影响到其他的线程。一个简单的思路是使用 Map 存储每个变量的副本,将当前线程的 id 作为 key,副本变量作为 value 值,下面是一个实现: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
65
66
67
68
69public class SimpleImpl {
public static class CustomThreadLocal {
private Map<Long, Integer> cacheMap = new HashMap<>();
private int defaultValue ;
public CustomThreadLocal(int value) {
defaultValue = value;
}
public Integer get() {
long id = Thread.currentThread().getId();
if (cacheMap.containsKey(id)) {
return cacheMap.get(id);
}
return defaultValue;
}
public void set(int value) {
long id = Thread.currentThread().getId();
cacheMap.put(id, value);
}
}
public static class Number {
private CustomThreadLocal value = new CustomThreadLocal(0);
public void increase() throws InterruptedException {
value.set(10);
Thread.sleep(10);
System.out.println("increase value: " + value.get());
}
public void decrease() throws InterruptedException {
value.set(-10);
Thread.sleep(10);
System.out.println("decrease value: " + value.get());
}
}
public static void main(String[] args) throws InterruptedException {
final Number number = new Number();
Thread increaseThread = new Thread(new Runnable() {
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread decreaseThread = new Thread(new Runnable() {
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
increaseThread.start();
decreaseThread.start();
}
}
但是上面的实现会存在下面的问题:
number
变量存在,线程的副本变量依然会存在(存放在 number 的 cacheMap 中)。但是作为特定线程的副本变量,该变量的生命周期应该由线程决定,线程消亡之后,该变量也应该被回收。为了解决上面的问题,我们换种思路,每个线程创建一个 Map,存放当前线程中副本变量,用 CustomThreadLocal 的实例作为 key 值,下面是一个示例: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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79public class SimpleImpl2 {
public static class CommonThread extends Thread {
Map<Integer, Integer> cacheMap = new HashMap<>();
}
public static class CustomThreadLocal {
private int defaultValue;
public CustomThreadLocal(int value) {
defaultValue = value;
}
public Integer get() {
Integer id = this.hashCode();
Map<Integer, Integer> cacheMap = getMap();
if (cacheMap.containsKey(id)) {
return cacheMap.get(id);
}
return defaultValue;
}
public void set(int value) {
Integer id = this.hashCode();
Map<Integer, Integer> cacheMap = getMap();
cacheMap.put(id, value);
}
public Map<Integer, Integer> getMap() {
CommonThread thread = (CommonThread) Thread.currentThread();
return thread.cacheMap;
}
}
public static class Number {
private CustomThreadLocal value = new CustomThreadLocal(0);
public void increase() throws InterruptedException {
value.set(10);
Thread.sleep(10);
System.out.println("increase value: " + value.get());
}
public void decrease() throws InterruptedException {
value.set(-10);
Thread.sleep(10);
System.out.println("decrease value: " + value.get());
}
}
public static void main(String[] args) throws InterruptedException {
final Number number = new Number();
Thread increaseThread = new CommonThread() {
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread decreaseThread = new CommonThread() {
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
increaseThread.start();
decreaseThread.start();
}
}
在上面的实现中,当线程消亡之后,线程中 cacheMap 也会被回收,它当中存放的副本变量也会被全部回收,并且 cacheMap 是线程私有的,不会出现多个线程同时访问一个 cacheMap 的情况。在 Java 中,ThreadLocal 类的实现就是采用的这种思想,注意只是思想,实际的实现和上面的并不一样。
Java 使用 ThreadLocal 类来实现线程局部变量模式,ThreadLocal 使用 set 和 get 方法设置和获取变量,下面是函数原型:1
2public void set(T value);
public T get();
下面是使用 ThreadLocal 的一个完整示例: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
27public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static int value = 0;
public static class ThreadLocalThread implements Runnable {
public void run() {
threadLocal.set((int)(Math.random() * 100));
value = (int) (Math.random() * 100);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf(Thread.currentThread().getName() + ": threadLocal=%d, value=%d\n", threadLocal.get(), value);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadLocalThread());
Thread thread2 = new Thread(new ThreadLocalThread());
thread.start();
thread2.start();
thread.join();
thread2.join();
}
}
下面是一种可能的输出:1
2Thread-0: threadLocal=87, value=15
Thread-1: threadLocal=69, value=15
我们看到虽然 threadLocal
是静态变量,但是每个线程都有自己的值,不会受到其他线程的影响。
ThreadLocal 的实现思想,我们在前面已经说了,每个线程维护一个 ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 实例本身,value 是要存储的副本变量。ThreadLocal 实例本身并不存储值,它只是提供一个在当前线程中找到副本值的 key。 如下图所示:
图片来自 http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
我们从下面三个方面看下 ThreadLocal 的实现:
线程使用 ThreadLocalMap 来存储每个线程副本变量,它是 ThreadLocal 里的一个静态内部类。ThreadLocalMap 也是采用的散列表(Hash)思想来实现的,但是实现方式和 HashMap 不太一样。我们首先看下散列表的相关知识:
理想状态下,散列表就是一个包含关键字的固定大小的数组,通过使用散列函数,将关键字映射到数组的不同位置。下面是理想散列表的一个示意图:
图片来自 数据结构与算法分析: C语法描述
在理想状态下,哈希函数可以将关键字均匀的分散到数组的不同位置,不会出现两个关键字散列值相同(假设关键字数量小于数组的大小)的情况。但是在实际使用中,经常会出现多个关键字散列值相同的情况(被映射到数组的同一个位置),我们将这种情况称为散列冲突。为了解决散列冲突,主要采用下面两种方式:
分离链表法
分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素。下面是一个示意图:
图片来自 http://faculty.cs.niu.edu/~freedman/340/340notes/340hash.htm
开放定址法
开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 – 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。如下图所示:
关于两种方式的比较,可以参考 这篇文章。ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。
我们知道 Map 是一种 key-value 形式的数据结构,所以在散列数组中存储的元素也是 key-value 的形式。ThreadLocalMap 使用 Entry 类来存储数据,下面是该类的定义:1
2
3
4
5
6
7
8
9static class Entry extends WeakReference <ThreadLocal <?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal <?> k, Object v) {
super(k);
value = v;
}
}
Entry 将 ThreadLocal 实例作为 key,副本变量作为 value 存储起来。注意 Entry 中对于 ThreadLocal 实例的引用是一个弱引用,该引用定义在 Reference 类(WeakReference的父类)中,下面是 super(k)
最终调用的代码:1
2
3
4
5
6
7
8Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue <? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
关于弱引用和为什么使用弱引用可以参考 Java 理论与实践: 用弱引用堵住内存泄漏 和 深入分析 ThreadLocal 内存泄漏问题。下面看一下 ThreadLocalMap 的 set 函数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
34private void set(ThreadLocal <?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len - 1);
// 使用线性探测法查找元素
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal <?> k = e.get();
// ThreadLocal 对应的 key 存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前数组中的 Entry 是一个陈旧(stale)的元素
if (k == null) {
// 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏,具体可以看源代码,没看太懂
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry。
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlot 清理陈旧的 Entry(key == null),具体的参考源码。如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
关于 set 方法,有几点需要地方:
int i = key.threadLocalHashCode & (len - 1);
,这里实际上是对 len-1 进行了取余操作。之所以能这样取余是因为 len 的值比较特殊,是 2 的 n 次方,减 1 之后低位变为全 1,高位变为全 0。例如 16,减 1 之后对应的二进制为: 00001111,这样其他数字中大于 16 的部分就会被 0 与掉,小于 16 的部分就会保留下来,就相当于取余了。threshold = len * 2 / 3;
1 | private void rehash() { |
我们再看一下 getEntry (没有 get 方法,就叫 getEntry)方法:1
2
3
4
5
6
7
8private Entry getEntry(ThreadLocal <?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
因为 ThreadLocalMap 中采用开放定址法,所以当前 key 的散列值和元素在数组中的索引并不一定完全对应。所以在 get 的时候,首先会看 key 的散列值对应的数组元素是否为要查找的元素,如果不是,再调用 getEntryAfterMiss
方法查找后面的元素。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16private Entry getEntryAfterMiss(ThreadLocal <?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal < ? > k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
最后看一下删除操作。删除其实就是将 Entry 的键值设为 null,变为陈旧的 Entry。然后调用 expungeStaleEntry
清理陈旧的 Entry。1
2
3
4
5
6
7
8
9
10
11
12private void remove(ThreadLocal <?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
前面说完了 ThreadLocalMap,副本变量的存取操作就很好理解了。下面是 ThreadLocal 中的 set 和 get 方法的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
"unchecked") (
T result = (T) e.value;
return result;
}
}
return setInitialValue();
}
存取的基本流程就是首先获得当前线程的 ThreadLocalMap,将 ThreadLocal 实例作为键值传入 Map,然后就是进行相关的变量存取工作了。线程中的 ThreadLocalMap 是懒加载的,只有真正的要存变量时才会调用 createMap 创建,下面是 createMap 的实现:1
2
3void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
如果想要给 ThreadLocal 的副本变量设置初始值,需要重写 initialValue 方法,如下面的形式:1
2
3
4
5ThreadLocal <Integer> threadLocal = new ThreadLocal() {
protected Integer initialValue() {
return 0;
}
};
当创建了一个 ThreadLocal 的实例后,它的散列值就已经确定了,下面是 ThreadLocal 中的实现: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/**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
我们看到 threadLocalHashCode 是一个常量,它通过 nextHashCode()
函数产生。nextHashCode()
函数其实就是在一个 AtomicInteger 变量(初始值为0)的基础上每次累加 0x61c88647,使用 AtomicInteger 为了保证每次的加法是原子操作。而 0x61c88647 这个就比较神奇了,它可以使 hashcode 均匀的分布在大小为 2 的 N 次方的数组里。下面写个程序测试一下:1
2
3
4
5
6
7
8
9
10
11
12public static void main(String[] args) {
AtomicInteger hashCode = new AtomicInteger();
int hash_increment = 0x61c88647;
int size = 16;
List <Integer> list = new ArrayList <> ();
for (int i = 0; i < size; i++) {
list.add(hashCode.getAndAdd(hash_increment) & (size - 1));
}
System.out.println("original:" + list);
Collections.sort(list);
System.out.println("sort: " + list);
}
我们将 size 设为 16,32 和 64 分别测试一下:1
2
3
4
5
6
7
8
9
10
11// size=16
original:[0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9]
sort: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
// size=32
original:[0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25]
sort: [0, 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]
// size=64
original:[0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 6, 13, 20, 27, 34, 41, 48, 55, 62, 5, 12, 19, 26, 33, 40, 47, 54, 61, 4, 11, 18, 25, 32, 39, 46, 53, 60, 3, 10, 17, 24, 31, 38, 45, 52, 59, 2, 9, 16, 23, 30, 37, 44, 51, 58, 1, 8, 15, 22, 29, 36, 43, 50, 57]
sort: [0, 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]
可以看到随着 size 的变化,hashcode 总能均匀的分布。其实这就是 Fibonacci Hashing,具体可以参考 这篇文章。所以虽然 ThreadLocal 的 hashcode 是固定的,当 ThreadLocalMap 中的散列表调整大小(变为原来的 2 倍)之后重新散列,hashcode 仍能均匀的分布在散列表中。
最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。如1
2
3
4
5
6
7
8
9private static ThreadLocal < Connection > connectionHolder = new ThreadLocal < Connection > () {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
1 | private static final ThreadLocal threadSession = new ThreadLocal(); |
一款在线的 Markdown 阅读器,主要用来展示 Markdown 内容。支持 HTML 导出,同时可以方便的添加扩展功能。在这个阅读器的基础又做了一款在线 Github Pages 页面生成器,可以方便的生成不同主题风格的 GitHub Page 页面。
Prism.js
/ Highlight.js
代码高亮在上面的基础上加上了下面的功能
阅读器
生成器
程序使用 marked 将 markdown 格式转为 html 格式,这是一个 js 的库,可以直接在浏览器端使用。下面是一个基本的示例1
2var htmlContent = marked(mdContent);
$("#content").html(htmlContent);
同时 marked 提供了一些接口,让我们可以方便的定制自己的功能。具体的可以参考它的 说明文件 。在下面我们会介绍我们是如何利用这些接口来实现扩展功能。
原始的上传按钮太丑了,所以我们需要自定义自己的样式。这里使用的方式是使用在 input
上面覆盖一个 button
,用 button
来显示样式。同时我们将 button
的 pointer-events
设为 none
,就可以阻止 button
的事件响应(具体可以参考这里)。下面是具体的实现代码:
html:1
2
3
4<div class="upload-area" id="upload-area">
<input type="file" id="select-file" class="select-file">
<button class="select-file-style" id="drop">选择或者拖拽 Markdown 文件到此</button>
</div>
css1
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.upload-area {
width: auto;
height: 200px;
margin: 0 2.6em 0 0.4em;
padding: 0;
position: relative;
cursor: pointer;
transition: height 0.5s;
}
.upload-area .select-file {
border-width: 0px;
width: 100%;
height: 200px;
margin: 0;
cursor: pointer;
}
.upload-area .select-file-style {
background: #F5F7FA;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200px;
border: 0px;
pointer-events: none;
color: #AAB2BD;
font-size: 2em;
line-height: 2em;
font-family: "Microsoft YaHei", "Tahoma", arial;
}
下面是效果图
因为程序完全是运行在浏览器端,所以我们使用 html5 的 FileReader
来读取本地文件。FileReader
提供 4 种读取文件的方式
readAsBinaryString(Blob|File)
readAsText(Blob|File, opt_encoding)
readAsDataURL(Blob|File)
readAsArrayBuffer(Blob|File)
其中 readAsText
用来读取文本文件,readAsDataUrl
可以用来读取图片。具体的介绍可以参考 这里 。FileReader
一般结合文件选择事件或者拖拽事件使用,因为通过这两个事件可以获得源文件。另外 FileReader
是异步读取的,通过 onload
事件可以监听文件是否读取完毕。下面是一个示例, 通过点击 <input type= "file">
选择文件,然后读取文件内容。1
2
3
4
5
6
7
8
9
10document.getElementById("file-select").addEventListener("change", function(e) {
e.stopPropagation();
e.preventDefault();
var reader = new FileReader();
reader.readAsText(this.files[0]);
reader.onload = function (e) {
var content = e.target.result;
//......
};
}, false);
为了方便用户操作,我们提供了点击和拖拽两种方式来上传文件。现在的主流浏览器都支持文件拖拽功能,下面是拖拽过程中触发的事件
事件 | 描述 |
---|---|
dragstart | 用户开始拖动对象时触发。 |
dragenter | 鼠标初次移到目标元素并且正在进行拖动时触发。这个事件的监听器应该之指出这个位置是否允许放置元素。如果没有监听器或者监听器不执行任何操作,默认情况下不允许放置。 |
dragover | 拖动时鼠标移到某个元素上的时候触发。 |
dragleave | 拖动时鼠标离开某个元素的时候触发。 |
drag | 对象被拖拽时每次鼠标移动都会触发。 |
drop | 拖动操作结束,放置元素时触发。 |
dragend | 拖动对象时用户释放鼠标按键的时候触发。 |
另外在拖拽过程中是不触发鼠标事件的。文件读取完后文件信息会保存在 DataTransfer
对象中。详细的介绍可以参考 这里 。下面是添加事件的示例1
2
3fileSelect.addEventListener("dragenter", dragMdEnter, false);
fileSelect.addEventListener("dragleave", dragMdLeave, false);
fileSelect.addEventListener('drop', dropMdFile, false);
读取拖拽的文件1
2
3
4
5
6
7
8
9
10
11function dropMdFile(e) {
// 取消浏览器默认行为
e.stopPropagation();
e.preventDefault();
var reader = new FileReader();
reader.readAsText(e.dataTransfer.files[0]);
reader.onload = function (e) {
var content = e.target.result;
//......
};
}
因为没有服务器,所以为了显示本地图片,使用了替换图片 src
的方式。首先读取本地文件,然后将 <img>
的 src
路径替换为图片内容 。如下所示:1
2
3<img src="path">
// 替换为
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgI...">
下面是具体的代码实现: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// 读取选择或者拖拽的文件(多个文件)
function processImages(imgFiles) {
var index = 0;
for (i = 0; i < imgFiles.length; i++) {
var file = imgFiles[i];
var reader = new FileReader();
reader.readAsDataURL(file);
(function (reader, file) {
reader.onload = function (e) {
cacheImages[file.name] = e.target.result;
index++;
if (index == length) {
replaceImage();
}
}
})(reader, file);
}
}
// 将路径替换为图片内容
function replaceImage() {
var images = $("img");
var i;
for (i = 0; i < images.length; i++) {
var imgSrc = images[i].src;
var imgName = getImgName(imgSrc);
if (cacheImages.hasOwnProperty(imgName)) {
images[i].src = cacheImages[imgName];
}
}
}
如果图片过大,我们可以将图片压缩一下,具体方法就是创建一个 canvas
元素,将图片绘制到 canvas
上,然后将 canvas
转为图片。这种方式对 jpg
文件压缩效果较好,对 png
文件压缩效果不太好。下面是代码实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function compressImage(img, format) {
var max_width = 862;
var canvas = document.createElement('canvas');
var width = img.width;
var height = img.height;
if (format == null || format == "") {
format = "image/png";
}
if (width > max_width) {
height = Math.round(height *= max_width / width);
width = max_width;
}
// resize the canvas and draw the image data into it
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
return canvas.toDataURL(format);
}
为了方便使用,我们可以同时上传多个图片,我们使用 for
循环来读取多个文件,但是有个问题是文件的读取是异步的,也就是说在 for
循环执行完之后,图片可能仍在读取中,当图片读取完后,再调用 onload
回调函数进行处理。简单一点就是说如何在 for
循环中正确使用延迟调用的回调函数。看下面的例子:1
2
3
4
5
6
7
8
9
10
11function print(value, callback) {
console.log("value in print", value);
setTimeout(callback, 1000);
}
for(var i = 0; i < 4; i++) {
var value = i;
print(value, function() {
console.log("value in callback", value);
});
}
上面打的代码和我们读取图片文件的逻辑类似,callback
函数会在调用 print
函数1秒后执行,下面是输出结果1
2
3
4
5
6
7
8value in print 0
value in print 1
value in print 2
value in print 3
value in callback 3
value in callback 3
value in callback 3
value in callback 3
最后在 callback
中 value
值都是3,这是因为在 js 中没有块级作用域,只有函数作用域,也就是说下面的两段代码是等同的:1
2
3
4
5
6
7
8
9
10for(var i = 0; i < 4; i++) {
var value = i;
// do someting
}
// 等同于
var value;
for(var i = 0; i < 4; i++) {
value = i;
// do someting
}
因此,为了解决这个问题,我们只需要为循环中的回调函数添加一个单独的作用域即可,我们使用闭包来实现:1
2
3
4
5
6
7
8for(var i = 0; i < 4; i++) {
var value = i;
(function(value) {
print(value, function() {
console.log("value in callback", value);
});
}(value));
}
我们使用两款代码高亮插件 – highlight.js 和 prism.js,根据喜好可以自由切换。这两款插件对代码块的 html 格式有不同的要求,我们重写了 marked
中解析代码块的方法,根据高亮方式来生成不同的 html 代码:1
2
3
4
5
6renderer.code = function (code, lang) {
if (Setting.highlight == Constants.highlight) {
return "<pre><code class='" + lang + "'>" + code + "</code></pre>";
}
return "<pre><code class='language-" + lang + "'>" + code + "</code></pre>";
};
然后调用 highlight.js 和 prism.js 的代码高亮方法即可1
2
3
4
5
6
7
8
9if (Setting.highlight == Constants.highlight) {
$('pre code').each(function (i, block) {
hljs.highlightBlock(block);
});
} else {
// 添加行号支持
$("pre").addClass("line-numbers");
Prism.highlightAll();
}
为了生成文件的目录,我们需要首先获得目录信息,因此我们重写 marked
的 heading
方法, 将目录信息保存起来,同时为每个标题添加链接图标(仿照 github),下面是代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18renderer.heading = function (text, level) {
var slug = text.toLowerCase().replace(/[\s]+/g, '-');
if (tocStr.indexOf(slug) != -1) {
slug += "-" + tocDumpIndex;
tocDumpIndex++;
}
tocStr += slug;
toc.push({
level: level,
slug: slug,
title: text
});
return "<h" + level + " id=\"" + slug + "\"><a href=\"#" + slug + "\" class=\"anchor\">" + '' +
'<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>' +
'' + "</a>" + text + "</h" + level + ">";
};
同时需要加入下面的 css,以是标题的链接图片正常显示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor {
text-decoration: none
}
h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link {
visibility: visible
}
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
.anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
为了生成目录,我们只需按照保存的目录信息,生成 <ul>
和 <li>
标签即可,具体的可以参考源码中的实现。
目录使用的是页内锚链接的方式进行跳转,如下面所示:1
2
3
4<a href="#h1">跳转到 H1</a>
...
<h1 id="h1">我是 H1</h1>
...
默认情况下,页内锚链接跳转之后,目标标签(上面代码中的 <h1>
)会移动到页面的最顶部,但是在我们的程序中有一个固定的 header,如果跳转到最顶部,目标标签会被 header 遮挡住,所以我们希望目标标签移动到距离页面顶部 header-height
的地方。为了实现我们的需要,只要加入下面的 css 代码即可。1
2
3
4
5
6:target:before {
content:"";
display:block;
height:50px; /* fixed header height*/
margin:-50px 0 0; /* negative fixed header height */
}
Todo 列表实际上就是 checkbox 的列表,完成的工作用选中的 checkbox 表示,未完成的工作用喂选中的列表表示,如下图所示:
一般来说,会将下面形式的 markdown 代码解析为 todo 列表1
2
3- [x] 完成
- [ ] 未完成
- [ ] 未完成
为了实现这个功能,我们重写 marked
中解析列表的方法,加入对 todo 列表的支持。1
2
3
4
5
6
7
8
9
10renderer.listitem = function (text) {
if (/^\s*\[[x ]\]\s*/.test(text)) {
text = text
.replace(/^\s*\[ \]\s*/, '<input type="checkbox" class="task-list-item-checkbox" disabled> ')
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" class="task-list-item-checkbox" disabled checked> ');
return '<li style="list-style: none">' + text + '</li>';
} else {
return '<li>' + text + '</li>';
}
};
同时加入下面的样式:1
2
3
4
5
6
7
8
9.task-list-item-checkbox {
margin: 0 0.2em 0.25em -2.3em;
vertical-align: middle;
}
[type="checkbox"], [type="radio"] {
box-sizing: border-box;
padding: 0;
}
现在的浏览器都已经支持 localStorage
,可以方便的存储数据。localStorage
就是一个对象。我们存储数据就是直接给它添加一个属性,可以通过 localStoage["a"]=1
或者 localStorage.a = 1
的方式来存储数据,但是看起来总觉的不太优雅,因为一般使用下面的方式来操作 localStorage
:1
2
3localStorage.setItem(key, vlaue);
localStorage.getItem(key);
localStorage.removeItem(key);
另外 localStorage
也有一些局限,使用时需要注意:
5M
左右,和浏览器有关JSON.stringfy
方法将对象进行序列化处理之后再保存。使用时需要使用 JSON.parse
方法将字符串转为对象。通过使用 FileSaver.js,我们可以方便的在浏览器端生成文件,并提供给用户下载。使用方法也很简单:1
2var blob = new Blob([htmlContent], {type: "text/html;charset=utf-8"});
saveAs(blob, name);
我们提供了一些扩展功能,用来更好的展示 markdown 内容。在现在的程序中我们可以很方便的添加扩展功能,下面会具体介绍。
为了添加扩展,我们首先需要确定哪些内容需要作为扩展处理。因为在将 markdown 文件转为 html 的过程中,一般是不处理代码块中的内容的,所以我们使用代码块来存放扩展内容,通过代码块的语言来确定是哪种扩展。以添加序列图扩展为例:
确定时序图的代码标记
修改 marked
中对于代码块的解析函数,添加对于时序图标记的支持
1 | var renderer = new marked.Renderer(); |
引入 js-sequence-diagrams
相关文件
1 | <link href="{{ bower directory }}/js-sequence-diagrams/dist/sequence-diagram-min.css" rel="stylesheet" /> |
渲染 Markdown 文件时,调用相关函数
1 | $(".diagram").sequenceDiagram({theme: 'simple'}); |
添加扩展会影响文件的渲染速度,如果不需要某个扩展可以手动关闭。
使用Mathjax 对数学公式进行支持。关于Mathjax 语法,请参考这里。下面是添加扩展的流程:
引入文件并配置
1 | <script type="text/x-mathjax-config"> |
将 markdown 文件转为 html 之后,调用 Mathjax 中的方法将对应标记转为数学公式。
1 | // content 是需要处理的 html 标签的 id |
使用 emojify.js 来提供对 Emoji 标签的支持。Emoji表情参见 EMOJI CHEAT SHEET。下面是添加扩展的流程
引用文件并配置
1 | <script src="http://cdn.bootcss.com/emojify.js/1.1.0/js/emojify.min.js"></script> |
将 markdown 文件转为 html 之后,调用 emojify 中的方法将对应标记转换 emoji 表情。
1 | emojify.run(document.getElementById('content')) |
使用 ECharts 来提供对图表的支持。ECharts 的语法可以参考 官网的示例。下面是使用方法:
确定 ECharts 在 markdown 中的语法标签
在 code 方法解析中添加对 echarts 的支持
1 | renderer.code = function (code, language) { |
将 markdown 文件转为 html 之后,调用 echarts 中的方法,将对应的 div 转为图表:
1 | var chart; |
在生成Github Page页面时,我们可以选择添加 多说 或者 Disqus 评论,其中多说就是在导出的页面中加入下面的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14<div class="ds-thread" data-thread-key="" data-title="" data-url=""></div>
<script type="text/javascript">
var duoshuoQuery = {
short_name: ""
};
(function() {
var ds = document.createElement("script");
ds.type = "text/javascript";
ds.async = true;
ds.src = (document.location.protocol == "https:" ? "https:" : "http:") + "//static.duoshuo.com/embed.js";
ds.charset = "UTF-8";
(document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(ds);
})();
</script>
其中 data-thread-key
, data-title
, data-url
和 short_name
是需要我们自定义的东西。而Disqus 需要在导出时插入下面的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<div id="disqus_thread"></div>
<script type="text/javascript">
var disqus_shortname = '';
var prefix = document.location.protocol == "https:" ? "https:" : "http:"
var disqus_config = function() {
this.page.url = "";
this.page.identifier = ""
};
(function() {
var d = document,
s = d.createElement('script');
s.src = prefix + '//' + disqus_shortname + '.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
其中 disqus_shortname
, page.url
和 page.indertifier
是需要我们自定义的东西。这里需要注意的是 page.url
要使用绝对路径。
具体的插入逻辑可参考源码的实现,这里不再赘述。
]]>canvas 没有提供为其内部元素添加事件监听的方法,因此如果要使 canvas 内的元素能够响应事件,需要自己动手实现。实现方法也很简单,首先获得鼠标在 canvas 上的坐标,计算当前坐标在哪些元素内部,然后对元素进行相应的操作。配合自定义事件,我们就可以实现为 canvas 内的元素添加事件监听的效果。
为了实现javascript对象的自定义事件,我们可以创建一个管理事件的对象,该对象中包含一个内部对象(当作map使用,事件名作为属性名,事件处理函数作为属性值,因为可能有个多个事件处理函数,所以使用数组存储事件处理函数),存储相关的事件。然后提供一个激发事件的函数,通过使用 call
方法来调用之前绑定的函数。下面是代码示例: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
65
66
67(function () {
cce.EventTarget = function () {
this._listeners = {};
this.inBounds = false;
};
cce.EventTarget.prototype = {
constructor: cce.EventTarget,
// 查看某个事件是否有监听
hasListener: function (type) {
if (this._listeners.hasOwnProperty(type)) {
return true;
} else {
return false;
}
},
// 为事件添加监听函数
addListener: function (type, listener) {
if (!this._listeners.hasOwnProperty(type)) {
this._listeners[type] = [];
}
this._listeners[type].push(listener);
cce.EventManager.addTarget(type, this);
},
// 触发事件
fire: function (type, event) {
if (event == null || event.type == null) {
return;
}
if (this._listeners[event.type] instanceof Array) {
var listeners = this._listeners[event.type];
for (var i = 0, len = listeners.length; i < len; i++) {
listeners[i].call(this, event);
}
}
},
// 如果listener 为null,则清除当前事件下的全部事件监听
removeListener: function (type, listener) {
if (listener == null) {
if (this._listeners.hasOwnProperty(type)) {
this._listeners[type] = [];
cce.EventManager.removeTarget(type, this);
}
}
if (this._listeners[type] instanceof Array) {
var listeners = this._listeners[type];
for (var i = 0, len = listeners.length; i < len; i++) {
if (listeners[i] === listener) {
listeners.splice(i, 1);
if (listeners.length == 0)
cce.EventManager.removeTarget(type, this);
break;
}
}
}
}
};
}());
在上面的代码中,EventManager
用来存储所有绑定了事件监听的对象,便于后面判断鼠标是否位于某个对象内部。如果一个自定义对象需要添加事件监听,只需要继承 EventTarget
。
在判断触发某个事件的元素时,需要遍历所有绑定了该事件的元素,判断鼠标位置是否位于元素内部。为了减少不必要的比较,这里使用了一个有序数组,使用元素区域的最小 x 值作为比较值,按照升序排列。如果一个元素区域的最小 x 值大于鼠标的 x 值,那么就无需比较数组中该元素后面的元素。具体实现可以看 SortArray.js
这里设计了一个抽象类,来作为所有元素对象的父类,该类继承了 EventTarget
,并且定义了三个函数,所有子类都应该实现这三个函数。 具体代码如下所示: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(function () {
// 抽象类,该类继承了事件处理类,所有元素对象应该继承这个类
// 为了实现对象比较,继承该类时应该同时实现compareTo, comparePointX 以及 hasPoint 方法。
cce.DisplayObject = function () {
cce.EventTarget.call(this);
this.canvas = null;
this.context = null;
};
cce.DisplayObject.prototype = Object.create(cce.EventTarget.prototype);
cce.DisplayObject.prototype.constructor = cce.DisplayObject;
// 在有序数组中会根据这个方法的返回结果将对象排序
cce.DisplayObject.prototype.compareTo = function (target) {
return null;
};
// 比较目标点的x值与当前区域的最小 x 值,结合有序数组使用,如果 point 的 x 小于当前区域的最小 x 值,那么有序数组中剩余
// 元素的最小 x 值也会大于目标点的 x 值,就可以停止比较。在事件判断时首先使用该函数过滤一下。
cce.DisplayObject.prototype.comparePointX = function (point) {
return null;
};
// 判断目标点是否在当前区域内
cce.DisplayObject.prototype.hasPoint = function (point) {
return false;
};
}());
以鼠标事件为例,这里我们实现了 mouseover
, mousemove
, mouseout
三种鼠标事件。首先对 canvas 添加 mouseover
事件,当鼠标在 canvas 上移动时,会时时对比当前鼠标位置与绑定了上述三种事件的元素的位置,如果满足了触发条件就调用元素的 fire
方法触发对应的事件。下面是示例代码: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_handleMouseMove: function (event, container) {
// 这里传入container 主要是为了使用 _windowToCanvas函数
var point = container._windowToCanvas(event.clientX, event.clientY);
// 获得绑定了 mouseover, mousemove, mouseout 事件的元素对象
var array = cce.EventManager.getTargets("mouse");
if (array != null) {
array.search(point);
// 鼠标所在的元素
var selectedElements = array.selectedElements;
// 鼠标不在的元素
var unSelectedElements = array.unSelectedElements;
selectedElements.forEach(function (ele) {
if (ele.hasListener("mousemove")) {
var event = new cce.Event(point.x, point.y, "mousemove", ele);
ele.fire("mousemove", event);
}
// 之前不在区域内,现在在了,说明鼠标进入了
if (!ele.inBounds) {
ele.inBounds = true;
if (ele.hasListener("mouseover")) {
var event = new cce.Event(point.x, point.y, "mouseover", ele);
ele.fire("mouseover", event);
}
}
});
unSelectedElements.forEach(function (ele) {
// 之前在区域内,现在不在了,说明鼠标离开了
if (ele.inBounds) {
ele.inBounds = false;
if (ele.hasListener("mouseout")) {
var event = new cce.Event(point.x, point.y, "mouseout", ele);
ele.fire("mouseout", event);
}
}
});
}
}
诸如下面形式的函数称之为立即执行函数。1
2
3(function() {
// code
}());
使用立即执行函数的好处就是它限定了变量的作用域,使在立即执行函数中定义变量不会污染其他作用域,更加详细的讲解请看这里
这三个函数的使用类似于java 反射中的 Method.invoke
,方法作为一个主体,将执行方法的对象作为参数传入到方法里。其中 apply 和 call 作用一样,调用后都会立即执行,只是接受参数的形式不同。1
2func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
而 bind 会返回对应函数,不会立即执行,便于以后调用。 看下面的例子:1
2
3
4
5
6
7function aa() {
console.log(111);
console.log(this);
}
var bb = aa.bind(Math);
bb();
更加详细的讲解请看这里
如果给某个元素添加事件监听时需要传递参数,可以使用下面的方法1
2
3
4var i = 1;
aa.addEventListener("click", function() {
bb(i);
}, false);
使用 call
即可1
2
3Child = function() {
Parent.call(this);
}
判断对象为 null 或者 undefined
1 | // `null == undefined` 为true |
判断对象是否有某个属性
1 | if(myObj.hasOwnProperty("<property name>")){ |
canvas中判断点是否在某个路径内部,可以用于多边形的检测。不过 isPointInPath
使用路径是最后一次绘制的图形,如果有多个图形需要判断,需要将前面的图形路径保存下来,判断时需要重新构造路径,不过不需要绘制,如下面1
2
3
4
5
6
7
8
9
10
11
12
13
14
15this.context.save();
this.context.beginPath();
//console.log(this.points);
this.context.moveTo(this.points[0].x, this.points[0].y);
for (var i = 1; i < this.points.length; i++) {
this.context.lineTo(this.points[i].x, this.points[i].y);
}
if (this.context.isPointInPath(target.x, target.y)) {
isIn = true;
}
this.context.closePath();
this.context.restore();
参考文章:
]]>本文绝大部分摘自 CSS 权威指南 第三版
float
)或者定位(absolute
, fixed
)。div
, p
, h1-h6
, ul
, li
, canvas
, table
等。完整的元素可以参考这里。通过使用 display:block
,可以将元素生成块级框。a
, img
, button
, br
, input
, label
, select
, textarea
。完整的可以参考这里通过使用 display:inline
可以让元素变成内联元素。<p>
。<img>
和大部分表单元素 <input type="radio">
。em
: 1em等于 font-size
的设置值
在盒子模型中,水平和垂直方向上各有7个属性:
margin-left
, border-left
, padding-left
, width
, padding-right
, border-right
, margin-right
margin-top
, border-top
, padding-top
, height
, padding-bottom
, border-bottom
, margin-bottom
其中 margin
称为外边距,在计算元素整体宽高的时候一般不包括它。CSS3 中新增了一个属性 box-sizing
,可以用来指定使用的盒模型计算方式。下面是 CSS3 中支持的盒模型计算方式(CSS2种只支持默认的)
content-box
(默认值): width
和 height
属性只作用到 Content Area 的长宽,在 Content Area 外面绘制内边距和边框,见图 (1)。Width = width + padding-left + padding-right + border-left + border-right
Height = height + padding-top + padding-bottom + border-top + border-bottom
border-box
: width
和 height
属性设置的值就为元素整体的宽高,内边距和边框在已设定的宽度和高度内进行绘制,见图 (2)。Width = width
Height = height
inherit
: 继承父类的属性
在上面提到的几个属性中,只有margin
, width
, height
可以设为 auto
,padding
和 border
必须设定为特定值或者使用默认值。
在上面提到的7个水平属性中,只有3个值可以设置为 auto
:width
, margin-left
, margin-right
。其余属性必须设置为特定的值或者使用默认值。下面是使用 auto
的几种情形:
auto
: 如果 width
, margin-left
, margin-right
这三个属性都设为非 auto
的特定值,那么会将 margin-right
强制为 auto
。auto
: 如果三个属性中某个值设为 auto
,而余下的两个属性设为特定的值,那么设置为 auto
的属性值会自动确定所需长度,从而使元素框的总宽度(上面提到的7种属性相加)等于父容器的 width
。auto
,width
设为特定值: 元素会居中(常用的居中方式),margin-left
和 margin-right
会设为相等的长度width
设为 auto
,外边距有一个或者两个均设为 auto
: 设为 auto
的外边距会变成0,如果两个外边距都设为 auto
,会都变为0。margin-top
和 margin-bottom
都设为 auto
(对于定位元素会有不同),会将它们计算为0。height
设为 auto
,一般等于其包含的子元素的总高度。针对垂直外边距(margin-top
和 margin-bottom
),两个相邻的垂直外边距会合并成一个外边距,两个外边距中较小的一个会被较大的一个合并。详细内容可以参考 这里 。
如果外边距中有负值:
margin-bottom:-10px
,和它相邻的另外一个为:margin-top:-20px
,会保留 margin-top:-20px
。margin-bottom:20px
,另外一个为: margin-top:-10px
,最终的效果相当于 margin-bottom:10px
。外边距可以是负的,即 margin
可以设为负值,此时子元素的 width
或者 height
就有可能大于父元素的 width
。只有外边距能小于0,内边距、边框和内容的宽高都不能设为负值。
东西比较多,先附一些文章链接:
http://meyerweb.com/eric/css/inline-format.html
https://www.w3.org/TR/CSS21/visuren.html#inline-box
行内框通过向内容区(context-area)增加行间距(leading)来描述。对于非替换元素来说,元素行内框的高度刚好等于 line-height
的值。对于替换元素来说,元素行内框的高度等于元素的 height + margin-top + margin-bottom + padding-top + padding-bottom + border-top + border-bottom
。
浏览器会根据行内元素行内框的大小来对元素布局。假设行内元素的内容区高 20px
,但是 line-height
只有 14px
,那么为该元素分配的高度只有 14px
,就会出现内容去溢出的情况(覆盖其他的行元素)。
width
和 height
属性不会作用于行内非替换元素,即不能设置宽高。width
和 height
。如果不设置宽高,会使用元素本来的宽度和高度。line-height
只作用于内联元素或者其他的内联内容。
normal
- 默认值,设置合理的行间距(1.2)12px
、1em
等等font-size
的比值font-size
的百分比inherit
- 从父类中继承width
和 height
是不起作用的line-height
margin-top
和 margin-bottom
不作用于行内非替换元素,比如 span
使用 display:value
可以修改元素的类别。有效值如下:
值 | 描述 |
---|---|
none | 此元素不会被显示。 |
block | 此元素将显示为块级元素,此元素前后会带有换行符。 |
inline | 默认。此元素会被显示为内联元素,元素前后没有换行符。 |
inline-block | 行内块元素。(CSS2.1 新增的值) |
list-item | 此元素会作为列表显示。 |
run-in | 此元素会根据上下文作为块级元素或内联元素显示。 |
compact | CSS 中有值 compact,不过由于缺乏广泛支持,已经从 CSS2.1 中删除。 |
marker | CSS 中有值 marker,不过由于缺乏广泛支持,已经从 CSS2.1 中删除。 |
table | 此元素会作为块级表格来显示(类似 table),表格前后带有换行符。 |
inline-table | 此元素会作为内联表格来显示(类似 table),表格前后没有换行符。 |
table-row-group | 此元素会作为一个或多个行的分组来显示(类似 tbody)。 |
table-header-group | 此元素会作为一个或多个行的分组来显示(类似 thead)。 |
table-footer-group | 此元素会作为一个或多个行的分组来显示(类似 tfoot)。 |
table-row | 此元素会作为一个表格行显示(类似 tr)。 |
table-column-group | 此元素会作为一个或多个列的分组来显示(类似 colgroup)。 |
table-column | 此元素会作为一个单元格列显示(类似 col) |
table-cell | 此元素会作为一个表格单元格显示(类似 td 和 th) |
table-caption | 此元素会作为一个表格标题显示(类似 caption) |
inherit | 规定应该从父元素继承 display 属性的值。 |
inline-block
:会使元素表现的像行内非替换元素一样,是行内元素,但是可以设置宽高,margin, border, padding 会影响行内框的高度run-in
:使某些块级元素成为下一个元素的行内元素(chrome不支持)。下面是MDN上关于 float
的定义
The float CSS property specifies that an element should be taken from the normal flow and placed along the left or right side of its container, where text and inline elements will wrap around it.
根据定义需要注意的有下面三点:
display:block
。如果浮动元素和正常流中的内容发生重叠(浮动元素的外边距为负值),会按照以下规则显示内容:
清除浮动就是让元素的左边或者右边或者两边不会有浮动元素出现。清除浮动的一个主要的原因就是增加父容器的高度,当子元素浮动时,会脱离正常流,因此父元素计算高度时不会加上浮动子元素的高度,就会造成父元素的高度小于浮动子元素。当清除浮动之后,父容器就可以正确高度。下面是清除浮动的几种方式,更多方式可以参考 这里 :
使用带clear元素的空属性
1 | .clear{ |
使用 :after
伪元素
1 | .clearfix:after { |
在父容器里添加 overflow:auto
或者 overflow:hidden
CSS 有三种基本的定位机制: 正常流、浮动和绝对定位。使用 position
可以设置不同类型的定位方式。下面是 position
属性值的定义:
static
:默认值,元素框正常生成,不会被特殊的定位。块级元素生成块级块,行内元素生成一个或者多个行框,置于其父元素中。relative
: 元素框偏移某个距离。元素仍保持其未定位前的形状,它原本所占的空间仍保留。relative
的表现和 static
十分类似,不同的是相对于定位参考的是它应该在的位置(或者说它自身的位置),通过使用偏移属性 top
, bottom
, left
和 right
属性会使元素相对于 它的起点进行移动。其他元素的位置不会受到影响。absolute
: 元素会脱离正常流,相对于其最近的非 static 定位的祖先元素定位,如果没有满足条件的祖先元素,则会相对于文档的 body
元素。元素在正常流中的所占的位置会被清除,就好像该元素不存在一样。absolute
元素会生成一个块级框。fixed
: 和 absolute
类似,不过其定位的参考元素是视窗,当页面滚动时还是会停留在原先的位置。 absolute
会跟随父元素滚动。relative
, absolute
以及 fixed
为定位元素 (positioned)static
,其他三种定位都可以使用偏移属性 top
, bottom
, left
, right
。absolute
定位里 left
, right
, width
,有一个值设为 auto
,会自动调整其大小,使总长度相加等于父容器宽度。如果有没有auto,会重置 right
。top
和 bottom
类似。利用 z-index
可以修改元素相互的覆盖顺序。所有数都可以作为 z-index
的值,包括负数。需要注意的是
z-index
只能作用于定位元素,static
元素会失效z-index
,子元素设置的 z-index
是相对于父元素的局部 z-index
。比如下面的代码:1 | .p1 { |
.p2 .c
会在 .p1 .c
的下面
首先选择图片的一块区域,然后将这块区域放大,然后再绘制到原先的图片上,保证两块区域的中心点一致, 如下图所示:
1 | <canvas id="canvas" width="500" height="500"> |
获得 canvas 和 image 对象,这里使用 <img>
标签预加载图片, 关于图片预加载可以看这里1
2
3var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var img = document.getElementById("img");
设置相关变量1
2
3
4
5
6
7
8
9
10// 图片被放大区域的中心点,也是放大镜的中心点
var centerPoint = {};
// 图片被放大区域的半径
var originalRadius = 100;
// 图片被放大区域
var originalRectangle = {};
// 放大倍数
var scale = 2;
// 放大后区域
var scaleGlassRectangle
1 | function drawBackGround() { |
这里我们使用鼠标的位置作为被放大区域的中心点(放大镜随着鼠标移动而移动),因为 canvas 在画图片的时候,需要知道左上角的坐标以及区域的宽高,所以这里我们计算区域的范围1
2
3
4
5
6function calOriginalRectangle(point) {
originalRectangle.x = point.x - originalRadius;
originalRectangle.y = point.y - originalRadius;
originalRectangle.width = originalRadius * 2;
originalRectangle.height = originalRadius * 2;
}
放大镜一般是圆形的,这里我们使用 clip
函数裁剪出一个圆形区域,然后在该区域中绘制放大后的图。一旦裁减了某个区域,以后所有的绘图都会被限制的这个区域里,这里我们使用 save
和 restore
方法清除裁剪区域的影响。save
保存当前画布的一次状态,包含 canvas 的上下文属性,例如 style
,lineWidth
等,然后会将这个状态压入一个堆栈。restore
用来恢复上一次 save 的状态,从堆栈里弹出最顶层的状态。1
2
3
4
5
6context.save();
context.beginPath();
context.arc(centerPoint.x, centerPoint.y, originalRadius, 0, Math.PI * 2, false);
context.clip();
......
context.restore();
通过中心点、被放大区域的宽高以及放大倍数,获得区域的左上角坐标以及区域的宽高。1
2
3
4
5
6scaleGlassRectangle = {
x: centerPoint.x - originalRectangle.width * scale / 2,
y: centerPoint.y - originalRectangle.height * scale / 2,
width: originalRectangle.width * scale,
height: originalRectangle.height * scale
}
在这里我们使用 context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
方法,将 canvas 自身作为一副图片,然后取被放大区域的图像,将其绘制到放大镜区域里。1
2
3
4
5
6context.drawImage(canvas,
originalRectangle.x, originalRectangle.y,
originalRectangle.width, originalRectangle.height,
scaleGlassRectangle.x, scaleGlassRectangle.y,
scaleGlassRectangle.width, scaleGlassRectangle.height
);
createRadialGradient
用来绘制渐变图像1
2
3
4
5
6
7
8
9
10
11
12
13context.beginPath();
var gradient = context.createRadialGradient(
centerPoint.x, centerPoint.y, originalRadius - 5,
centerPoint.x, centerPoint.y, originalRadius);
gradient.addColorStop(0, 'rgba(0,0,0,0.2)');
gradient.addColorStop(0.80, 'silver');
gradient.addColorStop(0.90, 'silver');
gradient.addColorStop(1.0, 'rgba(150,150,150,0.9)');
context.strokeStyle = gradient;
context.lineWidth = 5;
context.arc(centerPoint.x, centerPoint.y, originalRadius, 0, Math.PI * 2, false);
context.stroke();
为 canvas 添加鼠标移动事件1
2
3canvas.onmousemove = function (e) {
......
}
鼠标事件获得坐标一般为屏幕的或者 window 的坐标,我们需要将其装换为 canvas 的坐标。getBoundingClientRect
用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。1
2
3
4function windowToCanvas(x, y) {
var bbox = canvas.getBoundingClientRect();
return {x: x - bbox.left, y: y - bbox.top}
}
我们可以通过 css 来修改鼠标样式1
2
3
4
5
6#canvas {
display: block;
border: 1px solid red;
margin: 0 auto;
cursor: crosshair;
}
我们可能基于 canvas 绘制一些图表或者图像,如果两个元素的坐标离得比较近,就会给元素的选择带来一些影响,例如我们画两条线,一个线的坐标是(200.5, 400) -> (200.5, 200)
,另一个线的坐标为 (201.5, 400) -> (201.5, 20)
,那么这两条线几乎就会重叠在一起,如下图所示:
使用图表放大镜的效果
类似于地图中的图例,放大镜使用较为精确的图例,如下图所示:
在放大镜坐标系统中,原始的区域会变大,如下图所示
首先创建一个线段对象1
2
3
4
5
6
7
8
9
10
11
12
13
14function Line(xStart, yStart, xEnd, yEnd, index, color) {
// 起点x坐标
this.xStart = xStart;
// 起点y坐标
this.yStart = yStart;
// 终点x坐标
this.xEnd = xEnd;
// 终点y坐标
this.yEnd = yEnd;
// 用来标记是哪条线段
this.index = index;
// 线段颜色
this.color = color;
}
初始化线段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// 原始线段
var chartLines = new Array();
// 处于放大镜中的原始线段
var glassLines;
// 放大后的线段
var scaleGlassLines;
// 位于放大镜中的线段数量
var glassLineSize;
function initLines() {
var line;
line = new Line(200.5, 400, 200.5, 200, 0, "#888");
chartLines.push(line);
line = new Line(201.5, 400, 201.5, 20, 1, "#888");
chartLines.push(line);
glassLineSize = chartLines.length;
glassLines = new Array(glassLineSize);
for (var i = 0; i < glassLineSize; i++) {
line = new Line(0, 0, 0, 0, i);
glassLines[i] = line;
}
scaleGlassLines = new Array(glassLineSize);
for (var i = 0; i < glassLineSize; i++) {
line = new Line(0, 0, 0, 0, i);
scaleGlassLines[i] = line;
}
}
绘制线段1
2
3
4
5
6
7
8
9
10
11
12
13function drawLines() {
var line;
context.lineWidth = 1;
for (var i = 0; i < chartLines.length; i++) {
line = chartLines[i];
context.beginPath();
context.strokeStyle = line.color;
context.moveTo(line.xStart, line.yStart);
context.lineTo(line.xEnd, line.yEnd);
context.stroke();
}
}
1 | function calGlassRectangle(point) { |
由原理图我们知道,放大镜中使用坐标系的图例要比原始坐标系更加精确,比如原始坐标系使用 1:100
,那么放大镜坐标系使用 1:10
,因此我们需要重新计算线段在放大镜坐标系中的位置。同时为了简便,我们将线段的原始坐标进行了转化,减去原始区域起始的x值和y值,即将原始区域左上角的点看做为(0,0)
。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
41function calScaleLines() {
var xStart = originalRectangle.x;
var xEnd = originalRectangle.x + originalRectangle.width;
var yStart = originalRectangle.y;
var yEnd = originalRectangle.y + originalRectangle.height;
var line, gLine, sgLine;
var glassLineIndex = 0;
for (var i = 0; i < chartLines.length; i++) {
line = chartLines[i];
// 判断线段是否在放大镜中
if (line.xStart < xStart || line.xEnd > xEnd) {
continue;
}
if (line.yEnd > yEnd || line.yStart < yStart) {
continue;
}
gLine = glassLines[glassLineIndex];
sgLine = scaleGlassLines[glassLineIndex];
if (line.yEnd > yEnd) {
gLine.yEnd = yEnd;
}
if (line.yStart < yStart) {
gLine.yStart = yStart;
}
gLine.xStart = line.xStart - xStart;
gLine.yStart = line.yStart - yStart;
gLine.xEnd = line.xEnd - xStart;
gLine.yEnd = line.yEnd - yStart;
sgLine.xStart = parseInt(gLine.xStart * scale);
sgLine.yStart = parseInt(gLine.yStart * scale);
sgLine.xEnd = parseInt(gLine.xEnd * scale);
sgLine.yEnd = parseInt(gLine.yEnd * scale);
sgLine.color = line.color;
glassLineIndex++;
}
glassLineSize = glassLineIndex;
}
绘制放大镜中心的瞄准器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function drawAnchor() {
context.beginPath();
context.lineWidth = 2;
context.fillStyle = "#fff";
context.strokeStyle = "#000";
context.arc(parseInt(centerPoint.x), parseInt(centerPoint.y), 10, 0, Math.PI * 2, false);
var radius = 15;
context.moveTo(parseInt(centerPoint.x - radius), parseInt(centerPoint.y));
context.lineTo(parseInt(centerPoint.x + radius), parseInt(centerPoint.y));
context.moveTo(parseInt(centerPoint.x), parseInt(centerPoint.y - radius));
context.lineTo(parseInt(centerPoint.x), parseInt(centerPoint.y + radius));
//context.fill();
context.stroke();
}
1 | function drawMagnifyingGlass() { |
鼠标移动到放大镜上,然后按下鼠标左键,可以拖动放大镜,不按鼠标左键或者不在放大镜区域都不可以拖动放大镜。
为了实现上面的效果,我们要实现3种事件 mousedown
, mousemove
, ‘mouseup’, 当鼠标按下时,检测是否在放大镜区域,如果在,设置放大镜可以移动。鼠标移动时更新放大镜中兴点的坐标。鼠标松开时,设置放大镜不可以被移动。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
33canvas.onmousedown = function (e) {
var point = windowToCanvas(e.clientX, e.clientY);
var x1, x2, y1, y2, dis;
x1 = point.x;
y1 = point.y;
x2 = centerPoint.x;
y2 = centerPoint.y;
dis = Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2);
if (dis < Math.pow(originalRadius, 2)) {
lastPoint.x = point.x;
lastPoint.y = point.y;
moveGlass = true;
}
}
canvas.onmousemove = function (e) {
if (moveGlass) {
var xDis, yDis;
var point = windowToCanvas(e.clientX, e.clientY);
xDis = point.x - lastPoint.x;
yDis = point.y - lastPoint.y;
centerPoint.x += xDis;
centerPoint.y += yDis;
lastPoint.x = point.x;
lastPoint.y = point.y;
draw();
}
}
canvas.onmouseup = function (e) {
moveGlass = false;
}
当移动到对应的线段上时,鼠标双击可以选择该线段,将该线段的颜色变为红色。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
33canvas.ondblclick = function (e) {
var xStart, xEnd, yStart, yEnd;
var clickPoint = {};
clickPoint.x = scaleGlassRectangle.x + scaleGlassRectangle.width / 2;
clickPoint.y = scaleGlassRectangle.y + scaleGlassRectangle.height / 2;
var index = -1;
for (var i = 0; i < scaleGlassLines.length; i++) {
var scaleLine = scaleGlassLines[i];
xStart = scaleGlassRectangle.x + scaleLine.xStart - 3;
xEnd = scaleGlassRectangle.x + scaleLine.xStart + 3;
yStart = scaleGlassRectangle.y + scaleLine.yStart;
yEnd = scaleGlassRectangle.y + scaleLine.yEnd;
if (clickPoint.x > xStart && clickPoint.x < xEnd && clickPoint.y < yStart && clickPoint.y > yEnd) {
scaleLine.color = "#f00";
index = scaleLine.index;
break;
}
}
for (var i = 0; i < chartLines.length; i++) {
var line = chartLines[i];
if (line.index == index) {
line.color = "#f00";
} else {
line.color = "#888";
}
}
draw();
}
因为线段离得比较近,所以使用鼠标移动很难精确的选中线段,这里使用键盘的w
, a
, s
, d
来进行精确移动1
2
3
4
5
6
7
8
9
10
11
12
13
14
15document.onkeyup = function (e) {
if (e.key == 'w') {
centerPoint.y = intAdd(centerPoint.y, -0.2);
}
if (e.key == 'a') {
centerPoint.x = intAdd(centerPoint.x, -0.2);
}
if (e.key == 's') {
centerPoint.y = intAdd(centerPoint.y, 0.2);
}
if (e.key == 'd') {
centerPoint.x = intAdd(centerPoint.x, 0.2);
}
draw();
}
程序
默认程序
-> 设置默认程序
左侧选中 Windows Media Player
,右侧选择 选择此程序的默认值
在 协议
下勾选上 MMS
参考文章: Solution to Windows Media Player 11 (WMP11) Cannot Stream and Play MMS Media Protocol in Vista
]]>Pthreads 有几种工作模型,例如 Boss/Workder Model、Pileline Model(Assembly Line)、Background Task Model、Interface/Implementation Model,详细介绍可以参考 pthread Tutorial,这里给出一个流水线模型(Pipeline Model)的简单示例。在该示例中,主线程开启了两个子线程,一个子线程用来读取文件,一个子线程用于将结果写入文件,而主线程自身用来计算。
很多时候,一个程序可以分为几个阶段,比如说读取数据、计算、将结果写入文件,当然我们可以使用每个线程依次执行这些操作,但是一个更好的选择是一个线程处理一个阶段,因为对于文件操作来说,硬盘的读写速率是一定的(IO很多时候会成为性能的瓶颈),即使多个线程读取文件,其读写速率也不会变快(IO操作无法使用线程并行)。所以我们可以用一个线程来处理IO,另外的线程全部用于计算上,如果计算量较大,IO的耗时是可以掩盖过去的。比如读取一个 2G 的文件,然后进行计算。使用流水线模型,我们可以这样做,用一个线程专门读取文件,我们将其成为IO线程。IO线程一次读取 50M 数据,之后交给计算线程来处理这些数据,在计算线程处理数据的同时,IO线程再去读文件,假设处理 50M 数据的时间大于读取50M数据的时间, 当计算线程处理完上一份数据之后,要处理的下一份数据读取完毕,那么计算线程又可以紧接着处理这部分数据,这样循环操作,除了第一次读取数据的时候计算线程处于空闲状态,其余读取的时候计算线程都在进行计算,这样就掩盖掉了IO的时间
主线程在程序开始时创建两个子线程,一个用于读,一个用于写,读线程每次只读取一部分文件内容,写线程将这部分数据处理完之后的结果写入文件。创建完线程之后,主线程和写线程就处于等待状态,而读线程就开始读取文件,当读线程读取完第一部分数据之后,读线程进入阻塞状态,主线程开始计算,主线程计算完毕后,写线程开始写入计算结果,同时读线程开始下一部分数据的读取。按照这个流程循环取算存,直到程序结束。
在执行中,3个线程都会进行等待操作,并且处理完自己的任务之后,还要再次进入等待状态。这里使用条件变量来控制线程的挂起和唤醒,使用while循环控制线程的状态的多次切换。下面是示例代码1
2
3
4
5
6
7
8while(1) {
pthread_mutex_lock(&read_lock);
while(read_count == 0 ) {
pthread_cond_wait(&read_cond, &read_lock);
}
read_count--;
pthread_mutex_unlock(&read_lock);
}
上面的代码中,while循环会一直执行,所以我们还要加一个是否可以跳出 while 循环的判断,以便在任务结束后可以终止线程, 如下面的代码:1
2
3
4
5
6
7
8
9
10
11
12while(1) {
pthread_mutex_lock(&read_lock);
while(read_count == 0 && !read_shutdown ) {
pthread_cond_wait(&read_cond, &read_lock);
}
if(read_shutdown) {
break;
}
read_flag = 1 - read_flag;
pthread_mutex_unlock(&read_lock);
}
我们看到在判断线程是否挂起的 while 循环中也加入了!read_shutdown
的判断,即如果马上就要跳出while循环,标明线程已经执行完了它的任务,则无需再进行挂起操作。唤醒该线程的代码如下所示:1
2
3
4
5
6
7pthread_mutex_lock(&read_lock);
if(loop_index == loop_nums - 1) {
read_shutdown = 1;
}
read_count = 1;
pthread_cond_signal(&read_cond);
pthread_mutex_unlock(&read_lock);
下面分析一下条件变量,首先读线程和写线程都要对应一个条件变量,暂称为 read_cond
和 write_cond
, 主线程用read_cond
来告诉读线程自己已经开始计算,读线程可以继续读取下一部分数据了,用write_cond
告诉写线程,计算已经完毕,可以将结果写入文件了 。而主线程需要两个条件变量,暂称为 cal_cond
和 cal_cond2
, 读线程使用 cal_cond
告诉主线程自己已经读完这部分数据了,主线程可以开始计算了。而写线程用 cal_cond2
告诉主线程自己已经写完了上次计算结果,可以再次分配写入的任务了。如果读线程没有读完或者写线程没有写完,主线程都要进入等待状态。
我们知道每个条件变量都会对应一个条件以及一个互斥锁,下面分析一下各个条件的初始值,程序开始时读线程开始工作,主线程要等待读线程读完才能进行计算,所以 read_cond
对应的条件为 true, cal_cond
对应的条件的为 false,写线程必须要等待主线程计算完才可以写,并且在第一次的时候写线程肯定是空闲的, 所以 write_cond
对应的条件为 false,cal_cond2
对应的的条件为 ture。
当读线程读完数据,将数据存到一个缓冲区中(比如一个数组),主线程开始计算,此时读线程又去进行读取操作。如果读线程还是将数据读到上一次读取的缓冲区中(这个缓冲区此时正在被主线程使用),那么就会出现数据竞争。为了解决这个情况,我们可以使用两个缓冲区,读线程填满一个之后再去填另外一个,使用一个变量判断当前该使用哪个缓冲区,即如下面的形式:1
2
3
4
5
6
7
8
9
10
11
12
13int read_buffer_a[BUFFER_SIZE], read_buffer_b[BUFFER_SIZE];
int read_flag;
if(read_flag) {
for(i = 0; i < BUFFER_SIZE; i++) {
fscanf(read_arg->fp, "%d", read_buffer_a+i);
}
} else {
for(i = 0; i < BUFFER_SIZE; i++) {
fscanf(read_arg->fp, "%d", read_buffer_b + i);
}
}
read_flag = 1 -read_flag;
下面是完整的代码, 这里是github地址,可以下载下来运行一下。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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
uint32_t microseconds = 100;
// 线程信息
typedef struct _thread_info{
pthread_t thread_id;
pthread_mutex_t lock;
pthread_cond_t cond;
int run_flag;
int buffer_flag;
int shutdown;
} thread_info;
// 线程函数参数
typedef struct _thread_arg {
FILE *fp;
} thread_arg;
thread_info input_info, output_info, cal_input_info, cal_output_info;
int read_buffer_a[BUFFER_SIZE], read_buffer_b[BUFFER_SIZE];
int write_buffer_a[BUFFER_SIZE], write_buffer_b[BUFFER_SIZE];
void init_resources(int n, ...) {
va_list arg_ptr ;
int i;
va_start(arg_ptr, n);
thread_info * tmp_info = NULL;
for(i = 0; i < n; i++) {
tmp_info = va_arg(arg_ptr, thread_info *);
pthread_mutex_init(&(tmp_info->lock), NULL);
pthread_cond_init(&(tmp_info->cond), NULL);
}
va_end(arg_ptr);
}
void free_resources(int n, ...) {
va_list arg_ptr;
int i;
va_start(arg_ptr, n);
thread_info * tmp_info = NULL;
for(i = 0; i < n; i++) {
tmp_info = va_arg(arg_ptr, thread_info *);
pthread_mutex_destroy(&(tmp_info->lock));
pthread_cond_destroy(&(tmp_info->cond));
}
va_end(arg_ptr);
}
void * input_task(void * args){
thread_arg * input_arg = (thread_arg *) args;
int i;
while(1) {
pthread_mutex_lock(&(input_info.lock));
while(input_info.run_flag == 0 && !input_info.shutdown) {
pthread_cond_wait(&(input_info.cond), &(input_info.lock));
}
if(input_info.shutdown) {
break;
}
input_info.run_flag = 0;
input_info.buffer_flag = 1 - input_info.buffer_flag;
pthread_mutex_unlock(&(input_info.lock));
if(input_info.buffer_flag) {
for(i = 0; i < BUFFER_SIZE; i++) {
fscanf(input_arg->fp, "%d", read_buffer_a + i);
}
} else {
for(i = 0; i < BUFFER_SIZE; i++) {
fscanf(input_arg->fp, "%d", read_buffer_b + i);
}
}
pthread_mutex_lock(&(cal_input_info.lock));
cal_input_info.run_flag = 1;
pthread_cond_signal(&(cal_input_info.cond));
pthread_mutex_unlock(&(cal_input_info.lock));
}
return NULL;
}
void * output_task(void * args){
thread_arg * output_arg = (thread_arg *) args;
int i;
while(1) {
pthread_mutex_lock(&(output_info.lock));
while(output_info.run_flag == 0 && !output_info.shutdown) {
pthread_cond_wait(&(output_info.cond), &(output_info.lock));
}
if(output_info.shutdown) {
break;
}
output_info.run_flag = 0;
output_info.buffer_flag = 1 - output_info.buffer_flag;
pthread_mutex_unlock(&(output_info.lock));
if(output_info.buffer_flag) {
for(i = 0; i < BUFFER_SIZE; i++) {
fprintf(output_arg->fp, "%d\n", write_buffer_a[i]);
usleep(microseconds);
}
} else {
for(i = 0; i < BUFFER_SIZE; i++) {
fprintf(output_arg->fp, "%d\n", write_buffer_b[i]);
usleep(microseconds);
}
}
pthread_mutex_lock(&(cal_output_info.lock));
cal_output_info.run_flag = 1;
pthread_cond_signal(&(cal_output_info.cond));
pthread_mutex_unlock(&(cal_output_info.lock));
}
return NULL;
}
int main(){
FILE *fp_input, *fp_output;
char *input_name = "input.txt";
char *output_name = "output.txt";
int total_nums = 100;
int loop_nums = total_nums / BUFFER_SIZE;
int loop_index = 0;
int i;
thread_arg input_arg, output_arg;
if((fp_input = fopen(input_name, "r")) == NULL) {
printf("can't load input file\n");
exit(1);
}
if((fp_output = fopen(output_name, "w+")) == NULL) {
printf("can't load output file\n");
exit(1);
}
input_arg.fp = fp_input;
output_arg.fp = fp_output;
init_resources(4, &input_info, &output_info, &cal_input_info, &cal_output_info);
input_info.buffer_flag = output_info.buffer_flag = cal_input_info.buffer_flag = 0;
input_info.run_flag = cal_output_info.run_flag = 1;
output_info.run_flag = cal_input_info.run_flag = 0;
input_info.shutdown = output_info.shutdown = 0;
pthread_create(&(input_info.thread_id), NULL, input_task, &input_arg);
pthread_create(&(output_info.thread_id), NULL, output_task, &output_arg);
while(1) {
pthread_mutex_lock(&(cal_input_info.lock));
while(cal_input_info.run_flag == 0) {
pthread_cond_wait(&(cal_input_info.cond), &(cal_input_info.lock));
}
cal_input_info.buffer_flag = 1 - cal_input_info.buffer_flag;
cal_input_info.run_flag = 0;
pthread_mutex_unlock(&(cal_input_info.lock));
pthread_mutex_lock(&(input_info.lock));
if(loop_index == loop_nums - 1) {
input_info.shutdown = 1;
}
input_info.run_flag = 1;
pthread_cond_signal(&(input_info.cond));
pthread_mutex_unlock(&(input_info.lock));
// 这里可以使用OpenMp
if(cal_input_info.buffer_flag) {
for(i = 0; i < BUFFER_SIZE; i++) {
write_buffer_a[i] = read_buffer_a[i] + 1;
}
} else {
for(i = 0; i < BUFFER_SIZE; i++) {
write_buffer_b[i] = read_buffer_b[i] + 1;
}
}
pthread_mutex_lock(&(cal_output_info.lock));
while(cal_output_info.run_flag == 0) {
pthread_cond_wait(&(cal_output_info.cond), &(cal_output_info.lock));
}
cal_output_info.run_flag = 0;
pthread_mutex_unlock(&(cal_output_info.lock));
pthread_mutex_lock(&(output_info.lock));
output_info.run_flag = 1;
pthread_cond_signal(&(output_info.cond));
pthread_mutex_unlock(&(output_info.lock));
if(loop_index == loop_nums - 1) {
break;
}
loop_index++;
}
pthread_mutex_lock(&(cal_output_info.lock));
while(cal_output_info.run_flag == 0) {
pthread_cond_wait(&(cal_output_info.cond), &(cal_output_info.lock));
}
cal_output_info.run_flag = 0;
pthread_mutex_unlock(&(cal_output_info.lock));
pthread_mutex_lock(&(output_info.lock));
output_info.run_flag = 1;
output_info.shutdown = 1;
pthread_cond_signal(&(output_info.cond));
pthread_mutex_unlock(&(output_info.lock));
pthread_join(input_info.thread_id, NULL);
pthread_join(output_info.thread_id, NULL);
free_resources(4, &input_info, &output_info, &cal_input_info, &cal_output_info);
fclose(fp_input);
fclose(fp_output);
return 0;
}
本文主要参考了这个Pthreads线程池
]]>与OpenMP相比,Pthreads的使用相对要复杂一些,需要我们显式的创建、管理、销毁线程,但也正因为如此,我们对于线程有更强的控制,可以更加灵活的使用线程。这里主要记录一下Pthreads的基本使用方法,如果不是十分复杂的使用环境,这些知识应该可以了。本文大部分内容都是参考自这里,有兴趣的可以看一下原文。
1 |
|
编译程序, 需要加上 ‘-lpthread’1
gcc -o helloworld helloworld.c -lpthread
一种可能的输出结果1
2
3
4
5hello from main thread
Hello form sub thread 2
Hello form sub thread 3
Hello form sub thread 1
Hello form sub thread 0
Pthreads使用 pthread_create
函数来创建线程, 函数原型如下:1
2
3
4
5int pthread_create( pthread_t * thread,
const pthread_attr_t * attr,
void * (*start_routine) (void *),
void * arg
);
参数说明:
thread
指向执行线程标识符的指针, 通过该变量来控制线程
attr
设置线程属性, 如果为NULL, 则使用默认的属性
start_routine
线程运行函数的起始地址
arg
运行函数的参数, 这里使用 void*
来作为参数类型, 以便可以向运行函数中传递任意类型的参数, 当然需要在运行函数中将参数转换为其原来的类型.
返回值
如果创建线程成功会返回0, 否则返回错误码.
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13void * thread_function(void *arg) {
int * incoming = (int *)arg;
printf("this is in pthread and arg is %d\n", *incoming);
return NULL;
}
void hello_world() {
pthread_t thread_id ;
int value = 63;
pthread_create(&thread_id, NULL, thread_function, &value);
// 等待线程执行完
pthread_join(thread_id, NULL);
}
在上面的代码中, 在程序最后加上了 pthread_join
函数, 用来完成线程间的同步, 即主线程等待指定的线程(在上面的代码中是 thread_id 对应的线程)执行完再往下执行. 在下面会详细介绍该函数.
pthread_join
可以用于线程之间的同步, 当一个线程对另一个线程调用了join操作之后, 该线程会处于阻塞状态, 直到另外一个线程执行完毕. 下面是一个示意图:
下面是 pthread_join
的函数原型:1
2
3int pthread_join( pthread_t thread,
void ** retval
);
参数说明:
thread
线程标识符, 用来指定等待哪个线程
retaval
用来存储等待线程的返回值
下面是通过获取函数返回值的一个示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void * p_result(void * arg) {
char * m = malloc(sizeof(char) * 3);
m[0] = 'A';
m[1] = 'B';
m[2] = 'C';
return m;
}
void test_get_result() {
pthread_t thread_id;
void * exit_status ;
pthread_create(&thread_id, NULL, p_result, NULL);
pthread_join(thread_id, & exit_status);
char * m = (char* ) exit_status;
printf("m is %s\n", m);
free(m);
}
在 p_result
函数中为了使线程执行完, 我们还可以访问到变量 m 中的数据, m 的内存采用动态分配的方式, 如果静态分配, 即如 char m[3]
的形式, 那么在函数执行完就会清空 m 的值, 我们就无法获得想要的结果.
对于一个线程来说, 其终止方式有两种: 执行完线程函数或者自身调用 pthread_exit(void *)
, 如果线程通过执行完线程函数而终止的, 那么其他线程通过pthread_join
获得的线程返回值就是线程函数的返回值(如上面的例子), 如果线程是通过 pthread_exit(void *)
方式结束的线程, 其线程返回值就是 pthread_exit
传入的参数, 下面是一个示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void * p_exit_result(void * arg) {
printf("print before pthread_exit\n");
pthread_exit((void *)10L);
printf("print after pthread_exit\n");
return NULL;
}
void test_exit_result() {
pthread_t thread_id;
void * exit_status ;
pthread_create(&thread_id, NULL, p_exit_result, NULL);
pthread_join(thread_id, & exit_status);
long m = (long ) exit_status;
printf("m is %ld\n", m);
}
下面是输出结果1
2print before pthread_exit
m is 10
一般来说, 使用 Pthreads 创建的线程默认应该是可 join 的, 但是并不是所有实现都会这样, 所以必要情况下, 我们可以在创建线程时, 显式的指定线程是可 join 的1
2
3
4
5
6
7pthread_t thread_id;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_create(&thread_id, &attr, work, (void *)arg);
pthread_attr_destroy(&attr);
pthread_join(thread_id, NULL);
对于可 join 的线程, 只有当其他线程对其调用了 pthread_join
之后, 该线程才会释放所占用的资源(例如线程所对应的标识符pthread_t, 线程的返回值信息), 如果想要系统回收线程的资源, 而不是通过调用pthread_join
回收资源(会阻塞线程), 我们可以将线程设置为 DETACHED (分离的)
, 有三种方式将线程设为 detached
的
detach
属性: pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_detach(pthread_self());
pthread_detach(thread_id);
(非阻塞, 执行完会立即会返回), 通过上面的方式将线程设为 detached
, 线程运行结束后会自动释放所有资源.
互斥锁用来保护共享变量, 它可以保证某个时间内只有一个线程访问共享变量, 下面是使用互斥锁的具体步骤
pthread_mutex_t
(互斥锁类型) 类型的变量pthread_mutex_init()
来初始化变量pthread_mutex_lock()
获得互斥锁, 如果互斥锁被其他线程占用, 该线程会处于等待状态pthread_mutex_unlock()
释放互斥锁, 以便其他线程使用pthread_mutex_destroy()
释放资源.创建互斥锁有两种方式: 静态方式和动态方式. 静态方式是使用宏 PTHREAD_MUTEX_INITIALIZER
来初始化锁, 如下所示:1
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态方式是调用 pthread_mutex_init
函数动态初始锁, 下面是该函数原型1
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
下面是使用互斥锁的一个示例(使用动态方式):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
28pthread_mutex_t lock;
int share_data;
void * p_lock(void * arg) {
int i;
for(i = 0; i < 1024 * 1024; i++) {
pthread_mutex_lock(&lock);
share_data++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
void test_lock() {
pthread_t thread_id;
void *exit_status;
int i;
pthread_mutex_init(&lock, NULL);
pthread_create(&thread_id, NULL, p_lock, NULL);
for(i = 0; i < 10; i++) {
//sleep(1);
pthread_mutex_lock(&lock);
printf("Shared integer's value = %d\n", share_data);
pthread_mutex_unlock(&lock);
}
printf("\n");
pthread_join(thread_id, & exit_status);
pthread_mutex_destroy(&lock);
}
下是使用互斥量的几个注意点:
在动态创建互斥锁时, 我们可以传入一个锁属性变量 pthread_mutexattr_t
来初始化锁的属性, 通过下面两个函数来初始化和销毁该属性对象1
2int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
然后可以调用下面的方法对属性进行设置
范围
可以指定互斥锁是进程之间的同步还是进程内的同步, 下面是对应的两个锁的范围(scope)
PTHREAD_PROCESS_SHARE
: 进程间同步PTHREAD_PROCESS_PRIVATE
: 进程内同步, 默认值通过调用下面的函数可以设置和获取锁的范围1
2int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
类型
互斥锁的类型有以下几种取值方式(为了兼容性, 一个类型可能有多个名称):
PTHREAD_MUTEX_TIMED_NP / PTHREAD_MUTEX_NORMAL / PTHREAD_MUTEX_DEFAULT
: 缺省值, 也就是普通锁. 当一个线程获得锁之后, 其余请求锁的线程将形成一个等待队列, 并在加锁线程解锁后按照优先级获得锁. 这种策略保证了资源分配的公正性.PTHREAD_MUTEX_RECURSIVE_NP / PTHREAD_MUTEX_RECURSIVE
: 嵌套锁, 允许一个线程对同一个锁成功获得多次, 并通过多次 unlock 来解锁. 如果是不同线程请求, 则在加锁线程解锁后重新竞争.PTHREAD_MUTEX_ERRORCHECK_NP / PTHREAD_MUTEX_ERRORCHECK
: 如果同一个线程请求同一个锁,则返回EDEADLK
,否则与PTHREAD_MUTEX_TIMED_NP
类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁PTHREAD_MUTEX_ADAPTIVE_NP
: 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争可以使用下面的函数获取和设置锁的类型1
2int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
条件变量对应的数据类型为 pthread_cond_t
, 通过使用条件变量, 可以使线程在某个 特定条件 或者 事件 发生之前处于挂起状态. 当事件或者条件发生之后, 另一个线程可以通过信号来唤起挂起的线程. 条件变量主要使用下面几个函数
初始化(init)
和互斥锁一样, 条件变量也有两种初始化方式: 静态方式和动态方式1
2
3
4// 静态
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 动态, 成功返回0
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
销毁(destroy)1
int pthread_cond_destroy(pthread_cond_t *cond);
等待函数(wait)1
2
3
4
5
6int pthread_cond_wait( pthread_cond_t * restrict cond,
pthread_mutex_t * restrict mutex );
int pthread_cond_timedwait( pthread_cond_t * restrict cond,
pthread_mutex_t * restrict mutex,
const struct timespec * restrict abstime );
通过调用 wait 函数, 线程会处于挂起状态. 其中 pthread_cond_timedwait
的含义为: 如果在 abstime
时间内(系统时间小于abstime), 线程没有被唤醒, 那么线程就会结束等待, 同时返回 ETIMEDOUT
错误.
唤醒函数(signal)1
2int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
singal 函数一次只能唤醒一个线程, 而 broadcast 会唤醒所有在当前条件变量下等待的线程.
下面是条件变量的具体使用, 首先一个线程会根据条件来确实是否需要处于挂起状态, 即如下面的形式1
2
3if(flag == 0){
pthread_cond_wait(...);
}
如果flag不为0, 那么线程就不进入等待状态, 否则就挂起线程, 等待flag不为0(满足条件了, 可以往下执行)时被唤醒. 唤醒该线程的代码如下所示:1
2flag = 1;
pthread_cond_signal(...);
下面考虑一下这种情况, 首先 flag = 0
, 当线程1执行到 if(flag == 0)
时, 发现不满足继续往下执行的条件, 即将进入挂起状态, 就在其刚要挂起的时候(还没挂起), 线程2执行了唤醒线程1的代码(修改flag的值, 唤醒线程1), 假设线程2执行完上述操作之后, 线程1仍然还没有挂起, 所以 pthread_cond_signal
并没有起到作用. 此后线程1终于进入了挂起状态, 等待线程2的唤醒, 而线程2则认为它已经唤醒了线程1, 让其往下执行了. 此时问题就来了, 如果线程2不再执行唤醒线程1的操作, 那么线程1就会永远处于挂起状态. 为了解决这种情况, 需要满足从判断 flag==0
到 pthread_cond_wait()
执行, flag
的值不能发生变化,并且不能提前执行唤醒操作. 为了实现这种需求, 我们需要加一个锁操作,
等待代码:1
2
3
4
5pthread_mutex_lock(&mutex);
if(flag == 0){
pthread_cond_wait(...);
}
pthread_mutex_unlock(&mutex);
唤醒代码1
2
3
4pthread_mutex_lock(&mutex);
flag = 1;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&condition);
我们看到 pthread_cond_wait
的函数原型中第一个参数为条件变量, 第二个参数为互斥锁, 之所以需要传入互斥锁, 是因为如果不传入互斥锁, 当线程进入挂起状态时, 就无法释放掉该互斥锁, 而其他线程就无法获得该互斥锁,就没办法更新flag
的值, 也无法唤醒线程1. 线程1就会永远处于挂起状态, 线程2就会永远处于请求互斥锁的状态. 所以当线程1进入挂起状态时需要释放掉互斥锁, 被唤醒之后再重新获得互斥锁, 即 pthread_cond_wait
可以看成下面的操作:1
2
3pthread_mutex_unlock(&mutex);
wait_on_signal(&condition);
pthread_mutex_lock(&mutex);
所有一个条件变量总是和一个互斥锁关联.
下面再来看一下等待代码, 在某些特定情况下, 即使没有线程调用 pthread_cond_signal
函数, ‘pthread_cond_wait’ 函数也有可能返回(具体解释可以看看 spurious wakeup), 但是此时条件并不满足, 如果程序往下执行, 那么就可能会出错. 所以为了避免这种情况, 即使线程被唤醒了, 也应该再检查一下条件是否满足, 即使用 while 循环代替 if 判断
1 | pthread_mutex_lock(&mutex); |
下面是一个使用示例: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
29pthread_cond_t is_zero;
pthread_mutex_t mutex;
int con_share_data = 32767;
void * p_condition(void * arg) {
while(con_share_data > 0) {
pthread_mutex_lock(&mutex);
con_share_data--;
pthread_mutex_unlock(&mutex);
}
pthread_cond_signal(&is_zero);
}
void test_condition() {
pthread_t thread_id;
void *exit_status;
int i;
pthread_cond_init(&is_zero, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&thread_id, NULL, p_condition, NULL);
pthread_mutex_lock(&mutex);
while(con_share_data != 0) {
pthread_cond_wait(& is_zero, &mutex);
}
pthread_mutex_unlock(&mutex);
pthread_join(thread_id, &exit_status);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&is_zero);
}
信号量本质上可以看做是一个计数器, 它主要有两种操作, 第一类操作为 down 或者 wait – sem_wait(...)
, 目的是为了减小计数器(将信号俩减1), 另一类为 up 或者 signal – sem_post(...)
, 目的是为了增大计数器(将信号量加1). 当线程调用 sem_wait()
时, 如果信号量的值大于0, 那么只会把信号量减1, 线程会继续往下执行. 如果信号量的值为0, 那么线程就会进入阻塞状态, 直到另外一个线程执行了 sem_post()
操作, 对信号量进行了增操作, 该线程才会继续往下执行.
信号量主要用于对一些稀缺资源的同步, 什么叫做稀缺资源, 就是说这个资源只有有限的几个, 但是又多于一个, 在某一个时刻, 可以供有限的几个线程使用, 但又不是全部线程使用. 如果将信号量初始化为1, 那么该信号量就等同于互斥锁了, 因此一次只能有一个线程获得信号量的资源, 如果其他线程想要获得, 必须等该线程对信号量进行增操作. 举个例子说: 有10个人去银行办理业务, 但是银行只有4个窗口(信号量初始化为4), 所以前4个人到了银行就可以办理业务, 但是第5个人之后就必须要等待, 等前面的某个人办理完业务(增加信号量), 空出窗口来. 而当第5个人去办理业务时, 空出的窗口又被占用了(减小信号量), 剩下的人还是要等待. 信号量在执行过程中和上述例子不同的一点是, 当有空余的资源出现时, 线程并不一定按照 FIFO(先进先出) 的顺序来获取资源, 而有可能是随机一个线程获得资源.
下面是信号量相关的函数
类型
信号量的类型是 sem_t
, 需要引入头文件 #include <semaphore.h>
初始化和销毁1
2int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
init 函数的第二个参数用来标识信号量的范围: 0 表示一个进程中线程间共享, 非0 表示进程间共享. 第三个参数就是信号量的可用数量.
wait和signal1
2int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
下面是一个使用示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int sem_share_data = 0;
// use like a mutex
sem_t binary_sem;
void * p_sem(void * arg) {
sem_wait(&binary_sem); // 减少信号量
// 在这里使用共享数据;
sem_post(&binary_sem); // 增加信号量
}
void test_sem() {
sem_init(&binary_sem, 0, 1); // 信号量初始化为1, 当初互斥锁使用
// 在这里创建线程
sem_wait(&binary_sem);
// 在这里使用共享变量
sem_post(&binary_sem);
// 在这里join线程
sem_destroy(&binary_sem);
}
对于读写锁来说, 多个线程可以同时获得读锁, 但某一个时间内, 只有一个线程可以获得写锁. 如果已经有线程获得了读锁, 则任何请求写锁的线程将被阻塞在写锁函数的调用上, 同时如果线程已经获得了写锁, 那么任何请求读锁或者写锁 的线程都会被阻塞. 下面是读写锁的基本函数:
锁类型1
pthread_rwlock_t
初始化/销毁1
2int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
读锁1
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
写锁1
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
释放锁1
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18pthread_rwlock_t rw_lock;
void * p_rwlock(void * arg) {
pthread_rwlock_rdlock(&rw_lock);
// 读取共享变量
pthread_rwlock_unlock(&rw_lock);
}
void test_rwlock() {
pthread_rwlock_init(&rw_lock, NULL);
// 创建线程
pthread_rwlock_wrlock(&rw_lock);
// 修改共享变量
pthread_rwlock_unlock(&rw_lock);
// join线程
pthread_rwlock_destroy(&rw_lock);
}
pthread Tutoriaed Tutorial
POSIX Threads Programming
Linux线程-互斥锁pthread_mutex_t
Pthread:POSIX 多线程程序设计
下面列出一些学习资料,如果想深入学习Pthreads可以看下这些资料(摘自POSIX 多线程程序设计):
Pthreads多线程编程指南
Programing with POSIX thread
Pthread Primer
在程序中一般都会用到命令行选项, 我们可以使用getopt 和getopt_long函数来解析命令行参数
getopt主要用来处理短命令行选项, 例如./test -v
中-v
就是一个短选项. 使用该函数需要引入头文件<unistd.h>
, 下面是该函数的定义1
int getopt(int argc, char * const argv[], const char * optstring);
其中 argc 和 argv 是main函数中的传递的参数个数和内容, optstring用来指定可以处理哪些选项, 下面是optstring的一个示例:1
"a:bc"
该示例表明程序可以接受3个选项: -a -b -c
, 其中 a
后面的 :
表示该选项后面要跟一个参数, 即如 -a text
的形式, 选项后面跟的参数会被保存到 optarg 变量中. 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc, char **argv) {
int ch;
while((ch = getopt(argc, argv, "a:b")) != -1) {
switch(ch) {
case 'a':
printf("option a: %s\n", optarg);
break;
case 'b':
printf("option b \n");
break;
case '?': // 输入未定义的选项, 都会将该选项的值变为 ?
printf("unknown option \n");
break;
default:
printf("default \n");
}
}
}
执行 ./test -a aa -b -c
输出结果如下:1
2
3option a: aa
option b
unknown option
getopt_long支持长选项的命令行解析, 所为长选项就是诸如--help
的形式, 使用该函数, 需要引入<getopt.h>
下面是函数原型:1
2
3
4
5
6
7
8
9
10
11
12
13
int getopt_long(int argc,
char * const argv[],
const char *optstring,
const struct option *longopts,
int *longindex);
int getopt_long_only(int argc,
char * const argv[],
const char *optstring,
const struct option *longopts,
int *longindex);
其中 argc , argv , optstring 和getopt中的含义一样, 下面解释一下longopts 和longindex
longopts
longopts 指向一个struct option 的数组, 下面是option的定义:1
2
3
4
5
6struct option {
const char *name;
int has_arg;
int *flag;
int val;
};
下面是各字段的含义
help
下面是longopts的一个示例1
2
3
4
5struct option opts[] = {
{"version", 0, NULL, 'v'},
{"name", 1, NULL, 'n'},
{"help", 0, NULL, 'h'}
};
我们来看{"version", 0, NULL, 'v'}
, version 即为长选项的名称, 即按如下形式--version
, 0 表示该选项后面不带参数, NULL 表示直接将v返回(字符v在ascii码中对应的数值), 即在使用getopt_long遍历到该条选项时, getopt_long 返回值为字符v对应的ascii码值.
longindex
longindex表示长选项在longopts中的位置, 例如在上面的示例中, version 对应的 longindex 为0, name 对应的 longindex 为1, help对应的 longindex 为2, 该项主要用于调试, 一般设为 NULL 即可.
下面是一个使用示例: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
32void use_getpot_long(int argc, char *argv[]) {
const char *optstring = "vn:h";
int c;
struct option opts[] = {
{"version", 0, NULL, 'v'},
{"name", 1, NULL, 'n'},
{"help", 0, NULL, 'h'}
};
while((c = getopt_long(argc, argv, optstring, opts, NULL)) != -1) {
switch(c) {
case 'n':
printf("username is %s\n", optarg);
break;
case 'v':
printf("version is 0.0.1\n");
break;
case 'h':
printf("this is help\n");
break;
case '?':
printf("unknown option\n");
break;
case 0 :
printf("the return val is 0\n");
break;
default:
printf("------\n");
}
}
}
然后我们运行程序 ./test --name zhangjikai --version --help --haha
, 下面是运行结果:1
2
3
4
5username is zhangjikai
version is 0.0.1
this is help
./test: unrecognized option '--haha'
unknown option
当然我们也可以使用短选项 ./test -n zhangjikai -v -h
下面我们对程序做一下修改, 这一次将 struct option 中的 flag 和 longindex 设为具体的值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
36void use_getpot_long2(int argc, char *argv[]) {
const char *optstring = "vn:h";
int c;
int f_v = -1, f_n = -1, f_h = -1, opt_index = -1;
struct option opts[] = {
{"version", 0, &f_v, 'v'},
{"name", 1, &f_n, 'n'},
{"help", 0, &f_h, 'h'}
};
while((c = getopt_long(argc, argv, optstring, opts, &opt_index)) != -1) {
switch(c) {
case 'n':
printf("username is %s\n", optarg);
break;
case 'v':
printf("version is 0.0.1\n");
break;
case 'h':
printf("this is help\n");
break;
case '?':
printf("unknown option\n");
break;
case 0 :
printf("f_v is %d \n", f_v);
printf("f_n is %d \n", f_n);
printf("f_h is %d \n", f_h);
break;
default:
printf("------\n");
}
printf("opt_index is %d\n\n", opt_index);
}
}
运行程序: ./test --name zhangjikai --version --help
, 下面是运行结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14f_v is -1
f_n is 110
f_h is -1
opt_index is 1
f_v is 118
f_n is 110
f_h is -1
opt_index is 0
f_v is 118
f_n is 110
f_h is 104
opt_index is 2
我们可以看到当给 flag 指定具体的指针之后, getopt_long 会返回0, 因此会去执行case 0, 并且 val 的值赋给了 flag 指向的变量. 下面我们用短选项执行一下程序 ./test -n zhangjikai -v -h
, 下面是运行结果1
2
3
4
5
6
7
8username is zhangjikai
opt_index is -1
version is 0.0.1
opt_index is -1
this is help
opt_index is -1
我们看到使用短选项的时候 getopt_long 就相当于 getopt , flag 和 longindex都不起作用了.
下面解释一下 getopt_long 和 getopt_long_only的区别, 首先用下列选项运行一下 use_getopt_long ./test -name zhangjkai -version -help
, 下面是输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13username is ame
version is 0.0.1
./test: invalid option -- 'e'
unknown option
./test: invalid option -- 'r'
unknown option
./test: invalid option -- 's'
unknown option
./test: invalid option -- 'i'
unknown option
./test: invalid option -- 'o'
unknown option
username is -help
我们看到使用短选项标识符 -
指向长选项时, 程序还是会按短选项来处理, 即一个字符一个字符的解析. 下面我们将 use_getopt_long 做一下更改, 即将 getopt_long
改为 getopt_long_only
, 如下所示:
1 | void use_getpot_long3(int argc, char *argv[]) { |
下面再运行程序 ./test -name zhangjikai -version -help
, 下面是运行结果:1
2
3username is zhangjikai
version is 0.0.1
this is help
即使用 getopt_long_only 时, -
和 --
都可以作用于长选项, 而使用 getopt_only 时, 只有 --
可以作用于长选项.
这里主要记录一下C对二进制的读写操作, 包括随机读取文件和写入文件
fseek
fseek主要用来移动文件指针, 它允许用户像对待数组那样对待一个文件, 可以直接将文件指针移动到任意字节处, 下面是它的函数原型:1
int fseek ( FILE * stream, long int offset, int origin );
下面是个参数的含义
下面是一些使用示例, 其中fp是一个文件指针1
2
3
4
5fseek(fp, 0L, SEEK_SET) // 移动到文件开头
fseek(fp, 10L, SEEK_SET) // 移动到文件的第10个字节
fseek(fp, 2L, SEEK_CUR) // 从文件的当前位置向前移动两个字节
fssek(fp, 0L, SEEK_END) // 移动到文件的结尾处
fseek(fp, -10L, SEEK_END) // 从文件结尾处退回10个字节
如果函数执行正常, 那么返回值为0, 如果有错误, 则返回值为-1.
ftell
ftell函数用来获得当前文件指针的位置, 它返回当前文件指针距离文件开始处的字节数目, 函数原型如下1
long int ftell ( FILE * stream );
如果函数执行失败会返回-1,
下面是一个使用示例, 接合fseek和ftell用来获得文件的大小1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16long file_size(char *fileName) {
long size;
FILE *fp;
if((fp = fopen(fileName, "rb")) == NULL) {
printf("can't open file %s\n", fileName);
exit(EXIT_FAILURE);
}
fseek (fp, 0 , SEEK_END);
size = ftell(fp);
double mb;
mb = size * 1.0 / 1024 / 1024;
printf("file size is %ldB and %.2fMB \n", size, mb);
fclose(fp);
return size;
}
Writes an array of count elements, each one with a size of size bytes, from the block of memory pointed by ptr to the current position in the stream.
以二进制的形式将数据块写入文件, 函数原型为:1
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
下面是参数意义:
如果写入成功, 会返回写入的数据块的数量, 即count, 如果返回值不等于count, 说明程序运行出现了错误.
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void file_fwrite(char *fileName) {
FILE *fp;
if((fp = fopen(fileName, "w+b")) == NULL) {
printf("can't open file %s\n", fileName);
exit(EXIT_FAILURE);
}
int count = 20, i;
int *buffer;
buffer = malloc(sizeof(int) * count);
for(i = 0; i < count; i++) {
buffer[i] = i;
}
long success_num = 0;
success_num = fwrite(buffer, sizeof(int) , count, fp);
printf("success_num is %ld\n", success_num);
fclose(fp);
free(buffer);
}
下面是写入的内容1
2
3
4
50000 0000 0100 0000 0200 0000 0300 0000
0400 0000 0500 0000 0600 0000 0700 0000
0800 0000 0900 0000 0a00 0000 0b00 0000
0c00 0000 0d00 0000 0e00 0000 0f00 0000
1000 0000 1100 0000 1200 0000 1300 0000
上面是以16进制的形式进行显示, 即一个数字为4位, 一个int值占32位(4个字节), 在上面的内容中, 8个数字为1个int, 如 0000 0000
为第一个int值, 即0, 0100 0000
为第二个int值, 即1. 这里需要说明的是在写入时是字节作为一个基本单位的, 并且低位字节是先写入的, 如0100 0000
, 其中01
就是int的最低位的字节. 我们来看这个例子, 如果写入文件之后的值为1234 5678
, 那么其原先的值就是0x78563412
Reads an array of count elements, each one with a size of size bytes, from the stream and stores them in the block of memory specified by ptr.
以二进制的形式将数据块读入内存, 下面是函数原型:1
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
下面是参数含义
如果读入成功, 会返回读入的数据块的数量. 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void file_fread(char *fileName) {
long count;
int *buffer;
FILE *fp;
if((fp = fopen(fileName, "rb")) == NULL) {
printf("can't open file %s\n", fileName);
exit(EXIT_FAILURE);
}
fseek (fp, 0 , SEEK_END);
count = ftell(fp);
rewind(fp);
buffer =(int *) malloc(sizeof(int) * count);
fread(buffer, sizeof(int), count / sizeof(int), fp);
int i;
for(i = 0; i < count/ sizeof(int); i++) {
printf("%d\n", buffer[i]);
}
fclose(fp);
free(buffer);
}
Sets the position indicator associated with stream to the beginning of the file.
重置文件指针到文件开头位置, 下面是函数原型:1
void rewind ( FILE * stream );
当打开一个文件后, 系统会自动为该文件流分配一个缓冲区, 其大小为BUFSIZ
, 我们可以通过打印BUFSIZ
来获得默认的缓冲区大小. 如果想自定义缓冲区, 可以使用setbuf和setvbuf函数1
printf("%d", BUFSIZ);
setbuf
Specifies the buffer to be used by the stream for I/O operations, which becomes a fully buffered stream. Or, alternatively, if buffer is a null pointer, buffering is disabled for the stream, which becomes an unbuffered stream.
为文件流指定一个缓冲区, 函数原型为1
void setbuf ( FILE * stream, char * buffer );
buffer表示指定的缓冲区, 需要注意的一点是, 这里缓冲区的大小仍然为BUFSIZ
, 只不过是缓冲区的位置发生了改变, 因此buffer的大小应该大于或者等于BUFSIZ
. 该函数应该在文件刚被打开时调用, 不能在进行了读写操作之后再调用. 如果buffer的为NULL, 就表示禁用缓冲区. 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main ()
{
char buffer[BUFSIZ];
FILE *pFile1, *pFile2;
pFile1=fopen ("myfile1.txt","w");
pFile2=fopen ("myfile2.txt","a");
setbuf ( pFile1 , buffer );
fputs ("This is sent to a buffered stream",pFile1);
fflush (pFile1);
setbuf ( pFile2 , NULL );
fputs ("This is sent to an unbuffered stream",pFile2);
fclose (pFile1);
fclose (pFile2);
return 0;
}
setvbuf
Specifies a buffer for stream. The function allows to specify the mode and size of the buffer (in bytes).
为文件指定一个缓冲区, 同时可以指定缓冲区的类型和大小, 下面是函数原型:1
int setvbuf ( FILE * stream, char * buffer, int mode, size_t size );
其中 stream表示操作的文件, buffer为指定的缓冲区首地址, 如果缓冲区为NULL, 系统会自动创建一个大小为size的缓冲区, mode为缓冲区的类别, size为缓冲区的大小, 其中mode的值可以为下面几个:
_IOFBF - 全缓冲(Full buffering), 当缓冲区满时才执行真正的I/O操作, 例如对磁盘文件的读写.
On output, data is written once the buffer is full (or flushed). On Input, the buffer is filled when an input operation is requested and the buffer is empty.
_IOLBF - 行缓冲(Line buffering), 在输入和输出时遇到换行符时才进行真正的I/O操作, 例如标准输入(stdin)和标准输出(stdout).
On output, data is written when a newline character is inserted into the stream or when the buffer is full (or flushed), whatever happens first. On Input, the buffer is filled up to the next newline character when an input operation is requested and the buffer is empty.
_IONBF - 无缓冲(No buffering), 在这种情况下buffer和size参数会被忽略.
其实setbuf将相当于调用了setvbuf1
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZE)
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
int main (){
FILE *pFile;
pFile=fopen ("myfile.txt","w");
setvbuf ( pFile , NULL , _IOFBF , 1024 );
// File operations here
fclose (pFile);
return 0;
}
对于输入输出流, 下列情况会自动刷新缓冲区
对于一个输出流, 可以调用fflush进行显示的刷新缓冲区, 即将缓冲区的内容写入到文件中, 但是对于一个输入流使用fflush函数的效果没有定义. 下面是函数原型:1
int fflush ( FILE * stream );
如果stream为NULL, 那么所有的缓冲区都将被刷新.
stat函数主要用于获取文件状态, 函数原型为1
int stat (const char *filename, struct stat *buf)
下面是struct stat的定义: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
28struct stat {
//device 文件的设备编号
dev_t st_dev;
//inode 文件的i-node
ino_t st_ino;
//protection 文件的类型和存取的权限
mode_t st_mode;
//number of hard links 连到该文件的硬连接数目, 刚建立的文件值为1.
nlink_t st_nlink;
//user ID of owner 文件所有者的用户识别码
uid_t st_uid;
//group ID of owner 文件所有者的组识别码
gid_t st_gid;
//device type 若此文件为装置设备文件, 则为其设备编号
dev_t st_rdev;
//total size, in bytes 文件大小, 以字节计算
off_t st_size;
//blocksize for filesystem I/O 文件系统的I/O 缓冲区大小.
unsigned long st_blksize;
//number of blocks allocated 占用文件区块的个数, 每一区块大小为512 个字节.
unsigned long st_blocks;
//time of lastaccess 文件最近一次被存取或被执行的时间, 一般只有在用mknod、utime、read、write 与tructate 时改变.
time_t st_atime;
//time of last modification 文件最后一次被修改的时间, 一般只有在用mknod、utime 和write 时才会改变
time_t st_mtime;
//time of last change i-node 最近一次被更改的时间, 此参数会在文件所有者、组、权限被更改时更新
time_t st_ctime;
};
其中st_mode定义了以下数种情况
下面是一个使用示例: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
26void file_stat(char * fileName) {
struct stat fileStat;
stat(fileName, &fileStat);
printf("Information for %s\n", fileName);
printf("---------------------------\n");
printf("File Size: \t\t%ld bytes\n",fileStat.st_size);
printf("Number of Links: \t%ld\n",fileStat.st_nlink);
printf("File inode: \t\t%ld\n",fileStat.st_ino);
printf("File Permissions: \t");
printf( (S_ISDIR(fileStat.st_mode)) ? "d" : "-");
printf( (fileStat.st_mode & S_IRUSR) ? "r" : "-");
printf( (fileStat.st_mode & S_IWUSR) ? "w" : "-");
printf( (fileStat.st_mode & S_IXUSR) ? "x" : "-");
printf( (fileStat.st_mode & S_IRGRP) ? "r" : "-");
printf( (fileStat.st_mode & S_IWGRP) ? "w" : "-");
printf( (fileStat.st_mode & S_IXGRP) ? "x" : "-");
printf( (fileStat.st_mode & S_IROTH) ? "r" : "-");
printf( (fileStat.st_mode & S_IWOTH) ? "w" : "-");
printf( (fileStat.st_mode & S_IXOTH) ? "x" : "-");
printf("\n\n");
printf("The file %s a symbolic link\n", (S_ISLNK(fileStat.st_mode)) ? "is" : "is not");
}
输出结果如下:1
2
3
4
5
6
7
8Information for data/test.txt
---------------------------
File Size: 24023896 bytes
Number of Links: 1
File inode: 8261278
File Permissions: -rw-rw-r--
The file is not a symbolic link
flush指令主要用于处理内存一致性问题. 每个处理器(processor)都有自己的本地(local)存储单元:寄存器和缓存, 当一个线程更新了共享变量之后, 新的值会首先存储到寄存器中, 然后更新到本地缓存中. 这些更新并非立刻就可以被其他线程得知, 因此在其它处理器中运行的线程不能访问这些存储单元. 如果一个线程不知道这些更新而使用共享变量的旧值就行运算, 就可能会得到错误的结果.
通过使用flush指令, 可以保证线程读取到的共享变量的最新值. 下面是语法形式:1
list指定需要flush的共享变量, 如果不指定list, 将flush作用于所有的共享变量. 在下面的几个位置已经隐式的添加了不指定list的flush指令.
threadprivate作用于全局变量, 用来指定该全局变量被各个线程各自复制一份私有的拷贝, 即各个线程具有各自私有、线程范围内的全局对象, 语法形式如下:1
其与private不同的时, threadprivate变量是存储在heap或者Thread local storage当中, 可以跨并行域访问, 而private绝大多数情况是存储在stack中, 只在当前并行域中访问, 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int counter;
void test_threadprivate() {
{
counter = omp_get_thread_num();
printf("1: thread %d : counter is %d\n", omp_get_thread_num(), counter);
}
printf("\n");
{
printf("2: thread %d : counter is %d\n", omp_get_thread_num(), counter);
}
}
下面是输出结果1
2
3
4
5
6
7
8
91: thread 3 : counter is 3
1: thread 0 : counter is 0
1: thread 2 : counter is 2
1: thread 1 : counter is 1
2: thread 2 : counter is 2
2: thread 0 : counter is 0
2: thread 3 : counter is 3
2: thread 1 : counter is 1
从输出结果我们可以看到, 在第二个并行域中, counter保存了在第一个并行域中的值. 如果要使两个并行域之间可以共享threadprivate变量的值, 需要满足以下几个条件:
用来控制并行域是串行执行还是并行执行, 只能作用于paralle指令, 下面是其语法形式:1
如果if的判断条件为true, 则并行执行, 否则串行执行, 下面是一个使用示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void test_if() {
int n = 1, tid;
printf("n = 1\n");
private(tid) shared(n)
{
tid = omp_get_thread_num();
printf("thread %d is running\n", tid);
}
printf("\n");
n = 10;
printf("n = 10\n");
private(tid) shared(n)
{
tid = omp_get_thread_num();
printf("thread %d is running\n", tid);
}
}
输出结果如下1
2
3
4
5
6
7
8n = 1
thread 0 is running
n = 10
thread 0 is running
thread 2 is running
thread 3 is running
thread 1 is running
如果利用循环, 将某项计算的所有结果进行求和(或者减、乘等其他操作)得出一个数值, 这在并行计算中十分常见, 通常将其称为规约. OpenMP提供了reduction子句由于规约操作, 其语法形式为1
reduction(operator:list)
下面是一个使用实例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void test_reduction() {
int sum, i;
int n = 100;
int a[n];
for(i = 0; i < n; i++) {
a[i] = i;
}
private(i) shared(a,n) reduction(+:sum)
for(i = 0; i < n; i++) {
sum += a[i];
}
printf("sum is %d\n", sum);
}
使用规约子句之后, 无需再对sum进行保护, 下面是reduction支持的操作符以及变量的初值
在使用乘法时发现其初始值同样为0, 可能和具体的实现有关.
将主线程中threadprivate变量的值复制到执行并行域的各个线程的threadprivate变量中, 作为各线程中threadprivate变量的初始值. 作用于parallel指令, 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13int counter = 10;
void test_copyin() {
printf("counter is %d\n", counter);
{
counter = omp_get_thread_num() + counter + 1;
printf(" thread %d : counter is %d\n", omp_get_thread_num(), counter);
}
printf("counter is %d\n", counter);
}
下面是输出结果:1
2
3
4
5
6counter is 10
thread 0 : counter is 11
thread 2 : counter is 13
thread 3 : counter is 14
thread 1 : counter is 12
counter is 11
将一个线程私有变量的值广播到执行同一并行域的其他线程. 只能作用于single指令, 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int counter = 10;
void test_copyprivate() {
int i;
{
{
i = 50;
counter = 100;
printf("thread %d execute single\n", omp_get_thread_num());
}
printf("thread %d: i is %d and counter is %d\n",omp_get_thread_num(), i, counter);
}
}
下面是程序运行结果:1
2
3
4
5thread 3 execute single
thread 2: i is 50 and counter is 100
thread 3: i is 50 and counter is 100
thread 0: i is 50 and counter is 100
thread 1: i is 50 and counter is 100
下面是将copyprivate(i, counter)去掉的运行结果1
2
3
4
5thread 0 execute single
thread 2: i is 0 and counter is 10
thread 0: i is 50 and counter is 100
thread 3: i is 0 and counter is 10
thread 1: i is 32750 and counter is 10
OpenMP标准定义了内部控制变量(internal control variables), 这些变量可以影响程序运行时的行为, 但是它们不能被直接访问或者修改, 我们需要通过OpenMP函数或者环境变量来访问或者修改它们, 下面是被定义的内部变量
我们可以通过以下几种方式来设置线程数量
OMP_NUM_THREADS
我们可以在命令行(command line)下设置OMP_NUM_THREADS环境变量的值, 而该变量的值用于初始化 nthread-var 变量.
omp_set_num_threads
在程序中我们可以使用omp_set_num_threads函数来设置线程数量, 语法形式为omp_set_num_threads(integer)
num_threads
最后我们可以在构造并行域的时候使用num_threads子句来控制线程的数量
上面的三种方式优先级依次递增, 另外在程序执行时, 我们可以使用下面几个函数获得线程的数量信息
下面是一个使用示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void test_numthread() {
printf("max thread nums is %d\n", omp_get_max_threads());
printf("omp_get_num_threads: out parallel region is %d\n", omp_get_num_threads());
omp_set_num_threads(2);
printf("after omp_set_num_threads: max thread nums is %d\n", omp_get_max_threads());
{
{
printf("omp_get_num_threads: in parallel region is %d\n\n", omp_get_num_threads());
}
printf("1: thread %d is running\n", omp_get_thread_num());
}
printf("\n");
{
printf("2: thread %d is running\n", omp_get_thread_num());
}
}
下面是程序运行结果:1
2
3
4
5
6
7
8
9
10
11max thread nums is 4
omp_get_num_threads: out parallel region is 1
after omp_set_num_threads: max thread nums is 2
omp_get_num_threads: in parallel region is 2
1: thread 0 is running
1: thread 1 is running
2: thread 0 is running
2: thread 1 is running
2: thread 2 is running
dyn-var控制程序是否在运行中是都可以动态的调整线程的数量, 可以通过下面的两种方式来设置
OMP_DYNAMIC
通过OMP_DYNAMIC环境变量来控制, 如果设为true, 则代表允许动态调整, 设为false则不可以
omp_set_dynamic
通过omp_set_dynamic函数, omp_set_dynamic(1)表示允许, omp_set_dynamic(0)表示不可以, 注意omp_set_dynamic可以传入其他非负整数, 但是作用和输入1是相同的, 都是表示true.
可以通过omp_get_dynamic来获得dynamic的状态, 返回值为0和1, 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void test_dynamic() {
printf("dynamic state is %d\n", omp_get_dynamic());
omp_set_num_threads(6);
{
printf("thread %d is running\n", omp_get_thread_num());
}
omp_set_dynamic(1);
printf("\n");
printf("dynamic state is %d\n", omp_get_dynamic());
{
printf("thread %d is running\n", omp_get_thread_num());
}
}
下面是输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13dynamic state is 0
thread 3 is running
thread 4 is running
thread 0 is running
thread 5 is running
thread 1 is running
thread 2 is running
dynamic state is 1
thread 3 is running
thread 1 is running
thread 2 is running
thread 0 is running
当允许动态调整之后, 第二个for循环只打印了四次,即只有四个线程在执行. 一般来说动态调整会根据系统资源来确定线程数量, 大多数情况下会生成和CPU数目相同的线程. 还有一点, 动态调整时生成的线程不会超过当前运行环境所允许的最大线程数量, 在上面的代码中, 如果将omp_set_num_threads(6)
改为omp_set_num_threads(2)
, 那么动态调整时最多只会生成两个线程.
nest-var用来控制是否可以嵌套并行, 可以通过下面两种方式来设置
OMP_NESTED
通过设置OMP_NESTED环境变量, true表示允许, false表示不允许
omp_set_nested
通过omp_set_nested函数, omp_set_nested(1或其他非负整数)表示允许, omp_set_nested(0)表示不允许.
可以通过omp_get_nested来获得是否可以嵌套并行, 返回值是0或1, 下面是一个使用示例: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
30void test_nested() {
int tid;
printf("nested state is %d\n", omp_get_nested());
{
tid = omp_get_thread_num();
printf("In outer parallel region: thread %d is running\n", tid);
{
printf("In nested parallel region: thread %d is running and outer thread is %d\n", omp_get_thread_num(), tid);
}
}
omp_set_nested(1);
printf("\n");
printf("nested state is %d\n", omp_get_nested());
{
tid = omp_get_thread_num();
printf("In outer parallel region: thread %d is running\n", tid);
{
printf("In nested parallel region: thread %d is running and outer thread is %d\n", omp_get_thread_num(), tid);
}
}
}
下面是程序运行结果:1
2
3
4
5
6
7
8
9
10
11
12
13nested state is 0
In outer parallel region: thread 0 is running
In nested parallel region: thread 0 is running and outer thread is 0
In outer parallel region: thread 1 is running
In nested parallel region: thread 0 is running and outer thread is 1
nested state is 1
In outer parallel region: thread 1 is running
In outer parallel region: thread 0 is running
In nested parallel region: thread 0 is running and outer thread is 0
In nested parallel region: thread 0 is running and outer thread is 1
In nested parallel region: thread 1 is running and outer thread is 1
In nested parallel region: thread 1 is running and outer thread is 0
当不允许嵌套并行时, 在并行域内创建的新并行域会以单线程执行, 而允许嵌套并行之后, 会在并行域内创建新的并行域, 为其分配新的线程执行.
通过OMP_SCHEDULE环境变量, 可以设置循环调度为runtime时的调度类型, 具体参见这里
omp_get_num_procs
获得程序中可以使用的处理器数量, 是一个全局的值
omp_in_parallel
判断是否在一个活跃的并行域(active parallel region)内, 返回0或1.
该应用主要目的是为了在使用linux系统的时候,实现手机和电脑之间的文件传输。前台界面使用的是angular-filemanager, 后台使用的是Spring MVC, 可以下载该应用的war包, 放到tomcat中使用。程序对angular-filemanager的原始功能进行了精简, 同时做了一些更改。下面该应用的具体功能:
下面是该应用的一些截图
前台使用的是使用angularjs + bootstrap写的一个在线文件管理系统, 这里是github地址, 后台作者已经给出了php和servlet的实现, 之所以使用Spring MVC重写后台,主要是为了熟悉一下Spring MVC, 同时精简了该管理系统的一些功能,因为主要目的是在linux系统下为手机和电脑之间的文件传输提供一个中介,当然也可以在windows系统下使用,也可以将该应用作为一个局域网中的一个文件共享系统。下面主要介绍在更改界面时的遇到的一些问题。
进行文件上传的插件有很多,比如bootstrap-fileinput 和 jQuery-File-Upload,不过这里使用的是jquery-upload-file,因为感觉比上面两种更加简单,下面是一个使用示例: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<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jquery-upload-file demo</title>
<link href="http://7xqp2l.com1.z0.glb.clouddn.com/uploadfile.css" rel="stylesheet">
<script src="http://7xqp2l.com1.z0.glb.clouddn.com/jquery-1.11.2.min.js" type="text/javascript"></script>
<script src="http://7xqp2l.com1.z0.glb.clouddn.com/jquery.uploadfile.js"></script>
<style type="text/css">
#content {
width: 600px;
margin: 0 auto;
}
.ajax-file-upload-filename {
width: 590px;
}
</style>
</head>
<body>
<div id="content">
<div id="fileuploader">Upload</div>
<button id="extrabutton" class="ajax-file-upload-green">上传</button>
</div>
<script>
$(document).ready(function () {
var extraObj = $("#fileuploader").uploadFile({
url: "uploadFile",
fileName: "file",
showFileSize: true,
showDelete: true,
autoSubmit: false,
statusBarWidth: 590,
dragdropWidth: 590,
dragdropHeight: 200,
uploadStr: "选择",
cancelStr: "取消",
"abortStr": "终止",
"deleteStr": "删除",
dynamicFormData: function () {
var data = {"param": ""};
return data;
},
onSuccess: function (files, data, xhr, pd) {
var obj = eval(data);
console.log(obj);
}
});
$("#extrabutton").click(function()
{
extraObj.startUpload();
});
});
</script>
</body>
</html>
下面是效果
在线演示(只是界面)
下面说几个选项:
url
- 文件上传地址,相当于<form>
的action
属性fileName
- 文件上传的name
属性,相当于<input type='file' name='file'>
中的name
dynamicFormData
- 提供动态的表单数据,格式为{"key": "value"}
onSuccess
- 文件上传成功的回调函数更多的选项和参数可以参考官方文档
我们使用jquery-upload-file插件代替了系统中原来的上传界面,关于angularjs和jquery插件的整合可以参考Angularjs集成第三方js插件之Uploadify,下面说明如何讲angularjs和jquery-upload-file整合
1 | app.directive("jqueryUpload", ["fileNavigator", function (fileNavigator) { |
其中["fileNavigator", function (fileNavigator)
表示将fileNavigator注入进来以便使用。extraObj
是预定义的一个全局变量,因为实在没有搞清楚angularjs的全局变量如何定义使用,所以直接在index.html中定义了该变量1
2
3<script type="text/javascript">
var extraObj;
</script>
定义好了directive之后,使用十分简单,如下所示1
<div id="fileuploader" jquery-upload="" >选择</div>
其中jquery-upload
和directive中的jqueryUpload
相对应。
bootstrap对弹出框进行了封装,使用起来十分方便,下面是一个使用示例: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<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>bootstrap modal demo</title>
<link href="http://7xqp2l.com1.z0.glb.clouddn.com/bootstrap-v3.3.4-bootstrap.min.css" rel="stylesheet">
<script src="http://7xqp2l.com1.z0.glb.clouddn.com/jquery-1.11.2.min.js" type="text/javascript"></script>
<script src="http://7xqp2l.com1.z0.glb.clouddn.com/bootstrap-v3.3.4-bootstrap.min.js"></script>
</head>
<body>
<button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myModal">通过data-target打开弹窗</button>
<button class="btn btn-primary btn-lg" onclick="openDialog();">通过js打开弹窗</button>
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">标题</h4>
</div>
<div class="modal-body">这里是内容</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary">提交</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal -->
</div>
<script type="text/javascript">
function openDialog() {
$('#myModal').modal();
}
</script>
</body>
</html>
有两种方式可以操作弹窗
上面代码演示了这两种方式,在线演示
二维码生成插件使用的jquery-qrcode,这里是github地址,下面是一个示例代码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<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jquery 二维码生成插件</title>
<script src="http://7xqp2l.com1.z0.glb.clouddn.com/jquery-1.11.2.min.js" type="text/javascript"></script>
<script src="http://7xqp2l.com1.z0.glb.clouddn.com/jquery.qrcode.min.js"></script>
<style type="text/css">
#qrcode1{
float: left;
margin-right: 20px;
}
#qrcode2{
float: left;
}
</style>
</head>
<body>
<div id="qrcode1"></div>
<div id="qrcode2"></div>
<script type="text/javascript">
$('#qrcode1').qrcode("http://zhangjikai.com");
$('#qrcode2').qrcode({width: 128,height: 128,text: "http://zhangjikai.com"});
</script>
</body>
</html>
下面是一个示例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<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jquery-upload-file demo</title>
<script src="http://7xqp2l.com1.z0.glb.clouddn.com/jquery-1.11.2.min.js" type="text/javascript"></script>
<style type="text/css">
#content {
width: 600px;
margin: 0 auto;
}
.red_color{
color: red;
}
</style>
</head>
<body>
<div id="content"></div>
<script>
var content = $("#content");
$("<h1 />", {
"text":"Hello World",
"class" : "red_color"
}).appendTo(content);
var ulObj = $("<ul />",{
"id" : "ulObj"
}).appendTo(content);
for(var i = 0; i < 4; i++) {
$("<li />", {
"text" : "这是项目" + i
}).appendTo(ulObj);
}
var btnObj = $("<button />", {
"text" : "点我删除列表",
"id" : "btnObj"
}).appendTo(content);
$("#btnObj").click(function(e) {
$("#ulObj").remove();
})
</script>
</body>
</html>
OpenMP通过在串行程序中插入编译制导指令, 来实现并行化, 支持OpenMP的编译器可以识别, 处理这些指令并实现对应的功能. 所有的编译制导指令都是以#pragma omp
开始, 后面跟具体的功能指令(directive)或者命令. 一般格式如下所示:1
2
structured block
为了使程序可以并行执行, 我们首先要构造一个并行域(parallel region), 在这里我们使用parallel
指令来实现并行域的构造, 其语法形式如下1
2
structured block
我们看到其实就是在omp后面加了一个parallel
关键字, 该指令主要作用就是用来构造并行域, 创建线程组并且并发执行任务. 需要注意的是该指令只保证代码以并行的方式执行, 但是并不负责线程之间的任务分发. 在并行域执行结束之后, 会有一个隐式的屏障(barrier), 来同步所有的该区域内的所有线程. 下面是一个使用示例:1
2
3
4
5
6void parallel_construct() {
{
printf("Hello from thread %d\n", omp_get_thread_num());
}
}
其中omp_get_thread_num()
用来获取当前线程的编号, 该函数是定义在<omp.h>
中的. 输出结果如下:1
2
3
4Hello from thread 1
Hello from thread 3
Hello from thread 0
Hello from thread 2
parallel指令后面可以跟一些子句(clause), 如下所示1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if(scalar-expression)
num_threads(integer-expression)
private(list)
firstprivate(list)
shared(list)
default(none | shared)
copyin(list)
reduction(operator:list)
在后面会介绍这些从句的用法
任务分担指令主要用于为线程分配不同的任务, 一个任务分担域(work-sharing region)必须要和一个活跃(active)的并行域(parellel region)关联, 如果任务分担指令处于一个不活跃的并行域或者处于一个串行域中, 那么该指令就会被忽略. 在C/C++有3个任务分担指令: for、sections、single, 严格意义上讲只有for和sections是任务分担指令, 而single只是协助任务分担的指令.
用于for循环中, 将不同的循环分配给不同的线程, 语法如下所示:1
2
for-loop
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11void parallel_for() {
int n = 9;
int i = 0;
{
for(i = 0; i < n; i++) {
printf("Thread %d executes loop iteration %d\n", omp_get_thread_num(),i);
}
}
}
下面是程序执行结果1
2
3
4
5
6
7
8
9Thread 2 executes loop iteration 5
Thread 2 executes loop iteration 6
Thread 3 executes loop iteration 7
Thread 3 executes loop iteration 8
Thread 0 executes loop iteration 0
Thread 0 executes loop iteration 1
Thread 0 executes loop iteration 2
Thread 1 executes loop iteration 3
Thread 1 executes loop iteration 4
在上面的程序中共有4个线程执行9次循环, 线程0分到了3次, 剩余的线程分到了2次, 这是一种常用的调度方式, 即假设有n次循环迭代, t个线程, 那么每个线程分配到n/t 或者 n/t + 1 次连续的迭代计算, 但是某些情况下使用这种方式并不是最好的选择, 我们可以使用schedule
来指定调度方式, 在后面会具体介绍. 下面是for 指令后面可以跟的一些子句:1
2
3
4
5
6
7
8
9
10
11
12
13private(list)
fistprivate(list)
lastprivate(list)
reduction(operator:list)
ordered
schedule(kind[,chunk_size])
nowait
sections指令可以为不同的线程分配不同的任务, 语法如下所示:1
2
3
4
5
6
7
8
{
[
structured block
[
structured block
...
}
从上面的代码中我们可以看到, sections将代码分为多个section, 每个线程处理一个section, 下面是一个使用示例: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/**
* 使用#pragma omp sections 和 #pragma omp section, 来使不同的线程执行不同的任务
* 如果线程数量大于section数量, 那么多余的线程会处于空闲状态(idle)
* 如果线程数量少于section数量, 那么一个线程会执行多个section代码
*/
void funcA() {
printf("In funcA: this section is executed by thread %d\n",
omp_get_thread_num());
}
void funcB() {
printf("In funcB: this section is executed by thread %d\n",
omp_get_thread_num());
}
void parallel_section() {
{
{
{
(void)funcA();
}
{
(void)funcB();
}
}
}
}
下面是执行结果:1
2In funcA: this section is executed by thread 3
In funcB: this section is executed by thread 0
下面是sections后面可以跟的一些子句1
2
3
4
5
6
7
8
9private(list)
firstprivate(list)
lastprivate(list)
reduction(operator:list)
nowait
single 指令用来指定某段代码块只能被一个线程来执行, 如果没有nowait字句, 所有线程在 single 指令结束处隐市同步点同步, 如果single指令有nowait从句, 则别的线程直接往下执行. 不过single指令并不指定哪个线程来执行. 语法如下所示:1
2
structured block
下面是一个使用示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24void parallel_single() {
int a = 0, n = 10, i;
int b[n];
{
// 只有一个线程会执行这段代码, 其他线程会等待该线程执行完毕
{
a = 10;
printf("Single construct executed by thread %d\n", omp_get_thread_num());
}
// A barrier is automatically inserted here
for(i = 0; i < n; i++) {
b[i] = a;
}
}
printf("After the parallel region:\n");
for (i=0; i<n; i++)
printf("b[%d] = %d\n",i,b[i]);
}
下面是执行结果:1
2
3
4
5
6
7
8
9
10
11
12Single construct executed by thread 2
After the parallel region:
b[0] = 10
b[1] = 10
b[2] = 10
b[3] = 10
b[4] = 10
b[5] = 10
b[6] = 10
b[7] = 10
b[8] = 10
b[9] = 10
下面是single指令后面可以跟随的子句:1
2
3
4
5
6
7private(list)
firstprivate(list)
copyprivate(list)
nowait
将parallel指令和work-sharing指令结合起来, 使代码更加简洁. 如下面的代码1
2
3
4
5
{
for(.....)
}
可以写为1
2
for(.....)
具体的参见下图:
使用这些组合结构体(combined constructs)不仅增加程序的可读性, 而且对程序的性能有一定的帮助. 当使用这些组合结构体的时候, 编译器可以知道下一步要做什么, 从而可能会生成更高效的代码.
OpenMP指令后面可以跟一些子句, 用来控制构造器的行为. 下面介绍一些常用的子句.
shared子句用来指定哪些数据是在线程之间共享的, 语法形式为shared(list)
, 下面是其使用方法:1
2
3
4
5
for(i = 0; i < n; i++)
{
a[i] += i;
}
在并行域中使用共享变量时, 如果存在写操作, 需要对共享变量加以保存, 因为可能存在多个线程同时修改共享变量或者在一个线程读取共享变量时另外一个变量在更新共享变量的情况, 而这些情况都可能会引起程序错误.
private子句用来指定哪些数据是线程私有的, 即每个线程具有变量的私有副本, 线程之间互不影响. 其语法形式为private(list)
, 使用方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14void test_private() {
int n = 8;
int i=2, a = 3;
// i,a 定义为private之后不改变原先的值
for ( i = 0; i<n; i++)
{
a = i+1;
printf("In for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(),a,i);
}
printf("\n");
printf("Out for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(),a,i);
}
下面是程序运行结果:1
2
3
4
5
6
7
8
9
10In for: thread 2 has a value of a = 5 for i = 4
In for: thread 2 has a value of a = 6 for i = 5
In for: thread 3 has a value of a = 7 for i = 6
In for: thread 3 has a value of a = 8 for i = 7
In for: thread 0 has a value of a = 1 for i = 0
In for: thread 0 has a value of a = 2 for i = 1
In for: thread 1 has a value of a = 3 for i = 2
In for: thread 1 has a value of a = 4 for i = 3
Out for: thread 0 has a value of a = 3 for i = 2
对于private子句中的变量, 需要注意一下两点:
lastprivate会在退出并行域时, 将其修饰变量的最后取值(last value)保存下来, 可以作用于 for
和 sections
, 语法格式为lastprivate(list)
. 关于last value的定义: 如果是作用于for指令, 那么last value就是指串行执行的最后一次循环的值;如果是作用于sections指令, 那么last value就是执行完最后一个包含该变量的section之后的值. 使用方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14void test_last_private() {
int n = 8;
int i=2, a = 3;
// lastprivate 将for中最后一次循环(i == n-1) a 的值赋给a
for ( i = 0; i<n; i++)
{
a = i+1;
printf("In for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(),a,i);
}
printf("\n");
printf("Out for: thread %d has a value of a = %d for i = %d\n", omp_get_thread_num(),a,i);
}
程序执行结果为:1
2
3
4
5
6
7
8
9
10In for: thread 3 has a value of a = 7 for i = 6
In for: thread 3 has a value of a = 8 for i = 7
In for: thread 2 has a value of a = 5 for i = 4
In for: thread 2 has a value of a = 6 for i = 5
In for: thread 1 has a value of a = 3 for i = 2
In for: thread 0 has a value of a = 1 for i = 0
In for: thread 0 has a value of a = 2 for i = 1
In for: thread 1 has a value of a = 4 for i = 3
Out for: thread 0 has a value of a = 8 for i = 2
firstprivate 子句用于为private变量提供初始值. 使用firstprivate修饰的变量会使用在前面定义的同名变量的值作为其初始值. 语法形式为firstprivate(list)
, 使用方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13void test_first_private() {
int n = 8;
int i=0, a[n];
for(i = 0; i < n ;i++) {
a[i] = i+1;
}
for ( i = 0; i<n; i++)
{
printf("thread %d: a[%d] is %d\n", omp_get_thread_num(), i, a[i]);
}
}
执行结果如下:1
2
3
4
5
6
7
8thread 0: a[0] is 1
thread 0: a[1] is 2
thread 2: a[4] is 5
thread 2: a[5] is 6
thread 3: a[6] is 7
thread 3: a[7] is 8
thread 1: a[2] is 3
thread 1: a[3] is 4
default子句用于设置变量默认的data-sharing属性, 在C/C++中只支持default(none | shared)
, 其中default(shared)
设置所有的变量默认为共享的, default(none)
取消变量的默认属性, 需要显示指定变量是共享的还是私有的.
用于取消任务分担结构(work-sharing constructs)中的隐式屏障(implicit barrier), 下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void test_nowait() {
int i, n =6;
{
for(i = 0; i < n; i++) {
printf("thread %d: ++++\n", omp_get_thread_num());
}
for(i = 0; i < n; i++) {
printf("thread %d: ----\n", omp_get_thread_num());
}
}
}
如果第一个 for 后面没有加 nowait , 那么输出如下所示:1
2
3
4
5
6
7
8
9
10
11
12thread 3: ++++
thread 0: ++++
thread 0: ++++
thread 2: ++++
thread 1: ++++
thread 1: ++++
thread 0: ----
thread 0: ----
thread 3: ----
thread 1: ----
thread 1: ----
thread 2: ----
因为for指令有一个隐式的屏障, 会同步所有的线程直到第一个for循环执行完, 再继续往下执行. 加上 nowait 之后就消除了这个屏障, 使线程执行完第一个for循环之后无需再等待其他线程就可以去执行第二个for循环的内容, 下面是加上nowait之后的输出:1
2
3
4
5
6
7
8
9
10
11
12thread 2: ++++
thread 2: ----
thread 1: ++++
thread 1: ++++
thread 1: ----
thread 1: ----
thread 3: ++++
thread 3: ----
thread 0: ++++
thread 0: ++++
thread 0: ----
thread 0: ----
使用nowait时需要注意前后for之间有没有依赖关系, 如果第二个for循环需要用到第一个for循环的结果, 那么使用nowait就可能会造成程序错误.
schedule子句只作用于循环结构(loop construct), 它用来设置循环任务的调度方式. 语法形式为schedule(kind[,chunk_size])
, 其中kind的取值有 static
, dynamic
, guided
, auto
, runtime
, chunk_size是可选项,可以指定也可以不指定. 下面是使用方法:1
2
3
4
5
6
7
8
9void test_schedule() {
int i, n = 10;
private(i) shared(n)
for(i = 0; i < n; i++) {
printf("Iteration %d executed by thread %d\n", i, omp_get_thread_num());
}
}
下面介绍一下各个取值的含义, 假设有n次循环, t个线程
static
静态调度, 如果不指定chunk_size , 那么会为每个线程分配 n/t 或者 n/t+1(不能除尽)次连续的迭代计算, 如果指定了 chunk_size, 那么每次为线程分配chunk_size次迭代计算, 如果第一轮没有分配完, 则循环进行下一轮分配, 假设n=8, t=4, 下表给出了chunk_size未指定、等于1、等于3时的分配情况.
线程编号\chunk_size | 未指定 | chunk_size = 1 | chunk_size = 3 |
---|---|---|---|
0 | 0 1 | 0 4 | 0 1 2 |
1 | 2 3 | 1 5 | 3 4 5 |
2 | 4 5 | 2 6 | 6 7 |
3 | 6 7 | 3 7 |
dynamic
动态调度, 动态为线程分配迭代计算, 只要线程空闲就为其分配任务, 计算快的线程分配到更多的迭代. 如果不指定chunk_size参数, 则每次为一个线程分配一次迭代循环(相当于chunk_size=1), 若指定chunk_size, 则每次为一个线程分配chunk_size次迭代循环. 在动态调度下, 分配结果是不固定的, 重复执行同一个程序, 每次的分配结果一般来说是不同的, 下面给出n=12, t=4时, chunk_size未指定、等于2时的分配情况(运行两次)
线程编号\chunk_size | 未指定(第一次) | 未指定(第二次) | chunk_size=2(第一次) | chunk_size = 2(第二次) |
---|---|---|---|---|
0 | 2 | 0 | 4 5 8 9 10 11 | 0 1 |
1 | 0 4 5 6 7 8 9 10 11 | 3 | 0 1 | 4 5 |
2 | 3 | 1 4 5 6 7 8 9 10 11 | 2 3 | 6 7 |
3 | 1 | 2 | 6 7 | 2 3 8 9 10 11 |
使用动态动态可以一定程度减少负载不均衡的问题, 但是需要注意任务动态申请时也会有一定的开销.
guided
guided调度是一种指定性的启发式自调度方法. 开始时每个线程会分配到较大的迭代块, 之后分配到的迭代块的大小会逐渐递减. 如果指定chunk_size, 则迭代块会按指数级下降到指定的chunk_size大小, 如果没有指定size参数, 那么迭代块大小最小会降到1(相当于chunk_size=1). 和动态调度一样, 执行块的线程会分到更多的任务, 不同的是这里迭代块的大小是变化的. 同样使用guided调度的分配结果也不是固定的, 重复执行会得到不同的分配结果. 下面给出n=20, t=4, chunk_size未指定、chunk_size=3时的分配情况(执行两次)
线程编号\chunk_size | 未指定(第一次) | 未指定(第二次) | chunk_size=3(第一次) | chunk_size = 3(第二次) |
---|---|---|---|---|
0 | 12 13 | 0 1 2 3 4 | 0 1 2 3 4 | 5 6 7 8 18 19 |
1 | 5 6 7 8 16 17 18 19 | 5 6 7 8 | 9 10 11 | 9 10 11 |
2 | 0 1 2 3 4 14 15 | 9 10 11 14 15 16 17 18 19 | 5 6 7 8 15 16 17 18 19 | 0 1 2 3 4 15 16 17 |
3 | 9 10 11 | 12 13 | 12 13 14 | 12 13 14 |
当设置chunk_size=3时, 因为最后只剩下18、19两次循环, 所以最后执行的那个线程只分配到2次循环.
下面的图展示了当循环次数为200次, 线程数量为4时, static 、 (dynamic,7) 、(guided, 7) 3种调度方式的分配情况
runtime
运行时调度, 并不是一种真正的调度方式, 在运行时同时环境变量OMP_SCHEDULE来确定调度类型, 最终的调度类型仍为上面的3种调度方式之一. 在bash下可以使用下面的方式设置:1
export OMP_SCHEDULE="static"
auto
将选择的权利赋予编译器, 让编译器自己选择合适的调度决策.
在for循环中, 如果每次循环之间花费的时间是不同的, 那么就可能出现负载不均衡问题, 下面代码模拟一下这种情况,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
65
66
67
68
69
70
71void test_schedule() {
int i,j, n = 10;
double start, end;
GET_TIME(start);
private(i, j) shared(n)
for(i = 0; i < n; i++) {
//printf("Iteration %d executed by thread %d\n", i, omp_get_thread_num());
for(j = 0; j < i; j++) {
system("sleep 0.1");
}
}
GET_TIME(end);
printf("static : use time %.2fs\n", end-start);
GET_TIME(start);
private(i, j) shared(n)
for(i = 0; i < n; i++) {
for(j = 0; j < i; j++) {
system("sleep 0.1");
}
}
GET_TIME(end);
printf("static,2 : use time %.2fs\n", end-start);
GET_TIME(start);
private(i, j) shared(n)
for(i = 0; i < n; i++) {
for(j = 0; j < i; j++) {
system("sleep 0.1");
}
}
GET_TIME(end);
printf("dynamic : use time %.2fs\n", end-start);
GET_TIME(start);
private(i, j) shared(n)
for(i = 0; i < n; i++) {
for(j = 0; j < i; j++) {
system("sleep 0.1");
}
}
GET_TIME(end);
printf("dynamic,2: use time %.2fs\n", end-start);
GET_TIME(start);
private(i, j) shared(n)
for(i = 0; i < n; i++) {
for(j = 0; j < i; j++) {
system("sleep 0.1");
}
}
GET_TIME(end);
printf("guided : use time %.2fs\n", end-start);
GET_TIME(start);
private(i, j) shared(n)
for(i = 0; i < n; i++) {
for(j = 0; j < i; j++) {
system("sleep 0.1");
}
}
GET_TIME(end);
printf("guided,2 : use time %.2fs\n", end-start);
}
GET_TIME的定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct timeval t; \
gettimeofday(&t, NULL); \
now = t.tv_sec + t.tv_usec/1000000.0; \
}
在上面的代码中, 对于第一个for循环, i越大, 循环消耗的时间越多, 下面是n=10时的输出1
2
3
4
5
6static : use time 1.74s
static,2 : use time 1.84s
dynamic : use time 1.53s
dynamic,2: use time 1.84s
guided : use time 1.63s
guided,2 : use time 1.53s
下面是n=20的输出1
2
3
4
5
6static : use time 8.67s
static,2 : use time 6.42s
dynamic : use time 5.62s
dynamic,2: use time 6.43s
guided : use time 5.92s
guided,2 : use time 6.43s
对于static调度, 如果不指定chunk_size的值, 则会将最后几次循环分给最后一个线程, 而最后几次循环是最耗时的, 其他线程执行完各自的工作需要等待这个线程执行完毕, 浪费了系统资源, 这就造成了负载不均衡. dynamic和guided可以在一定程度上减轻负载不均衡, 但是也不是绝对的, 最终选用哪种方式还是要根据具体的问题.
同步指令主要用来控制多个线程之间对于共享变量的访问. 它可以保证线程以一定的顺序更新共享变量, 或者保证两个或多个线程不同时修改共享变量.
同步路障(barrier), 当线程遇到路障时必须要停下等待, 直到并行区域中的所有线程都到达路障点, 线程才继续往下执行. 在每一个并行域和任务分担域的结束处都会有一个隐式的同步路障, 即在parallel、for、sections、single构造的区域之后会有一个隐式的路障, 因此在很多时候我们无需显示的插入路障. 下面是语法形式:1
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void print_time(int tid, char* s ) {
int len = 10;
char buf[len];
NOW_TIME(buf, len);
printf("Thread %d %s at %s\n", tid, s, buf);
}
void test_barrier() {
int tid;
{
tid = omp_get_thread_num();
if(tid < omp_get_num_threads() / 2)
system("sleep 3");
print_time(tid, "before barrier ");
print_time(tid, "after barrier ");
}
}
其中NOW_TIME的定义如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
time_t nowtime; \
nowtime = time(NULL); \
struct tm *local; \
local = localtime(&nowtime); \
strftime(buf, len, "%H:%M:%S", local); \
}
在上面的代码中有一半的线程(tid < 2) 会睡眠3秒之后再继续往下执行, 首先看看不加路障的输出, 即去掉#pragma omp barrier
1
2
3
4
5
6
7
8Thread 3 before barrier at 16:55:44
Thread 2 before barrier at 16:55:44
Thread 3 after barrier at 16:55:44
Thread 2 after barrier at 16:55:44
Thread 1 before barrier at 16:55:47
Thread 0 before barrier at 16:55:47
Thread 0 after barrier at 16:55:47
Thread 1 after barrier at 16:55:47
下面上加上路障的输出结果:1
2
3
4
5
6
7
8Thread 3 before barrier at 17:05:29
Thread 2 before barrier at 17:05:29
Thread 0 before barrier at 17:05:32
Thread 1 before barrier at 17:05:32
Thread 0 after barrier at 17:05:32
Thread 1 after barrier at 17:05:32
Thread 2 after barrier at 17:05:32
Thread 3 after barrier at 17:05:32
通过对比我们可以看出, 加上路障之后, 各线程要在路障点同步一次, 然后再继续往下执行.
ordered结构允许在并行域中以串行的顺序执行一段代码, 如果我们在并行域中想按照顺序打印被不同的线程计算的数据, 就可以使用这个子句, 下面是语法形式1
2
structured block
在使用时需要注意一下两点
1 |
下面是一个使用示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void test_order() {
int i, tid, n = 5;
int a[n];
for(i = 0; i < n; i++) {
a[i] = 0;
}
private (i, tid) shared(n, a)
for(i = 0; i < n; i++) {
tid = omp_get_thread_num();
printf("Thread %d updates a[%d]\n", tid, i);
a[i] += i;
{
printf("Thread %d printf value of a[%d] = %d\n", tid, i, a[i]);
}
}
}
下面是程序运行结果:1
2
3
4
5
6
7
8
9
10Thread 0 updates a[0]
Thread 2 updates a[2]
Thread 1 updates a[3]
Thread 0 printf value of a[0] = 0
Thread 0 updates a[4]
Thread 3 updates a[1]
Thread 3 printf value of a[1] = 1
Thread 2 printf value of a[2] = 2
Thread 1 printf value of a[3] = 3
Thread 0 printf value of a[4] = 4
从输出结果我们可以看到, 在update时是以乱序的顺序更新, 但是在打印时是以串行顺序的形式打印.
临界区(critical), 临界区保证在任意一个时间段内只有一个线程执行该区域中的代码, 一个线程要进入临界区必须要等待临界区处于空闲状态, 下面是语法形式1
2
structured block
其中name是为临界区指定的一个名字. 下面是一个求和的使用示例, 注意这里只是用来说明临界区的作用, 对于求和操作我们可以使用reduction指令1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25void test_critical() {
int n = 100, sum = 0, sumLocal, i, tid;
int a[n];
for(i = 0; i < n; i++) {
a[i] = i;
}
{
tid = omp_get_thread_num();
sumLocal = 0;
for(i = 0; i < n; i++) {
sumLocal += a[i];
}
{
sum += sumLocal;
printf("Thread %d: sumLocal = %d sum =%d\n", tid, sumLocal, sum);
}
}
printf("Value of sum after parallel region: %d\n",sum);
}
在该代码中, sum是全局的, localSum是每个线程执行完各自的求和任务后的和值, 将每个线程的sumLocal加给sum, 就是最后的和值. 在执行sum+=sunLocal
操作时, 需要保证一次只有一个线程执行该操作, 因此这里使用了临界区, 下面是运行结果:1
2
3
4
5Thread 2: sumLocal = 1550 sum =1550
Thread 3: sumLocal = 2175 sum =3725
Thread 1: sumLocal = 925 sum =4650
Thread 0: sumLocal = 300 sum =4950
Value of sum after parallel region: 4950
下面是将临界区去掉的运行结果(运行结果不是固定的, 这里只是其中一种情况):1
2
3
4
5Thread 2: sumLocal = 1550 sum =1550
Thread 3: sumLocal = 2175 sum =2475
Thread 1: sumLocal = 925 sum =925
Thread 0: sumLocal = 300 sum =300
Value of sum after parallel region: 2475
通过对比我们可以看到临界区保证了程序的正确性.
原子操作, 可以锁定一个特殊的存储单元(可以是一个单独的变量,也可以是数组元素), 使得该存储单元只能原子的更新, 而不允许让多个线程同时去写. atomic只能作用于单条赋值语句, 而不能作用于代码块. 语法形式为:1
2
statement
在C/C++中, statement必须是下列形式之一
x++, x--, ++x, --x
x binop= expr
其中binop是二元操作符: +, -, *, /, &, ^, |, <<, >>
之一atomic的可以有效的利用的硬件的原子操作机制来控制多个线程对共享变量的写操作, 效率较高, 下面是一个使用示例1
2
3
4
5
6
7
8
9
10
11void test_atomic() {
int counter=0, n = 1000000, i;
for(i = 0; i < n; i++) {
counter += 1;
}
printf("counter is %d\n", counter);
}
对于下面的情况1
2
ic += func();
atomic只保证ic的更新是原子的, 即不会被多个线程同时更新, 但是不会保证func函数的执行是原子的, 即多个线程可以同时执行func函数, 如果要使func的执行也是原子的, 可以使用临界区.
互斥锁, 提供了一个更底层的机制来处理同步的问题, 比使用critical和atomic有更多的灵活性, 但也相对更加复杂一些. openmp提供了两种类型的锁–简单锁(simple locks) 和 嵌套锁(nested locks), 对于简单锁来说, 如果其处于锁住的状态, 那么它就可能无法再次被上锁. 而对于嵌套锁来说, 可以被同一个线程上锁多次. 下面是简单锁的几个函数1
2
3
4
5void omp_init_lock(omp_lock_t *lck) // 初始化互斥锁
void omp_destroy_lock(omp_lock_t *lck) // 销毁互斥锁
void omp_set_lock(omp_lock_t *lck) // 获得互斥锁
void omp_unset_lock(omp_lock_t *lck) // 释放互斥锁
bool omp_test_lock(omp_lock_t *lck) // 尝试获得互斥锁, 如果获得成功返回true, 否则返回false
嵌套锁的函数和简单锁略有不同, 如下所示1
2
3
4
5void omp_init_nest_lock(omp_nest_lock_t *lck)
void omp_destroy_nest_lock(omp_nest_lock_t *lck)
void omp_set_nest_lock(omp_nest_lock_t *lck)
void omp_unset_nest_lock(omp_nest_lock_t *lck)
void omp_test_nest_lock(omp_nest_lock_t *lck)
下面是一个使用示例1
2
3
4
5
6
7
8
9
10
11
12
13
14void test_lock() {
omp_lock_t lock;
int i,n = 4;
omp_init_lock(&lock);
for(i = 0; i < n; i++) {
omp_set_lock(&lock);
printf("Thread %d: +\n", omp_get_thread_num());
system("sleep 0.1");
printf("Thread %d: -\n", omp_get_thread_num());
omp_unset_lock(&lock);
}
omp_destroy_lock(&lock);
}
其中system(“sleep 0.1”) 是为了两次的输出有个间隔, 以便和不加锁时的情况进行对比. 下面是程序的输出:1
2
3
4
5
6
7
8Thread 1: +
Thread 1: -
Thread 2: +
Thread 2: -
Thread 3: +
Thread 3: -
Thread 0: +
Thread 0: -
下面是去掉锁的输出1
2
3
4
5
6
7
8Thread 3: +
Thread 2: +
Thread 0: +
Thread 1: +
Thread 2: -
Thread 3: -
Thread 0: -
Thread 1: -
用于指定一段代码只由主线程执行. master指令和single指令的区别如下:
下面是一个使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void test_master() {
int a, i, n = 5;
int b[n];
{
{
a = 10;
printf("Master construct is executed by thread %d\n", omp_get_thread_num());
}
for(i = 0; i < n; i++)
b[i] = a;
}
printf("After the parallel region:\n");
for(i = 0; i < n; i++)
printf("b[%d] = %d\n", i, b[i]);
}
下面是输出结果1
2
3
4
5
6
7Master construct is executed by thread 0
After the parallel region:
b[0] = 10
b[1] = 10
b[2] = 10
b[3] = 10
b[4] = 10