0%

字节码操作

介绍

java的动态性的两种常见方式

  • 字节码操作
  • 反射

运行时操作字节码可以实现的功能如下:

  • 动态生成新的类

  • 动态改变某个类的结构(添加/删除/修改 新的属性)

优势:

  • 比反射开销小,性能高。

  • JAVAasist性能高于反射,低于ASM

常见的字节码操作类库

  • BCEL

    Byte Code Engineering Library (BCEL) ,这是 Apache Software Foundation 的 Jakarta 项目的一部分。 BCEL 是 Java classworking 广泛 使用的一种 框架 , 它 可以让您深入 JVM 汇编语言进行类操作的细节。 BCEL 与 Javassist 有不同的处理字节码方法, BCEL 在实际的 JVM 指令层次上进行操作 (BCEL 拥有丰富的 JVM 指令级支持 ) 而 Javassist 所 强调 的是源代码 级别的 工作 。

  • ASM

    是一个轻量级 java 字节码操作框架,直接涉及到 JVM 底层的操作和 指令

  • CGLIB

    是一个强大的,高性能,高质量的 Code

  • Javaassist

    性能较ASM差,跟cglib差不多,但是用用简单,很多框架都在使用

javassist

使用Javassist需要使用javassist.jar

优势:

比反射开销小,性能高。
JAVAsist性能高于反射,低于ASM

局限性:

JDK新语法不支持(包括泛型、枚举),不支持注解修改,但可以通过底层的javasist类来解决,具体参考:javassist.bytecode.annotaion
不支持数组的初始化,如 String[]{“1”,”2”},除非只有数组的容量为1
不支持内部类和匿名类
不支持 continue 和 break 表达式。
对于继承关系,有些不支持 。例如:- class A{} - class B extends A{} - class C enxends B {}

应用场景:

AOP:
给一个类增加新的方法
给一段语句前面和后面(before/after/around)动态的加代码
Reflection:起到类似反射的效果

javassist 的最外层的 API 和 JAVA 的反射包中的 API 颇为类似 。

它主要由 CtClass , CtMethod, ,以及 CtField 几个类组成。用以执行和 JDK 反射 API 中 java.lang.Class , java.lang.reflect.Method , java.lang.reflect.Method .Field 相同的 操作 。
方法操作

修改已有方法的方法体体(插入代码到已有方法体)
新增方法 删除方法
占位符参数介绍:

img

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
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
/**
* 创建一个新的类
* @author Administrator
*
*/
public class Test10 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault(); // 类池
CtClass class1 = pool.makeClass("com.gs.Emp");

//创建属性
CtField f1 = CtField.make("private int num;", class1);
CtField f2 = CtField.make("private String name;", class1);
class1.addField(f1);
class1.addField(f2);

//创建方法

CtMethod setName = CtMethod.make("public void setName(String name){this.name = name;}", class1);
CtMethod getName = CtMethod.make("public String getName(){return name;}", class1);
class1.addMethod(setName);
class1.addMethod(getName);

//添加构造器。 如果是带参构造器,需要传递参数类型,基本数据类型用CtClass获取,引用类型,需要用pool获取
CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType,pool.get("java.lang.String")}, class1);
constructor.setBody("{this.num = num;this.name = name;}"); //构造器的方法体

class1.writeFile("E:/myjava"); //将上面写好的类,写入到这个工作空间中

System.out.println("生成类成功!");
}
}

参考文章

Java 动态字节码技术 - 掘金 (juejin.cn)

脚本引擎

简介

脚本引擎就是一个计算机编程语言的解释器

JAVA脚本引擎是从JDK6.0之后添加的新功能。

脚本引擎介绍:

  • 使得Java应用程序可以通过一套固定的接口与各种脚本引擎交互,从而达到在Java平台上调用各种脚本语言的目的。
  • Java脚本API是连通Java平台和脚本语言的桥梁。
  • 可以把一些复杂异变的业务逻辑交给脚本语言处理,这又大大提高了开发效率

使用场景

在日常的Java项目中,我们免不了会遇到这样的需求:

  1. 动态地获取并运行自定义脚本文件,以实现特定的功能
  2. 对数据流执行用户自定义的数据有效性、公式计算、数据处理ETL(如数据截取、拼接)等不同业务逻辑
  3. 对用户输入的代码或脚本文件进行测试、运行,确保其语法正确、功能正常
  4. 处理需要灵活配置且不断变更的动态业务规则
  5. 代码的热更新、热修复

诸如此类的需求若采用硬编码实现,则迭代成本相当高,每次改动都需要进行开发、测试、部署。同时业务规则的频繁变更会导致代码的开发和维护成本大大提高

js语言简介

js属于的是解释性语言。

  • 支持动态类型,弱类型,在程序运行的时候才进行编译,效率不较低。

  • 不像编译性语言,源代码不能直接翻译成机器语言,先翻译成中间代码,再由解释器对中间代码进行解释运行。

  • 程序不需要编译,程序运行时才翻译成机器语言,每执行一次都要翻译一次。

  • 一般,编译性语言的运行效率比解释性语言更高;但是不能一概而论,部分解释性语言的解释器通过在运行时动态优化代码,甚至能使解释性语言的性能超过编译性语言;

简单的实现过程

查找脚本引擎

  1. 通过脚本名称获取:

    ScriptEngineManager:为ScriptEngine提供实例化机制。

      方法:getEngineByName(String shortName);//查找并创建指定名称的脚本引擎

    1
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); 
  2. 通过文件扩展名获取:

    ScriptEngine:是一个接口,该接口提供了基本的脚本功能(包括执行脚本,设置和获取值的方法)。

    1
    2
    3
    4
    5
    6
    7
    8
    ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");  

    方法:
    get(String key);//获取指定key所对应的值,这里的key看做变量名称,值看做变量名所对应的值。

         eval(String script);//执行指定的脚本代码

         eval(Reader reader);//执行指定的脚本文件
  3. 通过MIME类型获取:

    1
    ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");  

语言绑定

脚本语言支持API使用语言绑定对象实现Java语言编写的程序与脚本语言间的数据传递。语言绑定对象实际上就是一个简单的哈希表,用来存放和获取需要共享的数据,其定义的接口为javax.script.Bindings,继承自java.util.Map接口。一个脚本引擎在执行过程中可能会使用多个语言绑定对象,不同语言绑定对象的作用域不同。ScriptEngine类提供out和get方法对脚本引擎中特定作用域的默认语言绑定对象进行操作。

使用默认的语言绑定对象:

1
2
3
4
5
6
7
8
public void useDefaultBinding() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
engine.put("name", "Alex");
engine.eval("var message = 'Hello, ' + name;"); //执行指定的脚本代码
engine.eval("println(message);");
Object obj = engine.get("message");
System.out.println(obj);
}

亦可以自定义语言绑定对象(如语言绑定对象中包含程序自己独有的数据等情形……):

1
2
3
4
5
6
public void useCustomBinding() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
Bindings bindings = new SimpleBindings();
bindings.put("hobby", "playing games");
engine.eval("println('I like ' + hobby);", bindings);
}

脚本引擎执行脚本方法

一、

  1. 执行脚本方法,首先肯定是要在脚本中定义方法,然后执行。
  2. 执行时需要将engine转换为Invcable,然后调用其中的invokeFun

  Invocable:由ScriptEngines实现的可选接口,其方法允许调用先前已执行的脚本中的程序(方法)。

  1. 调用Object invokeFunction(String name, Object… args);执行指定方法。

  name为方法名称,args为方法参数,返回是一个Object对象 

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
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;


public class TestRhino {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript");
//定义方法的字符串形式
String funAdd = "function add(a,b){"
+ " var sum = a + b;"
+ " return sum;"
+ " }";
engine.eval(funAdd);//这一步可以看做将方法写入脚本
Invocable invo = (Invocable)engine;//转换成Invocable
Object result = null;
try {
//执行脚本中方法
result = invo.invokeFunction("add",new Object[]{17,23});
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(result);
}
}

二、

  1. 执行脚本文件,首先我们要指定文件的路径。
  2. 通过这个路径构建一个Reader对象
  3. 调用eval(Reader reader);//执行指定的脚本文件
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
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;


public class TestRhino {
public static void main(String[] args) throws ScriptException {
//E:\eclipse\Rhino
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript");
//脚本文件路径
String path = "E:\\eclipse\\Rhino\\test.js";//
Reader read = null;
try {
//通过脚本文件路径构造Reader对象
read = new FileReader(new File(path));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//执行脚本文件
engine.eval(read);
}
}

脚本执行的上下文

脚本引擎通过执行过程中的上下文对象获取与脚本执行相关的信息,同时允许程序员通过此对象配置脚本引擎的行为。其上下文对象来自javax.script.ScriptContext接口,类似于J2EE中javax.servlet.ServletContext接口,该接口主要包含3类信息:

输入输出

默认情况下,脚本输入输出都是在标准控制台中,可以通过setReader和setWriter方法对输出流进行重定向,可以通过setErrorWriter方法进行错误输出重定向。

1
2
3
4
5
6
7
//例:将输出重定向到文件  
public void scriptToFile() throws IOException, ScriptException {
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
context.setWriter(new FileWriter("output.txt"));
engine.eval("println('Hello World!');");
}

自定义属性

上下文中通过setAttribute和getAttribute方法获取和设置属性,类似于ServletContext中设置和获取属性操作。与ServletContext中不同的是,ScriptContext中的属性是有作用域之分的,ScriptContext按不同的顺序在不同的作用域中进行属性查找(类似于JSP中EL表达式属性的作用域)。通过ScriptContext的getScopes可以得到其中所有可用的作用域,其中预定义了两个作用域:常量ScriptContext.ENGINE_SCOPE(当前的脚本引擎)和ScriptContext.GLOBAL_SCOPE(从同一引擎工厂中创建的所有脚本引擎对象)。

1
2
3
4
5
6
7
public void scriptContextAttribute() {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
context.setAttribute("name", "Alex", ScriptContext.GLOBAL_SCOPE);
context.setAttribute("name", "Bob", ScriptContext.ENGINE_SCOPE);
context.getAttribute("name"); //值为Bob
}

语言绑定对象

语言绑定对象位于ScriptContext中,同样也有作用域之分,范围越小,优先级越高。执行如下代码,输出的name值为Bob。

1
2
3
4
5
6
7
8
9
10
11
public void scriptContextBindings() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
Bindings bindings1 = engine.createBindings();
bindings1.put("name", "Alex");
context.setBindings(bindings1, ScriptContext.GLOBAL_SCOPE);
Bindings bindings2 = engine.createBindings();
bindings2.put("name", "Bob");
context.setBindings(bindings2, ScriptContext.ENGINE_SCOPE);
engine.eval("println(name);"); //Bob
}

也可以通过ScriptContext获取语言绑定对象:

1
2
3
4
5
6
7
public void useScriptContextValues() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("name", "Alex");
engine.eval("println(name);");
}

前面说到语言绑定对象存在于上下文环境中,故context中保存的自定义属性其实也是保存于语言绑定对象中的,如2中的语言绑定。

1
2
3
4
5
6
public void attributeInBindings() throws ScriptException {  
ScriptEngine engine = getJavaScriptEngine();
ScriptContext context = engine.getContext();
context.setAttribute("name", "Alex", ScriptContext.GLOBAL_SCOPE);
engine.eval("println(name);");
}

脚本编译

脚本语言一般均是解释执行的,相对于编译执行的语言,效率较低一些。当脚本语言需要多次重复执行时,可以先对煎熬本进行编译,避免重复解析,提高效率(注:脚本编译需要脚本引擎支持,实现javax.script.Compilable接口)。JavaSE中自带的JavaScript引擎是支持对脚本进行编译的,编译的脚本用javax.script.CompiledScript来表示。

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
public class ScriptCompile extends JsScriptRunner {  
//对脚本进行编译
public CompiledScript compile(String scriptText) throws ScriptException {
ScriptEngine engine = getJavaScriptEngine();
if (engine instanceof Compilable) {
CompiledScript script = ((Compilable) engine).compile(scriptText);
return script;
}
return null;
}

//先编译再执行
public void run(String scriptText) throws ScriptException {
CompiledScript script = compile(scriptText);
if (script == null) {
return;
}
for (int i = 0; i < 100; i++) {
script.eval();
}
}

public static void main(String[] args) {
ScriptCompile sc = new ScriptCompile();
try {
sc.run("println('Hello');");
} catch (ScriptException ex) {
Logger.getLogger(ScriptCompile.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

方法调用

Java虚拟机支持脚本的意义在于实现函数式的编程,即脚本中最重要的便是方法。一些脚本引擎允许使用者单独调用脚本中的某个方法,支持此操作的脚本引擎可以通过实现javax.script.Invocable接口,支持顶层方法或者某对象中成员方法的调用。使用方法调用时最好先检查脚本引擎是否实现了Invocable接口,JavaSE中的JavaScript引擎已实现了Invocable接口。

  1. 在Java中调用脚本中的顶层方法

    1
    2
    3
    4
    5
    6
    7
    public void invokeFunction() throws ScriptException, NoSuchMethodException {  
    ScriptEngine engine = getJavaScriptEngine();
    String scriptText = "function greet(name) { println('Hello, ' + name); } ";
    engine.eval(scriptText);
    Invocable invocable = (Invocable) engine;
    invocable.invokeFunction("greet", "Alex");
    }
  2. 调用脚本中某对象的成员方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void invokeMethod() throws ScriptException, NoSuchMethodException {  
    ScriptEngine engine = getJavaScriptEngine();
    String scriptText = "var obj = { getGreeting : function(name) { return 'Hello, ' + name; } }; ";
    engine.eval(scriptText);
    Invocable invocable = (Invocable) engine;
    Object scope = engine.get("obj");
    Object result = invocable.invokeMethod(scope, "getGreeting", "Alex"); //第一个参数为方法所属对象
    System.out.println(result);
    }
  3. 指定脚本中的方法为Java接口的实现

    Greet是Java实现的接口,包含一个方法getGreeting,通过Invocable.getInterface()方法指定脚本中的方法为Java接口的实现。

    1
    2
    3
    4
    5
    6
    7
    8
    public void useInterface() throws ScriptException {  
    ScriptEngine engine = getJavaScriptEngine();
    String scriptText = "function getGreeting(name) { return 'Hello, ' + name; } ";
    engine.eval(scriptText);
    Invocable invocable = (Invocable) engine;
    Greet greet = invocable.getInterface(Greet.class);
    System.out.println(greet.getGreeting("Alex"));
    }

Nashorn JavaScript脚本引擎

动态编译

动态编译,就是在程序运行时产生java类,并编译成class文件。

动态编译的实现方式

正常情况下先编译(明文源码到字节码),后执行(JVM加载字节码,获得类模板,实例化,方法使用)。

程序流程图

相关类介绍

JavaCompiler: 负责读取源代码,编译诊断,输出class
JavaFileObject: 文件抽象,代表源代码或者编译后的class
JavaFileManager: 管理JavaFileObject,负责JavaFileObject的创建和保存位置
ClassLoader: 根据字节码,生成类模板

使用方式

由于代码在编译的时候,类定义甚至类名称还不存在,所以没法直接声明使用的。只能定义一个接口代替之,具体实现留给后面的动态编译。

1
2
3
public interface Printer {
public void print();
}

使用 JavaCompiler 接口来编译 java 源程序(最简单的)

通过 ToolProvider 类的静态方法 getSystemJavaCompiler 来得到一个 JavaCompiler 接 口的实例。

1
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

javaCompiler 中最核心的方法是 run。通过这个方法能编译 java 源程序。这个方法有 3 个固 定参数和 1 个可变参数。前 3 个参数分别用来为 java 编译器提供参数、得到 Java 编译器的输出信息及接收编译器的 错误信息,后面的可变参数能传入一个或多个 Java 源程式文件。如果 run 编译成功,返回 0。

1
2
3
int result = compiler.run(null,null,null,sourceFile);
System.out.println(result==0?"编译成功""编译失败");
return result;
  • 第一个参数:为java编译器提供参数

  • 第二个参数:得到java编译器的输出信息

  • 第三个参数:接收编译器的错误信息

  • 第四个参数:可变参数(String数组)能传入一个或者多个java源文件

  • 返回值:0表示编译成功,非0表示编译失败

  • 如果前 3 个参数传入的是 null,那么 run 方法将以标准的输入、输出代替,即 System.in、 System.out 和 System.err。如果我们要编译一个 hello.java 文件,并将使用标准输入输出,run 的使用方法如下:

    1
    int results = tool.run(null, null, null, "Hello.java");

使用 StandardJavaFileManager 编译 Java 源程序

StandardJavaFileManager 类能非常好地控制输入、输出,并且能通过 DiagnosticListener 得到诊断信息,而 DiagnosticCollector 类就是 listener 的实现。

使用 StandardJavaFileManager 需要两步。
首先建立一个 DiagnosticCollector 实例及通过 JavaCompiler 的 getStandardFileManager()方法得到一个 StandardFileManager 对象。
最后通过 CompilationTask 中的 call 方法编译源程序

源代码的文件级动态编译

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
String classPath = File2Class.class.getResource("/").getPath();
//在这里我们是动态生成定义,然后写入文件。也可以直接读一个已经存在的文件
String str = "import classloader.Printer;"
+ "public class MyPrinter1 implements Printer {"
+ "public void print() {"
+ "System.out.println(\"test1\");"
+ "}}";
FileWriter writer = new FileWriter(classPath + "MyPrinter1.java");
writer.write(str);;
writer.close();
//获得系统编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获得java文件管理器
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null, null);
//读入源文件
Iterable fileObject = fileManager.getJavaFileObjects(classPath + "MyPrinter1.java");
//生成编译任务
JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, null, null, null, fileObject);
//执行编译任务
task.call();
fileManager.close();
//指定class路径,默认和源代码路径一致,加载class
URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:" + classPath)});
Printer printer = (Printer)classLoader.loadClass("MyPrinter1").newInstance();
printer.print();

源代码的内存级动态编译

上节源代码落地了,这节让我们看下源代码和class全程在内存不落地,如何实现动态编译。思路是生成源代码对应的JavaFileObject时,从内存string读取;生成class对应的JavaFileObject时,以字节数组的形式存到内存。JavaFileObject是一个interface, SimpleJavaFileObject是JavaFileObject的一个基本实现,当自定义JavaFileObject时,继承SimpleJavaFileObject,然后改写部分函数。
自定义JavaSourceFromString,作为源代码的抽象文件(来自JDK API文档)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* A file object used to represent source coming from a string.
*/
public class JavaSourceFromString extends SimpleJavaFileObject {
/**
* The source code of this "file".
*/
final String code;
/**
* Constructs a new JavaSourceFromString.
* @param name the name of the compilation unit represented by this file object
* @param code the source code for the compilation unit represented by this file object
*/
JavaSourceFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}

JavaClassFileObject,代表class的文件抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JavaClassFileObject extends SimpleJavaFileObject {
//用于存储class字节
ByteArrayOutputStream outputStream;

public JavaClassFileObject(String className, Kind kind) {
super(URI.create("string:///" + className.replace('.', '/') + kind.extension), kind);
outputStream = new ByteArrayOutputStream();
}

@Override
public OutputStream openOutputStream() throws IOException {
return outputStream;
}

public byte[] getClassBytes() {
return outputStream.toByteArray();
}
}

ClassFileManager,修改JavaFileManager生成class的JavaFileObject的行为,另外返回一个自定义ClassLoader用于返回内存中的字节码对应的类模板

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
public class ClassFileManager extends ForwardingJavaFileManager {

private JavaClassFileObject classFileObject;
/**
* Creates a new instance of ForwardingJavaFileManager.
*
* @param fileManager delegate to this file manager
*/
protected ClassFileManager(JavaFileManager fileManager) {
super(fileManager);
}

/**
* Gets a JavaFileObject file object for output
* representing the specified class of the specified kind in the given location.
*/
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
classFileObject = new JavaClassFileObject(className, kind);
return classFileObject;
}

@Override
//获得一个定制ClassLoader,返回我们保存在内存的类
public ClassLoader getClassLoader(Location location) {
return new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = classFileObject.getClassBytes();
return super.defineClass(name, classBytes, 0, classBytes.length);
}
};
}
}

用自定义的JavaFileObject/JavaFileManager来动态编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String str = "import Printer;" 
+ "public class MyPrinter2 implements Printer {"
+ "public void print() {"
+ "System.out.println(\"test2\");"
+ "}}";
//生成源代码的JavaFileObject
SimpleJavaFileObject fileObject = new JavaSourceFromString("MyPrinter2", str);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//被修改后的JavaFileManager
JavaFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));
//执行编译
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(fileObject));
task.call();
//获得ClassLoader,加载class文件
ClassLoader classLoader = fileManager.getClassLoader(null);
Class printerClass = classLoader.loadClass("MyPrinter2");
//获得实例
Printer printer = (Printer) printerClass.newInstance();
printer.print();

慎用动态编译

  • 在框架中谨慎使用
  • 不要在要求高性能的项目使用
    动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。
  • 动态编译要考虑安全问题
    它是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。
  • 记录动态编译过程
    建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序

参考文章

Java 类运行时动态编译技术 - Fenrier Lab (seanwangjs.github.io)

深入理解Java的动态编译 - 掘金 (juejin.cn)

1.如果你遇见社会上有不平事,万不可挺身而出,讲公道话,否则,事情倒会移到你头上来,甚至于会被指作反动分子的。如果你遇见有人被冤枉,被诬陷的,即使明知道他是好人,也万不可挺身而出,去给他解释或分辩,否则,你就会被人说是他的亲戚,或得了他的贿路;倘使那是女人,就要被疑为她的情人的;如果他较有名,那便是党羽。——《世故三昧》
2.中国人的性情是总喜欢调和折中的,譬如你说,这屋子太暗,须在这里开一个窗,大家一定不允许的。但如果你主张拆掉屋顶他们就来调和,愿意开窗了。——《无声的中国》
3.楼下一个男人病得要死,那间壁的一家唱着留声机;对面是弄孩子。楼上有两人狂笑;还有打牌声。河中的船上有女人哭着她死去的母亲。人类的悲欢并不相通,我只觉得他们吵闹。——《小杂感》
4.真的猛士敢于直面惨淡的人生,敢于正视淋漓的鲜血。
5.你要灭一个人,一是骂杀,一是捧杀。
6.悲剧将人生的有价值的东西毁灭给人看,喜剧将那无价值的撕破给人看。
7.面具戴太久,就会长到脸上,在想揭下来,除非伤筋动骨扒皮。——《鲁迅日记》
8.愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光,就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。
9.

八、J.U.C-其他组件

fork-join框架

​ Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork/Join框架要完成两件事情:

  1.任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割

  2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。

Fork/Join框架简化了并行程序的原因有:

  • 它简化了线程的创建,在框架中线程是自动被创建和管理。
  • 它自动使用多个处理器,因此程序可以扩展到使用可用处理器。

由于支持真正的并行执行,Fork/Join框架可以显著减少计算时间,并提高解决图像处理、视频处理、大数据处理等非常大问题的性能。

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
public class ForkJoinExample extends RecursiveTask<Integer> {

private final int threshold = 5;
private int first;
private int last;

public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}

@Override
protected Integer compute() {
int result = 0;
if (last - first <= threshold) {
// 任务足够小则直接计算
for (int i = first; i <= last; i++) {
result += i;
}
} else {
// 拆分成小任务
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
}
1
2
3
4
5
6
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}

关于Fork/Join框架的一个有趣的地方是:它使用工作窃取算法来平衡线程之间的负载:如果一个工作线程没有事情要做,它可以从其他仍然忙碌的线程窃取任务。

Fork/Join框架在java.util.concurrent包下被实现。它的核心有4个类:

  • ForkJoinTask: 这是一个抽象任务类,并且运行在ForkJoinPool中。
  • ForkJoinPool:这是一个线程池管理并运行众多ForkJoinTask任务。
  • RecursiveAction: ForkJoinTask的子类,这个类没有返回值。
  • RecursiveTask: ForkJoinTask的子类,有返回值。

基本上,我们解决问题的代码是在RecursiveAction或者RecursiveTask中进行的,然后将任务提交由ForkJoinPool执行,ForkJoinPool处理从线程管理到多核处理器的利用等各种事务。

我们先来理解一下这些类中的关键方法。

ForkJoinTask

这是一个运行在ForkJoinPool中的抽象的任务类。类型V指定了任务的返回结果。ForkJoinTask是一个类似线程的实体,它表示任务的轻量级抽象,而不是实际的执行线程。该机制允许由ForkJoinPool中的少量实际线程管理大量任务。其关键方法是:

  • final ForkJoinTask fork()
  • final V join()
  • final V invoke()

fork()方法提交并执行异步任务,该方法返回ForkJoinTask并且调用线程继续运行。

join()方法等待任务直到返回结果。

invoke()方法是组合了fork()join(),它开始一个任务并等待结束返回结果。

此外,ForkJoinTask中还提供了用于一次调用多个任务的两个静态方法

  • static void invokeAll(ForkJoinTask task1, ForkJoinTask task2) :执行两个任务
  • static void invokeAll(ForkJoinTask<?>… taskList):执行任务集合

RecursiveAction

这是一个递归的ForkJoinTask子类,不返回结果。Recursive意思是任务可以通过分治策略分成自己的子任务。

我们必须重写compute()方法,并将计算代码写在其中:

1
protected abstract void compute();

RecursiveTask

RecursiveAction一样,但是RecursiveTask有返回结果,结果类型由V指定。我们仍然需要重写compute()方法:

1
protected abstract V compute();

ForkJoinPool

任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务(工作窃取算法)。

这是Fork/Join框架的核心类。它负责线程的管理和ForkJoinTask的执行,为了执行ForkJoinTask,首先需要获取到ForkJoinPool的实例。

有两种构造器方式可以获取ForkJoinPool的实例,第一种使用构造器创建:

  • ForkJoinPool(): 使用默认的构造器创建实例,该构造器创建出的池与系统中可用的处理器数量相等
  • ForkJoinPool(int parallelism):该构造器指定处理器数量,创建具有自定义并行度级别的池,该级别的并行度必须大于0,且不超过可用处理器的实际数量。

获取ForkJoinPool实例的第二种方法是使用以下ForkJoinPool的静态方法获取公共池实例:

1
public static ForkJoinPool commonPool();

这种方式创建的池不受shutdown()或者shutdownNow()方法的影响,但是他会在System.exit()时会自动中止。任何依赖异步任务处理的程序在主体程序中止前都应该调用awaitQuiescence()方法。该方式是静态的,可以自动被使用。

e.g.

在创建好ForkJoinPool实例之后,可以使用下面的方法执行任务:

  • T invoke(ForkJoinTask task):执行指定任务并返回结果,该方法是异步的,调用的线程会一直等待直到该方法返回结果,对于RecursiveAction任务来说,参数类型是Void.
  • void execute(ForkJoinTask<?> task):异步执行指定的任务,调用的线程一直等待知道任务完成才会继续执行。

另外,也可以通过ForkJoinTask自己拥有的方法fork()invoke()执行任务。在这种情况下,如果任务还没在ForkJoinPool中运行,那么commonPool()将会自动被使用。

值得注意的一点是:ForkJoinPool使用的是守护线程,当所有的用户线程被终止是它也会被终止,这意味着可以不必显示的关闭ForkPoolJoin(虽然这样也可以)。如果是common pool的情况下,调用shutdown没有任何效果,应为这个池总是可用的。

使用RecursiveAction

假设要对一个很大的数字数组进行变换,为了简单简单起见,转换只需要将数组中的每个元素乘以指定的数字。下面的代码用于转换任务:

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
import java.util.concurrent.*;

public class ArrayTransform extends RecursiveAction {
int[] array;
int number;
int threshold = 100_000;
int start;
int end;

public ArrayTransform(int[] array, int number, int start, int end) {
this.array = array;
this.number = number;
this.start = start;
this.end = end;
}

@Override
protected void compute() {
if (end - start < threshold) {
computeDirectly();
} else {
int middle = (end + start) / 2;

ArrayTransform subTask1 = new ArrayTransform(array, number, start, middle);
ArrayTransform subTask2 = new ArrayTransform(array, number, middle, end);

invokeAll(subTask1, subTask2);
}
}

protected void computeDirectly() {
for (int i = start; i < end; i++) {
array[i] = array[i] * number;
}
}
}

可以看到,这是一个RecursiveAction的子类,我们重写了compute()方法。

数组和数字从它的构造函数传递。参数start和end指定要处理的数组中的元素的范围。如果数组的大小大于阈值,这有助于将数组拆分为子数组,否则直接对整个数组执行计算。

观察else中的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
protected void compute() {
if (end - start < threshold) {
computeDirectly();
} else {
int middle = (end + start) / 2;

ArrayTransform subTask1 = new ArrayTransform(array, number, start, middle);
ArrayTransform subTask2 = new ArrayTransform(array, number, middle, end);

invokeAll(subTask1, subTask2);
}
}

这里,将数组分成两个部分,并分别创建他们的子任务,反过来,子任务也可以递归的进一步划分为更小的子任务,直到其大小小于直接调用computeDirectly();方法的的阈值。

然后,在main函数中创建ForkJoinPool执行任务:

1
2
3
ArrayTransform mainTask = new ArrayTransform(array, number, 0, SIZE);
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(mainTask);

或者使用common pool执行任务:

1
2
ArrayTransform mainTask = new ArrayTransform(array, number, 0, SIZE);
mainTask.invoke();

这里是全部的测试程序:

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
import java.util.*;
import java.util.concurrent.*;

public class ForkJoinRecursiveActionTest {
static final int SIZE = 10_000_000;
static int[] array = randomArray();

public static void main(String[] args) {

int number = 9;

System.out.println("数组中的初始元素: ");
print();
//ArrayTransform 是一个继承RecursiveAction的类,在覆写compute方法时改写成递归方法
ArrayTransform mainTask = new ArrayTransform(array, number, 0, SIZE);
//ForkJoinPool是一个线程池管理并运行众多ForkJoinTask任务。
ForkJoinPool pool = new ForkJoinPool();
//invoke()方法是组合了fork()和join(),它开始一个任务并等待结束返回结果。
pool.invoke(mainTask);
System.out.println("并行计算之后的元素:");
print();
}

static int[] randomArray() {
int[] array = new int[SIZE];
Random random = new Random();

for (int i = 0; i < SIZE; i++) {
array[i] = random.nextInt(100);
}

return array;
}

static void print() {
for (int i = 0; i < 10; i++) {
System.out.print(array[i] + ", ");
}
System.out.println();
}
}

如您所见,使用随机生成的1,000万个元素数组进行测试。由于数组太大,我们在计算前后只打印前10个元素,看效果如何:

1
2
3
4
数组中的初始元素:
42, 98, 43, 14, 9, 92, 33, 18, 18, 76,
并行计算之后的元素:
378, 882, 387, 126, 81, 828, 297, 162, 162, 684,

使用RecursiveTask

这个例子中,展示了如何使用带有返回值的任务,下面的任务计算在一个大数组中出现偶数的次数:

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
import java.util.concurrent.*;

public class ArrayCounter extends RecursiveTask<Integer> {
int[] array;
int threshold = 100_000;
int start;
int end;

public ArrayCounter(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}

protected Integer compute() {
if (end - start < threshold) {
return computeDirectly();
} else {
int middle = (end + start) / 2;

ArrayCounter subTask1 = new ArrayCounter(array, start, middle);
ArrayCounter subTask2 = new ArrayCounter(array, middle, end);

invokeAll(subTask1, subTask2);


return subTask1.join() + subTask2.join();
}
}

protected Integer computeDirectly() {
Integer count = 0;

for (int i = start; i < end; i++) {
if (array[i] % 2 == 0) {
count++;
}
}

return count;
}
}

如你所见,这个类是RecursiveTask的子类并且重写了compute()方法,并且返回了一个整型的结果。

这里还使用了join()方法去合并子任务的结果:

1
return subTask1.join() + subTask2.join();

测试程序就和RecursiveAction的一样:

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
import java.util.*;
import java.util.concurrent.*;

public class ForkJoinRecursiveTaskTest {
static final int SIZE = 10_000_000;
static int[] array = randomArray();

public static void main(String[] args) {

ArrayCounter mainTask = new ArrayCounter(array, 0, SIZE);
ForkJoinPool pool = new ForkJoinPool();
Integer evenNumberCount = pool.invoke(mainTask);

System.out.println("偶数的个数: " + evenNumberCount);
}

static int[] randomArray() {
int[] array = new int[SIZE];
Random random = new Random();

for (int i = 0; i < SIZE; i++) {
array[i] = random.nextInt(100);
}

return array;
}

}

运行程序就会看到如下的结果:

1
偶数的个数: 5000045

并行性试验

这个例子展示并行性的级别如何影响计算时间:

ArrayCounter类让阈值可以通过构造器传入:

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
import java.util.concurrent.*;

public class ArrayCounter extends RecursiveTask<Integer> {
int[] array;
int threshold;
int start;
int end;

public ArrayCounter(int[] array, int start, int end, int threshold) {
this.array = array;
this.start = start;
this.end = end;
this.threshold = threshold;
}

protected Integer compute() {
if (end - start < threshold) {
return computeDirectly();
} else {
int middle = (end + start) / 2;

ArrayCounter subTask1 = new ArrayCounter(array, start, middle, threshold);
ArrayCounter subTask2 = new ArrayCounter(array, middle, end, threshold);

invokeAll(subTask1, subTask2);


return subTask1.join() + subTask2.join();
}
}

protected Integer computeDirectly() {
Integer count = 0;

for (int i = start; i < end; i++) {
if (array[i] % 2 == 0) {
count++;
}
}

return count;
}
}

测试程序将并行度级别和阈值作为参数传递:

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
import java.util.*;
import java.util.concurrent.*;

public class ParallelismTest {
static final int SIZE = 10_000_000;

static int[] array = randomArray();

public static void main(String[] args) {
int threshold = Integer.parseInt(args[0]);
int parallelism = Integer.parseInt(args[1]);

long startTime = System.currentTimeMillis();

ArrayCounter mainTask = new ArrayCounter(array, 0, SIZE, threshold);
ForkJoinPool pool = new ForkJoinPool(parallelism);
Integer evenNumberCount = pool.invoke(mainTask);

long endTime = System.currentTimeMillis();

System.out.println("偶数的个数: " + evenNumberCount);

long time = (endTime - startTime);
System.out.println("执行时间: " + time + " ms");
}

static int[] randomArray() {
int[] array = new int[SIZE];
Random random = new Random();

for (int i = 0; i < SIZE; i++) {
array[i] = random.nextInt(100);
}

return array;
}

}

该程序允许您使用不同的并行度和阈值轻松测试性能。注意,它在最后打印执行时间。尝试用不同的参数多次运行这个程序,并观察执行时间。

结论

  • Fork/Join框架的设计简化了java语言的并行程序
  • ForkJoinPoolFork/Join框架的核心,它允许多个ForkJoinTask请求由少量实际线程执行,每个线程运行在单独的处理核心上
  • 既可以通过构造器也可以通过静态方法common pool去获取ForkJoinPool的实例
  • ForkJoinTask是一个抽象类,它表示的任务比普通线程更轻。通过覆盖其compute()方法实现计算逻辑
  • RecursiveAction是一个没有返回值的ForkJoinTask
  • RecursiveTask是一个有返回值的ForkJoinTask
  • ForkJoinPool与其它池的不同之处在于,它使用了工作窃取算法,该算法允许一个线程完成了可以做的事情,从仍然繁忙的其他线程窃取任务
  • ForkJoinPool中的线程是守护线程,不必显式地关闭池
  • 执行一个ForkJoinTask既可以通过调用它自己的invoke()fork()方法,也可以提交任务给ForkJoinPool并调用它的invoke()或者execute()方法
  • 直接使用ForkJoinTask自身的方法执行任务,如果它还没运行在ForkJoinPool中那么将运行在common pool
  • ForkJoinTask中使用join()方法,可以合并子任务的结果
  • invoke()方法会等待子任务完成,但是execute()方法不会

阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列(blockingqueue)导致线程阻塞。

阻塞队列提供了四种处理方法:

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用

异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。

  • 返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
  • 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

BlockingQueue的核心方法:

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
public interface BlockingQueue<E> extends Queue<E> {

//将给定元素设置到队列中,如果设置成功返回true, 否则抛出异常。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
boolean add(E e);

//将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
boolean offer(E e);

//将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
void put(E e) throws InterruptedException;

//将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;

//从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
E take() throws InterruptedException;

//在给定的时间里,从队列中获取值,如果没有取到会抛出异常。
E poll(long timeout, TimeUnit unit)
throws InterruptedException;

//获取队列中剩余的空间。
int remainingCapacity();

//从队列中移除指定的值。
boolean remove(Object o);

//判断队列中是否拥有该值。
public boolean contains(Object o);

//将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c);

//指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c, int maxElements);
}

在深入之前先了解下下ReentrantLock 和 Condition:
重入锁ReentrantLock:
ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。
ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。
主要方法:

  • lock()获得锁
  • lockInterruptibly()获得锁,但优先响应中断
  • tryLock()尝试获得锁,成功返回true,否则false,该方法不等待,立即返回
  • tryLock(long time,TimeUnit unit)在给定时间内尝试获得锁
  • unlock()释放锁

Condition:await()、signal()方法分别对应之前的Object的wait()和notify()

  • 和重入锁一起使用
  • await()是当前线程等待同时释放锁
  • awaitUninterruptibly()不会在等待过程中响应中断
  • signal()用于唤醒一个在等待的线程,还有对应的singalAll()方法

阻塞队列的成员

队列 有界性 数据结构
ArrayBlockingQueue bounded(有界) 加锁 arrayList
LinkedBlockingQueue optionally-bounded 加锁 linkedList
PriorityBlockingQueue unbounded 加锁 heap
DelayQueue unbounded 加锁 heap
SynchronousQueue bounded 加锁
LinkedTransferQueue unbounded 加锁 heap
LinkedBlockingDeque unbounded 无锁 heap

下面分别简单介绍一下:

  • ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】
  • LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序。
  • PriorityBlockingQueue: 一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
  • DelayQueue: 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。(DelayQueue可以运用在以下应用场景:1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。)
  • SynchronousQueue: 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
  • LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
  • LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。

ArrayBlockingQueue、LinkedBlockingQueue以及DelayQueue介绍

https://www.cnblogs.com/bjxq-cs88/p/9759571.html

九、线程安全的集合

首先要明白线程的工作原理,jvm有一个main memory,而每个线程有自己的working memory,一个线程对一个variable进行操作时,都要在自己的working memory里面建立一个copy,操作完之后再写入main memory。多个线程同时操作同一个variable,就可能会出现不可预知的结果。根据上面的解释,很容易想出相应的scenario。

而用synchronized的关键是建立一个monitor,这个monitor可以是要修改的variable也可以其他你认为合适的object比如method,然后通过给这个monitor加锁来实现线程安全,每个线程在获得这个锁之后,要执行完load到workingmemory -> use&assign -> store到mainmemory 的过程,才会释放它得到的锁。这样就实现了所谓的线程安全。

什么是线程安全?线程安全是怎么完成的(原理)? 线程安全就是说多线程访问同一代码,不会产生不确定的结果。编写线程安全的代码是低依靠线程同步。

1、早期线程安全的集合

1、Vector、ArrayList、LinkedList

Vector和ArrayList在使用上非常相似,都可用来表示一组数量可变的对象应用的集合,并且可以随机地访问其中的元素。

   Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。

ArrayList和LinkedList区别

   对于处理一列数据项,Java提供了两个类ArrayList和LinkedList,ArrayList的内部实现是基于内部数组Object[],所以从概念上讲,它更象数组,但LinkedList的内部实现是基于一组连接的记录,所以,它更像一个链表结构,所以,它们在性能上有很大的差别。

   从上面的分析可知,在ArrayList的前面或中间插入数据时,你必须将其后的所有数据相应的后移,这样必然要花费较多时间,所以,当你的操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能

   而访问链表中的某个元素时,就必须从链表的一端开始沿着连接方向一个一个元素地去查找,直到找到所需的元素为止,所以,当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

   如果在编程中,1,2两种情形交替出现,这时,你可以考虑使用List这样的通用接口,而不用关心具体的实现,在具体的情形下,它的性能由具体的实现来保证。

2、HashTable,HashMap,HashSet

HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是:

1)、HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是synchronized。

2)、HashTable不允许有null值的存在。

在HashTable中调用put方法时,如果key为null,直接抛出NullPointerException。其它细微的差别还有,比如初始化Entry数组的大小等等,但基本思想和HashMap一样。

HashSet:

1、HashSet基于HashMap实现,无容量限制。

2、HashSet是非线程安全的。

3、HashSet不保证有序。

HashMap:

1、HashMap采用数组方式存储key,value构成的Entry对象,无容量限制。

2、HashMap基于Key hash查找Entry对象存放到数组的位置,对于hash冲突采用链表的方式来解决。

3、HashMap在插入元素时可能会要扩大数组的容量,在扩大容量时须要重新计算hash,并复制对象到新的数组中。

4、HashMap是非线程安全的。

5、HashMap遍历使用的是Iterator

HashTable

1、HashTable是线程安全的。

2、HashTable中无论是Key,还是Value都不允许为null。

3、HashTable遍历使用的是Enumeration。

TreeSet,TreeMap

TreeSet:

1、TreeSet基于TreeMap实现,支持排序。

2、TreeSet是非线程安全的。

从对HashSet和TreeSet的描述来看,TreeSet和HashSet一样,也是完全基于Map来实现的,并且都不支持get(int)来获取指定位置的元素(需要遍历获取),另外TreeSet还提供了一些排序方面的支持。例如传入Comparator实现、descendingSet以及descendingIterator等。

TreeMap:

1、TreeMap是一个典型的基于红黑树的Map实现,因此它要求一定要有Key比较的方法,要么传入Comparator实现,要么key对象实现Comparable接口。

2、TreeMap是非线程安全的。

2、Collections包装方法

Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合

1
2
3
4
5
List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

3、java.util.concurrent包中的集合

在java.util.concurrent包中,不但包含了我们本篇要说的线程安全的集合,还涉及到了多线程、CAS、线程锁等相关内容,可以说是完整覆盖了Java并发的知识栈。

对于Java开发人员来说,学好java.util.concurrent包下的内容,是一个必备的功课,也是逐渐提升自己的一个重要阶段。

1.ConcurrentHashMap

ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率

2.CopyOnWriteArrayList和CopyOnWriteArraySet

它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

3.其他

除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到

Collection集合:

List:

1
CopyOnWriteArrayList

Set:

1
2
CopyOnWriteArraySet
ConcurrentSkipListSet

Queue:

1
2
3
4
5
6
7
8
9
10
BlockingQueue:
LinkedBlockingQueue
DelayQueue
PriorityBlockingQueue
ConcurrentLinkedQueue
TransferQueue:
LinkedTransferQueue
BlockingDeque:
LinkedBlockingDeque
ConcurrentLinkedDeque

Map集合:

Map:

1
2
3
4
ConcurrentMap:
ConcurrentHashMap
ConcurrentSkipListMap
ConcurrentNavigableMap

通过以上可以看出,java.util.concurrent包为每一类集合都提供了线程安全的实现。

线程安全有以下几种实现方式:

不可变

不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。

不可变的类型:

  • final 关键字修饰的基本数据类型
  • String
  • 枚举类型
  • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

1
2
3
4
5
6
7
public class ImmutableExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("a", 1);
}
}
1
2
3
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at ImmutableExample.main(ImmutableExample.java:9)

Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。

1
2
3
public V put(K key, V value) {
throw new UnsupportedOperationException();
}

互斥同步

synchronized 和 ReentrantLock。

非阻塞同步

互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。

1. CAS

乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

2. AtomicInteger

J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。

以下代码使用了 AtomicInteger 执行了自增的操作。

1
2
3
4
5
private AtomicInteger cnt = new AtomicInteger();

public void add() {
cnt.incrementAndGet();
}

以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。

1
2
3
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。

可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。

1
2
3
4
5
6
7
8
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

3. ABA

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

无同步方案

要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

1. 栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

1
2
3
4
5
6
7
8
9
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
1
2
3
4
5
6
7
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
1
2
100
100

2. 线程本地存储(Thread Local Storage)

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。

可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2);
threadLocal.remove();
});
thread1.start();
thread2.start();
}
}
1
1

为了理解 ThreadLocal,先看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalExample1 {
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
ThreadLocal threadLocal2 = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal1.set(1);
threadLocal2.set(1);
});
Thread thread2 = new Thread(() -> {
threadLocal1.set(2);
threadLocal2.set(2);
});
thread1.start();
thread2.start();
}
}

它所对应的底层结构图为:


每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

get() 方法类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。

在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。

3. 可重入代码(Reentrant Code)

这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

十、锁优化

这里的锁优化主要是指 JVM 对 synchronized 的优化。

自旋锁

互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。

自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。

在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。

锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。

对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:

1
2
3
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}

String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:

1
2
3
4
5
6
7
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。

上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。

轻量级锁

JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。

以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。


下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。


轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。

当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。


如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

偏向锁

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。

当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。

当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。


十一、Java 内存模型

Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。

加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。


所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。

线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。


内存间交互操作

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。


  • read:把一个变量的值从主内存传输到工作内存中
  • load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
  • use:把工作内存中一个变量的值传递给执行引擎
  • assign:把一个从执行引擎接收到的值赋给工作内存的变量
  • store:把工作内存的一个变量的值传送到主内存中
  • write:在 store 之后执行,把 store 得到的值放入主内存的变量中
  • lock:作用于主内存的变量
  • unlock

内存模型三大特性

1. 原子性

Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。

有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。

为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。

下图演示了两个线程同时对 cnt 进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入旧值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。


AtomicInteger 能保证多个线程修改的原子性。


使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:

1
2
3
4
5
6
7
8
9
10
11
public class AtomicExample {
private AtomicInteger cnt = new AtomicInteger();

public void add() {
cnt.incrementAndGet();
}

public int get() {
return cnt.get();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicExample example = new AtomicExample(); // 只修改这条语句
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
1
1000

除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。

1
2
3
4
5
6
7
8
9
10
11
public class AtomicSynchronizedExample {
private int cnt = 0;

public synchronized void add() {
cnt++;
}

public synchronized int get() {
return cnt;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicSynchronizedExample example = new AtomicSynchronizedExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
1
1000

2. 可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

主要有三种实现可见性的方式:

  • volatile
  • synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
  • final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。

3. 有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。

也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

先行发生原则

上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。

1. 单一线程原则

Single Thread rule

在一个线程内,在程序前面的操作先行发生于后面的操作。


2. 管程锁定规则

Monitor Lock Rule

一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。


3. volatile 变量规则

Volatile Variable Rule

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。


4. 线程启动规则

Thread Start Rule

Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。


5. 线程加入规则

Thread Join Rule

Thread 对象的结束先行发生于 join() 方法返回。


6. 线程中断规则

Thread Interruption Rule

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

7. 对象终结规则

Finalizer Rule

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

8. 传递性

Transitivity

如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

十二、多线程开发良好的实践

  • 给线程起个有意义的名字,这样可以方便找 Bug。

  • 缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。

  • 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。

  • 使用 BlockingQueue 实现生产者消费者问题。

  • 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。

  • 使用本地变量和不可变类来保证线程安全。

  • 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。

参考资料

五、线程同步

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

1、使用重入锁(Lock)实现线程同步

​ 在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:

1
2
3
ReentrantLock() : 创建一个ReentrantLock实例         
lock() : 获得锁
unlock() : 释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//只给出要修改的代码,其余代码与上同
class Bank {

private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();//
try{
account += money;
}finally{
lock.unlock();//解锁线程
}

}

lock.lock();确保只有一个线程进入临界区,一旦一个线程进入之后,会获得锁对象,其他线程无法通过lock语句。当其他线程调用lock时,它们会被阻塞,知道第一个线程释放锁对象。

lock.unlock();解锁操作,一定要放到finally里,因为如果try语句里出了问题,锁必须被释放,否则其他线程将永远被阻塞

因为系统会随机为线程分配资源,所以在线程获得锁对象之后,可能被系统剥夺运行权,这时候其他线程来访问,但是发现有锁,进不去,只能等拿到锁对象的线程把里面的代码执行完毕后,释放锁,第二个线程才能运行。

2、synchronzied关键字

前面我们讲了ReentrantLock锁对象的使用,但是在系统里面我们不一定要使用ReentrantLock锁,Java中还提供了一个内部的隐式锁,关键字是synchronized.

举个例子:

1
2
3
public synchronized void Method() {
//do some work...
}

synchronized关键字说明

  总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量(成员变量)、object reference(对象实例引用)、static函数和class literals(类名称字面常量)身上。

在进一步阐述之前,我们需要明确几点:

  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

  • 每个对象只有一个锁(lock)与之相关联。JVM会给类的每个实例化的对象赋予一个单独的锁。

  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

  注意:在同步块和同步方法中,是给类或类的对象进行加锁,而不是给方法加锁。所谓的需要获得对象的锁才能执行方法,也是针对线程而言的。

synchronized 方法:

通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如下:

1
2
3
4
5
6
class MyClass{
private String name;
public synchronized void setName(String name){
this.name = name;
}
}

  synchronized方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。

  在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

  synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run()声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。

synchronized 块:

  synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

  通过 synchronized关键字来声明synchronized 块。语法如下:

1
2
3
synchronized(syncObject){  
//允许访问控制的代码  
} 

3. 同步一个类

1
2
3
4
5
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

1
2
3
4
5
6
7
8
9
10
public class SynchronizedExample {

public void func2() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
1
2
3
4
5
6
7
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

synchronized (this)的理解说明

  • 当一个线程正在执行object的一个synchronized(this)同步代码块时,该线程就获得了这个object的对象锁。
  • 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。  
  • 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。  
  • 但是,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的除synchronized(this)同步代码块以外的部分。

总结

同步块、同步方法的锁定说明:

  • 对于同步的方法或者代码块来说,必须获得对象锁才能够进入同步方法或者代码块进行操作;
  • 如果采用普通方法级别的同步,则对象锁即为该方法所在的对象,如果是静态方法,对象锁即指该方法所在的类的锁(类的锁,对所有实例化对象都是唯一的)。
  • 对于代码块,对象锁即指synchronized(obj)中的obj;
  • 静态方法则一定会同步,非静态方法需在单例模式才生效,推荐用静态方法

实现同步的一些技巧

  搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。 还有一些技巧可以让我们对共享资源的同步访问更加安全:

  • 定义private的instance变量(成员变量)+对应的get()方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
  • 如果instance变量是一个对象(如数组或ArrayList),那上述方法仍然不安全,因为当外界对象通过get()方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get()方法也加上synchronized同步,并且,只返回这个private对象的clone(),这样,调用端得到的就是对象副本的引用了。
  • 还有,比较常用的就有:Collections.synchronizedMap(new HashMap()),当然这个MAP就是生命在类中的全局变量,就是一个线程安全的HashMap,web的application是全web容器公用的,所以要使用线程安全来保证数据的正确。

ava中多线程锁释放的条件:

  • 执行完同步代码块,就会释放锁。(synchronized)
  • 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。(exception)
  • 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进入对象的等待池。(wait)

比较

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

3. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

ReentrantLock 可中断,而 synchronized 不行。

4. 公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象。

3、Conditional条件对象

通常,线程拿到锁对象之后,却发现需要满足某一条件才能继续向下执行。

拿银行程序来举例子,我们需要转账方账户有足够的资金才能转出到目标账户,这时候需要用到ReentrantLock对象,因为如果我们已经完成转账方账户有足够的资金的判断之后,线程被其他线程中断,等其他线程执行完之后,转账方的钱又没有了足够的资金,这时候因为系统已经完成了判断,所以会继续向下执行,然后银行系统就会出现问题。

举例:

1
2
3
4
public void Transfer(int from, int to, double amount) {
if (Accounts[from] > amount)//系统在结束判断之后被剥夺运行权,然后账户通过网银转出所有钱,银行凉凉
DoTransfer(from, to, amount);
}

这时候我们就需要使用ReentrantLock对象了,我们修改一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void Transfer(int from, int to, double amount) {
ReentrantLock locker = new ReentrantLock();
locker.lock();
try {
while (Accounts[from] < amount) {
//等待有足够的钱
}
DoTransfer(from, to, amount);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
}

但是这样又有了问题,当前线程获取了锁对象之后,开始执行代码,发现钱不够,进入等待状态,然后其他线程又因为锁的原因无法给该账户转账,就会一直进入等待状态。

这个问题如何解决呢?

条件对象登场!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void Transfer(int from, int to, double amount) {
ReentrantLock locker = new ReentrantLock();
Condition sufficientFunds = locker.newCondition();//条件对象,
lock.lock();
try {
while (Accounts[from] < amount) {
sufficientFunds.await();
//等待有足够的钱
}
DoTransfer(from, to, amount);
sufficientFunds.signalAll();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
}

条件对象的关键字是:Condition一个锁对象可以有一个或多个相关的条件对象。可以通过锁对象.newCondition方法获得一个条件对象.

在进入锁之前,我们创建一个条件,然后如果金额不足,在这里调用条件对象的await方法,通知系统当前线程进入挂起状态,让其他线程执行。这样你这次调用会被锁定,然后系统可以再次调用该方法给其他账户转账,当每一次转账完成后,执行转账操作的线程在底部调用signalAll通知所有线程可以继续运行了,因为我们有可能是转足够的钱给当前账户,这时候有可能该线程会继续执行(不一定是你,是通知所有线程,如果通知的线程还是不符合条件,会继续调用await方法,并完成转账操作,然后通知其他挂起的线程。

你说为啥不直接通知当前线程?不行,可以调用signal方法只通知一个线程,但是如果这个线程操作的账户还是没钱(不是转账给这个账户的情况),那这个线程又进入等待了,这时候已经没有线程能通知其他线程了,程序死锁,所以还是用signal比较保险。

以上是使用ReentrantLock+Condition对象,那你说我要是使用synchronized隐式锁怎么办?

也可以,而且不需要

1
2
3
4
5
6
7
8
public void Transfer(int from, int to, double amount) {
while (Accounts[from] < amount) {
wait();//这个wait方法是定义在Object类里面的,可以直接用,和条件对象的await一样,挂起线程
//等待有足够的钱
}
DoTransfer(from, to, amount);
notifyAll();//通知其他挂起的线程
}

Object类里面定义了wait、notifyAll、notify方法,对应await、signalAll和signal方法,用来操作隐式锁,synchronized只能有一个条件,而ReentrantLock显式声明的锁可以用绑定多个Condition条件.

4、同步代码块

即有synchronized关键字修饰的语句块。

被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

1
2
3
4
5
public void method3(SomeObject obj){
synchronized(obj) { //锁定的是对象obj的对象锁
//…..
}
}

这时,锁就是obj这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以按以上方式来写程序;当没有明确的对象作为锁时,但还想让一段代码同步时,可以创建一个特殊的instance变量(必须是一个对象)来充当锁,此时代码如下:

1
2
3
4
5
6
7
8
class Foo implements Runnable{
private byte[] lock = new byte[0]; // 特殊的instance变量;也可以用String常量作为锁
Public void methodA(){
synchronized(lock) { //… }
}
//…..
}
//注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
1
2
3
4
5
6
7
8
Object locker = new Object();
synchronized (locker) {
*//do some work*
}
*//也可以直接锁当前类的对象*
sychronized(this){
*//do some work*
}

以上代码会获得Object类型locker对象的锁,这种锁是一个特殊的锁,在上面的代码中,创建这个Object类对象只是单纯用来使用其持有的锁.

这种机制叫做同步块,应用场景也很广:有的时候,我们并不是整个一个方法都需要同步,只是方法里的部分代码块需要同步,这种情况下,我们如果将这个方法声明为synchronized,尤其是方法很大的时候,会造成很大的资源浪费。所以在这种情况下我们可以使用synchronized关键字来声明同步块:

1
2
3
4
5
6
public void Method() {
//do some work without synchronized
synchronized (this) {
//do some synchronized operation
}
}

例如:

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
public class Bank {  

private int count =0;//账户余额

//存钱
public void addMoney(int money){

synchronized (this) {
count +=money;
}
System.out.println(System.currentTimeMillis()+"存进:"+money);
}

//取钱
public void subMoney(int money){

synchronized (this) {
if(count-money < 0){
System.out.println("余额不足");
return;
}
count -=money;
}
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}

//查询
public void lookMoney(){
System.out.println("账户余额:"+count);
}
}

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

5、监视器的概念

锁和条件是同步中一个很重要的工具,但是它们并不是面向对象的。多年来,Java的研究人员努力寻找一种方法,可以在不需要考虑如何加锁的情况下,就能保证多线程的安全性。最成功的的一个解决方案叫做monitor监视器,这个对象内置于每一个Object变量中,相当于一个许可证。拿到许可证就可以进行操作,没有拿到则需要阻塞等待。

监视器具有以下特性:

1.监视器是只包含私有域的类

2.每个监视器对象都有一个相关的锁

3.使用监视器对象的锁对所有的方法进行加锁(举个例子:如果调用obj.Method方法,obj对象的锁会在方法调用的时候自动获得,当方法结束或返回之后会自动释放该锁因为所有的域都是私有的,这样可以确保一个线程在操作类对象的时候,没有其他线程可以访问里面的域

4.该锁对象可以有任意多个相关条件

其实我们使用的synchronized关键字就是使用了monitor来实现加锁解锁,所以又被称为内部锁因为Object类实现了监视器,所以对象又被内置于任何一个对象之中。这就是我们为什么可以使用synchronized(locker)的方式锁定一个代码块了,其实只是用到了locker对象中内置的monitor而已。每一个对象的monitor类又是唯一的,所以就是唯一的许可证,拿到许可证的线程才可以执行,执行完后释放对象的monitor才可以被其他线程获取。

举个例子:

1
2
3
synchronized (this) {
//do some synchronized operation
}

它在字节码文件中会被编译为:

1
2
3
monitorenter;//get monitor,enter the synchronized block
//do some synchronized operation
monitorexit;//leavel the synchronized block,release the monitor

6、死锁

产生死锁的必要条件:

互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

为什么倾向于使用signalAll和notifyAll方式,如果假设使用signal和notify,随机选择的线程发现自己还是不能运行,那么它再次被阻塞。这样就又会造成死锁现象。

7、锁测试和超时

线程在调用lock方法获得另一个线程持有的锁的时候,很可能发生阻塞。应该更加谨慎的申请锁,tryLock方法试图申请一个锁,如果申请成功,返回true,否则,立刻返回false,线程就会离开去做别的事,而不是被阻塞等待锁对象。

语法:

1
2
3
4
5
6
7
8
9
10
11
12
ReentrantLock locker = new ReentrantLock();
if (locker.tryLock()) {
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
} else {
//do other work
}

也可以给其指定超时参数,单位有SECONDSMILLISECONDSMICROSEONDSMANOSECONDS.

1
2
3
4
5
6
7
8
9
10
11
12
ReentrantLock locker = new ReentrantLock();
if (locker.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
} else {
//do other work
}

lock方法不能被中断,如果一个线程在调用了lock方法后等待锁的时候被中断,中断线程在获得锁之前一直处于阻塞状态。

如果带有超时参数的tryLock方法,那么如果等待期间线程被中断,会抛出InterruptedException异常,这是一个很好的特性,允许程序打破死锁。

8、读写锁

eentrantLock类属于java.util.concurrent.locks包,这个包底下还有一个ReentrantReaderWriterLock类,如果使用多线程对数据读的操作很多,但是写的操作很少的话,可以使用这个类。

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
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():

public void Read() {
Lock readLocker = rwl.readLock();//创建读取锁对象
readLocker.lock();//使用读取锁对象加锁
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
readLocker.unlock();
}
}

public void Write() {
Lock writeLocker = rwl.writeLock();//创建写入锁对象
writeLocker.lock();//使用写入锁对象加锁
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
writeLocker.unlock();
}
}

9、使用特殊域变量(volatile)实现线程同步

• volatile关键字为域变量的访问提供了一种免锁机制;

• 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;

• 因此每次使用该域就要重新计算,而不是使用寄存器中的值;

• volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

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
public class SynchronizedThread {

class Bank {

private volatile int account = 100;

public int getAccount() {
return account;
}

/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}

/**
* 用同步代码块实现
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}

class NewThread implements Runnable {
private Bank bank;

public NewThread(Bank bank) {
this.bank = bank;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" +bank.getAccount());
}
}

}

/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}

public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。

10、final变量

上一节已经了解到,除非使用锁或volatile修饰符,否则无法从多个线程安全地读取一个域。

还有一种情况可以安全地访问一个共享域,即这个域声明为final时。考虑以下声明:

finalMap<String,Double〉accounts=newHashKap<>0;

其他线程会在构造函数完成构造之后才看到这个accounts变量。

如果不使用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是看到null,而不是新构造的HashMap。

当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步

11、线程的局部变量

线程间有时要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。

例如,SimpleDateFormat类不是线程安全的。

1
2
3
public static final SimpleDateFormat dateFormat = 
new SimpleDateFormat("yyyy-MM-dd");

如果两个线程都执行以下操作:

1
String dateStamp = dateFormat.format(new Date());

结果可能很混乱,因为dateFormat使用的内部数据结构可能会被并发的访问所破坏。当然可以使用同步,但开销很大;或者也可以在需要时构造一个局部SimpleDateFormat对象,不过这也太浪费了。

要为每个线程构造一个实例,可以使用以下代码:

1
2
public static final ThreadLocal<SimpleDateFormat> dateFormat = 
ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));

要访问具体的格式化方法,可以调用:

1
String dateStamp = dateFormat.get().format(new Date());

在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。

在多个线程中生成随机数也存在类似的问题。java.util.Random类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,这会很低效。

可以使用ThreadLocal辅助类为各个线程提供一个单独的生成器,不过Java SE 7还另外提供了一个便利类。只需要做以下调用:

1
int random = ThreadLocalRandom.current().nextInt(upperBound);

ThreadLocalRandom.current()调用会返回特定于当前线程的Random类实例。

六、线程通信

1、借助于Object类的wait()、notify()和notifyAll()实现通信

​ 线程执行wait()后,就放弃了运行资格,处于冻结状态;

​ 线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
​ notifyall(), 唤醒线程池中所有线程。
注: (1) wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
​ (2) wait(),notify(),notifyall(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。

单个消费者生产者例子如下:

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
class Resource{  //生产者和消费者都要操作的资源  
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
if(flag)
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
this.notify();
}
public synchronized void out(){
if(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
this.notify();
}
}
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res=res;
}
public void run(){
while(true){
res.set("商品");
}
}
}
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res=res;
}
public void run(){
while(true){
res.out();
}
}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
t1.start();
t2.start();
}
}//运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。

七、同步器J.U.C-AQS

多线程并发的执行,之间通过某种 共享 状态来同步,只有当状态满足 xxxx 条件,才能触发线程执行 xxxx 。这个共同的语义可以称之为同步器。

可以认为以上所有的锁机制都可以基于同步器定制来实现的。

而juc(java.util.concurrent)里的思想是 将这些场景抽象出来的语义通过统一的同步框架来支持。

juc 里所有的这些锁机制都是基于 AQS ( AbstractQueuedSynchronizer )框架上构建的。下面简单介绍下 AQS( AbstractQueuedSynchronizer )。 可以参考Doug Lea的论文The java.util.concurrent Synchronizer Framework(http://gee.cs.oswego.edu/dl/papers/aqs.pdf)

Java中多线程开发时,离不开线程的分工协作,常用的多线程的同步器有如下几种:

1、CountDownLatch(倒计时门闩)

应用场景:等待一组线程任务完成后在继续执行当前线程。

用法:定义一个CountDownLatch变量latch,在当前线程中调用latch.await()方法,在要等待的一组线程中执行完后调用latch.countDown()方法,这样当该线程都调用过latch.countDown()方法后就开始执行当前线程latch.await()后的方法。


倒计时门闩会导致一条或多条线程在“门口”一直等待,直到另一条线程打开这扇门,线程才得以继续运行。他是由一个计数变量和两个操作组成的,这两个操作分别是“导致一条线程等待直到。

计数变为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
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
final static int NTHREADS = 3;
public static void main(String[] args) {
final CountDownLatch startSignal = new CountDownLatch(1);
final CountDownLatch doneSignal = new CountDownLatch(NTHREADS);
Runnable r = new Runnable() {

@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "进入等待");
startSignal.await();//3个线程进入等待,直到startSignal.countDown()被调用
System.out.println(Thread.currentThread().getName() + "开始执行任务");
Thread.sleep(200);
doneSignal.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
ExecutorService es = Executors.newFixedThreadPool(NTHREADS);
for (int i = 0; i < NTHREADS; i++) {
es.execute(r);
}
try {
Thread.sleep(1000);
startSignal.countDown();
//3个线程全部开始执行任务,主线程进入等待
System.out.println(Thread.currentThread().getName() + "进入等待");
//直到3个线程全部结束任务,doneSignal.countDown()被调用,主线程开始执行
doneSignal.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
es.shutdownNow();

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

2、CyclicBarrier(同步屏障)

应用场景:等待一组线程到达某个点后一起执行,该组线程达到指定点后可以再次循环执行。也可用于一组线程达达某个点后再执行某个方法。

用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。

用法:定义一个CyclicBarrier变量barrier,线程达到某个约定点时调用barrier.await()方法,当该组所有线程都调用了barrier.await()方法后改组线程一起向下执行。

CyclicBarrier和CountDownLatch的区别

  • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
  • CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

若有多条线程,他们到达屏障时将会被阻塞,只有当所有线程都到达屏障时才能打开屏障,


所有线程同时执行,若有这样的需求可以使用同步屏障。此外,当屏障打开的同时还能指定执行的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建同步屏障对象,并制定需要等待的线程个数 和 打开屏障时需要执行的任务
CyclicBarrier barrier = new CyclicBarrier(3,new Runnable(){
public void run(){
//当所有线程准备完毕后触发此任务
}
});
// 启动三条线程
for( int i=0; i<3; i++ ){
new Thread( new Runnable(){
public void run(){
// 等待,(每执行一次barrier.await,同步屏障数量-1,直到为0时,打开屏障)
barrier.await();
// 任务
任务代码……
}
} ).start();
}

倒计时门闩 与 同步屏障 的区别 倒计时门闩只会阻塞一条线程,目的是为了让该条任务线程满足条件后执行;

而同步屏障会阻塞所有线程,目的是为了让所有线程同时执行

3、Semaphore(信号量)

应用场景:对于一组有限制都资源访问。比如餐厅有5个位置但同时有7个人要吃饭,则要控制7个人对餐位的并发实用。

用法:定义Semaphore变量semaphore包含受限的资源个数,每个人要来用餐时先调用semaphore.acquire()方法获取一个餐位(若没有餐位,则阻塞等待),用完餐后调用semaphore.release()释放餐位给其它人用。

信号量维护了一组许可证,以约束访问被限制资源的线程数。当没有可用

的许可证时,线程的获取尝试会一直阻塞,直到其它的线程释放一个许可证。

【信号量
一个信号量管理多个许可证。为了通过信号量,线程通过调用acquire()请求许可。其实没有实际的许可对象,信号连也仅仅是维护一个计数器。
许可的数目是固定的,由此限制了线程通过的数量当一个线程执行完之后,应该调用release()释放许可证,让其他线程有机会执行。事实上,
任意一个线程都有可以释放任意个数的许可证,这可能会增加许可证的个数。所以我建议,如果不是非常明确的知道为什么要释放多个许可证,就一定
是让获得许可证的线程是放一个许可证。

【常用方法
1.构造函数:
  Semaphore(int permits):创建具有给定许可数和非公平设置的Semaphore

​   Semaphore(int permits,boolean fair):此类的构造方法可选地接受一个公平 参数。当设置为 false 时(默认也是false),此类不对线程获取许可的顺序做任何保证。

​ 特别地,闯入是允许的,也就是说可以在已经等待的线程前为调用 acquire() 的线程分配一个许可,从逻辑上说,就是新线程将自己置于等待线程队列的头部。
​ 当公平设置为 true 时,信号量保证对于任何调用获取方法的线程而言,都按照处理它们调用这些方法的顺序(即先进先出;FIFO)来选择线程、获得许可。
​ 注意,FIFO 排序必然应用到这些方法内的指定内部执行点。所以,可能某个线程先于另一个线程调用了 acquire,但是却在该线程之后到达排序点,并且从方法返回时也类似。

2.Semaphore还提供一些其他方法:
int availablePermits() :返回此信号量中当前可用的许可证数。
int getQueueLength():返回正在等待获取许可证的线程数。
boolean hasQueuedThreads() :是否有线程正在等待获取许可证。
void reducePermits(int reduction) :减少reduction个许可证。是个protected方法。
Collection getQueuedThreads() :返回所有等待获取许可证的线程集合。是个protected方法。

【补充
当许可证的个数为1时,可以充当互斥锁使用。

示例代码:

只能同时有5个线程访问的信号量

1
`// 创建信号量对象,并给予3个资源Semaphore semaphore = new Semaphore(3);// 开启10条线程for ( int i=0; i<10; i++ ) {    new Thread( new Runnbale(){        public void run(){            // 获取资源,若此时资源被用光,则阻塞,直到有线程归还资源            semaphore.acquire();            // 任务代码            ……            // 释放资源            semaphore.release();        }    } ).start();}`

4、Exchanger交换器

  • 交换值是同步的;
  • 成对的线程之间交换数据;
  • 可看成是双向的同步队列;
  • 可应用于演算法、流水线设计;

Exchanger类中的主要方法就是:exchange(V x)方法,成对的两个线程之间,都调用了该方法,就能在两个线程彼此都准备好数据后,成功的交换数据给对方,然后各自返回。如果想支持成对的两个线程之间,一个没耐性,等的时间过长,或者被打断了就不交换数据了,可以使用exchange(V x, long timeout, TimeUnit unit)方法。

例子:

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
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import java.util.concurrent.atomic.*;

//球线程
class BallTask implements Runnable
{
private Exchanger<String> e;
public BallTask(Exchanger<String> e){
this.e = e;
}

public void run(){
try{
long sleepTime = (long)(Math.random() * 2500) ;
String tName = Thread.currentThread().getName();
System.out.println(tName+"正在买球,用时["+sleepTime+"]才买到球,赶紧去换鱼...");
Thread.sleep(sleepTime);
//这里的str即为交换的东西
String str = e.exchange(tName+":的球");
System.out.println("【"+tName+":的球】换到了-->【"+str+"】");
}

catch(Exception e){
}
finally{
}
}
}

//鱼线程
class FishTask implements Runnable
{
private Exchanger<String> e;
public FishTask(Exchanger<String> e){
this.e = e;
}

public void run(){
try{
long sleepTime = (long)(Math.random() * 2500) ;
String tName = Thread.currentThread().getName();
System.out.println(tName+"正在钓鱼,用时["+sleepTime+"]才钓到鱼,赶紧去换球...");
Thread.sleep(sleepTime);
String str = e.exchange(tName+":的鱼");
System.out.println("【"+tName+":的鱼】换到了-->【"+str+"】");
}
catch(Exception e){
}
finally{
}
}
}

public class ExchangerTest
{
public static void main(String[] args)
{
Exchanger<String> e = new Exchanger<String>();

BallTask bTask = new BallTask(e); //任务:球线程
FishTask fTask = new FishTask(e); //任务:鱼线程

Thread bThread = new Thread(bTask,"Ball");
Thread fThread = new Thread(fTask,"Fish");

bThread.start();
fThread.start();

System.out.println("我是主线程,准备看看你们交易情况...\n\r");

try{
//Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。
bThread.join();
fThread.join();
}catch(Exception ep){}

System.out.println("\n\r我是主线程,已看到你们的交易结果...");
}
}

Exchanger和Semaphore区别
Exchanger交换器和Semaphore信号量在关于生产者消费者《产1消1模式》运用的区别:

1·Exchanger交换器:成对的两个线程,各个线程有各个线程的自己数据V,A线程拥有V1,B线程拥有V2,V1<…>V2互换。
2·Semaphore信号量:成对的两个线程,只需一个数据池即可,生产者生产数据注入数据池,消费者从数据池取走数据消费。
3·Exchanger交换器:两个线程之间的通讯仅仅一个Exchanger实例即可。
4·Semaphore信号量:两个线程之间的通讯需要两个信号量,生产信号指示灯,消费信号指示灯。
5·Exchanger和Semaphore的共同点:两个线程之间需要同步通讯。生产的过快,没用,必须等消费完了,才能进行下一生产1;同理,消费的过快,也没用,必须等生产完了,才能进行下一消费1。

5、同步队列与等待队列

书上:

【同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用SynchronousQueue的put方法时,它会阻塞直到另一个线程调用take方法为止,反之亦然。与Exchanger的情况不同,数据仅仅沿一个方向传递,从生产者到消费者。即使SynchronousQueue类实现了BlockingQueue接口,概念上讲,它依然不是一个队列。它没有包含任何元素,它的size方法总是返回0。】

简单的理解是同步队列存放着竞争同步资源的线程的引用(不是存放线程),而等待队列存放着待唤醒的线程的引用。

同步队列中存放着一个个节点,当线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点并将其加入同步队列,首节点表示的获取同步状态成功的线程节点。

Condition维护着一个等待队列与同步队列相似。主要针对await和signal的操作。

例子:

这里实现了三个多线程的run方法。A线程输出A然后通知B,然后B通知C。

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
public static class ThreadA extends Thread{
@Override
public void run(){
try{
lock.lock();
System.out.println("A进程输出" + " : " + ++index);
conditionB.signal();
conditionA.await();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}

public static class ThreadB extends Thread{
@Override
public void run(){
try{
lock.lock();
System.out.println("B进程输出" + " : " + ++index);
conditionC.signal();
conditionB.await();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}

public static class ThreadC extends Thread{
@Override
public void run(){
try{
lock.lock();
System.out.println("C进程输出" + " : " + ++index);
conditionA.signal();
conditionC.await();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CondtionTest {

public static ReentrantLock lock = new ReentrantLock();
public static Condition conditionA = lock.newCondition();
public static Condition conditionB = lock.newCondition();
public static Condition conditionC = lock.newCondition();
public static int index = 0;
public static void main(String[] args){
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
ThreadC threadC = new ThreadC();

threadA.start();//(1)
threadB.start();//(2)
threadC.start();//(3)
}
}

当(1)(2)(3)三个线程被调用时,因为三个线程同时竞争lock,这里假设线程A拿到了lock(线程A虽然是看起来是先start(),但是正在的调用还是看调度程序的,所以这里只能假设是A线程拿到同步资源)。首节点表示的是正在操作同步资源的线程。所以现在的同步队列是:

接着线程A输出了:“A进程输出 : 1”。然后调用conditionB.signal(),其实这一步的signal是没什么意义的,因为conditionB现在没有线程是可以被唤醒的。
当conditionA.await()被执行到的时候,线程A同步队列中被移除,对应操作是锁的释放; 线程A(节点A)接着被加入到ConditionA等待队列,因为线程需要singal信号。

同步队列

A等待队列

现在在同步队列中的首节点是B节点,那么B线程占用了同步资源就可以开始运行了。先是输出“B进程输出 : 2”,同样的signal操作也是没有意义的,因为conditionC是没有可以被唤醒的线程。当conditionB.await()被执行到的时候,线程B同步队列中被移除,线程B(节点B)接着被加入到ConditionB等待队列

同步队列

B等待队列

终于轮到了C线程占用同步资源了,再输出“C进程输出:3”之后,调用conditionA.signal(),注意这个signal是有用的
因为在conditionA的等待队列中A线程是在等待的,把它取出来加入到同步队列中去竞争,但是这个时候线程A还没唤醒。首节点还是C

同步队列

接着conditionC.await()被执行。线程C同步队列中被移除,线程C(节点C)接着被加入到ConditionC等待队列

同步队列

C等待队列

注意到同步队列中的首节点已经变回了节点A了。所以线程A在刚刚等待的地方继续执行,最后释放了lock。但是线程B和线程C最后也没有其他线程去唤醒,状态一直为WAITING,而线程A的状态为TERMINATED。

6、定时器

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行,在Java中,可以通过Timer和TimerTask类来实现定义调度的功能。

Timer类

1
2
3
4
5
public Timer()默认的构造方法
public void schedule(TimerTask task, long delay) 在指定的延迟后执行指定的任务
public void schedule(TimerTask task,long delay,long period) 计划重复固定延迟执行指定的任务,在指定的延迟后开始
public void schedule(TimerTask task, Date time) 在指定的时间计划指定的任务
public void schedule(TimerTask task, Date firstTime, long period) 计划重复固定延迟执行指定的任务,在指定的开始时间

TimerTask类

1
2
public abstract void run()	定时器任务执行的动作
public boolean cancel() 取消此定时器任务

代码演示

1、输出爆炸啦

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

//继承TimerTask,需要重新rum方法
public class Time extends TimerTask {
@Override
public void run() {
System.out.println("爆炸啦");
}
}
import java.util.Timer;

public class TimeTest {
public static void main(String[] args) {
//new一个Timer对象,用来调方法
Timer timer = new Timer();
//调用Timer对象的方法schedule,第一个参数必须是TimerTask对象,Time继承了它因此也是这个对象,第二个参数表示在2秒后运行run方法,这个参数只有在第一次使用run方法,最后一个参数是每隔1秒,运行一次run方法
timer.schedule(new Time(),2000,1000);
}
}

这个结果是,不停的输出爆炸啦,要想停掉的话,可以加入cancel()方法,在run方法最后加入这个话,结果会输出一个爆炸啦,因为执行第一次完后就执行到这一语句,定时器就取消了,如下。

2、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javapublic class Time extends TimerTask {
//定义一个Timer对象,到时候可以用它来调用cancel方法
Timer time;
//通过构造器给Timer对象赋值
public Time(Timer time){
this.time=time;
}
@Override
public void run() {
System.out.println("爆炸啦");
//取消定时器
time.cancel();
}
}
public class TimeTest {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new Time(timer),2000,1000);
}
}

Maven

  • 构建工具:自动化构建,抽象构建过程
  • 可跨平台——对外提供了一致的操作接口

Maven

  • 构建工具:自动化构建,抽象构建过程

    • 可跨平台——对外提供了一致的操作接口
  • 依赖管理工具和项目管理工具

    • 提供中央仓库,自动下载构件

最新最全的maven依赖项版本查询网站:

http://mvnrepository.com

一、为什么使用Maven这样的构建工具【why】

  1. 一个项目就是一个工程

如果项目非常庞大,就不适合使用package来划分模块,最好是每一个模块对应一个工程,利于分工协作。借助于maven就可以将一个项目拆分成多个工程

  1. 项目中使用jar包,需要“复制”、“粘贴”项目的lib中

同样的jar包重复的出现在不同的项目工程中,你需要做不停的复制粘贴的重复工作。借助于maven,可以将jar包保存在“仓库”中,不管在哪个项目只要使用引用即可就行。

  1. jar包需要的时候每次都要自己准备好或到官网下载

借助于maven我们可以使用统一的规范方式下载jar包,规范

  1. jar包版本不一致的风险

不同的项目在使用jar包的时候,有可能会导致各个项目的jar包版本不一致,导致未执行错误。借助于maven,所有的jar包都放在“仓库”中,所有的项目都使用仓库的一份jar包。

  1. 一个jar包依赖其他的jar包需要自己手动的加入到项目中

FileUpload组件->IO组件,commons-fileupload-1.3.jar依赖于commons-io-2.0.1.jar

极大的浪费了我们导入包的时间成本,也极大的增加了学习成本。借助于maven,它会自动的将依赖的jar包导入进来。

二、maven是什么【what】

① maven是一款服务于java平台的自动化构建工具

make->Ant->Maven->Gradle

② 构建

构建定义:把动态的Web工程经过编译得到的编译结果部署到服务器上的整个过程。

编译:java源文件[.java]->编译->Classz字节码文件[.class]

部署:最终在sevlet容器中部署的不是动态web工程,而是编译后的文件

③ 构建的各个环节

  • 清理clean:将以前编译得到的旧文件class字节码文件删除
  • 编译compile:将java源程序编译成class字节码文件
  • 测试test:自动测试,自动调用junit程序
  • 报告report:测试程序执行的结果
  • 打包package:动态Web工程打War包,java工程打jar包
  • 安装install:Maven特定的概念—–将打包得到的文件复制到“仓库”中的指定位置
  • 部署deploy:将动态Web工程生成的war包复制到Servlet容器下,使其可以运行

三、安装maven

① 当前系统是否配置JAVA_HOME的环境变量

② 下载maven,解压maven放在一个非中文无空格的路径下

③ 配置maven的相关环境变量

  • 在环境变量增加M2_HOME,路径是maven解压后的根目录
  • 在环境变量里的path中增加maven/bin的目录

④ 验证:maven -v 查看maven版本

看到版本信息,恭喜你已经OK了。

maven目录

  • bin目录

该目录包含了mvn运行的脚本,这些脚本用来配置java命令,准备好classpath和相关的Java系统属性,然后执行Java命令。

  • boot目录:

该目录只包含一个文件,该文件为plexus-classworlds-2.5.2.jarplexus-classworlds是一个类加载器框架,相对于默认的java类加载器,它提供了更加丰富的语法以方便配置,Maven使用该框架加载自己的类库。

  • conf目录:

该目录包含了一个非常重要的文件settings.xml。直接修改该文件,就能在机器上全局地定制Maven的行为,一般情况下,我们更偏向于复制该文件至/.m2/目录下(表示用户目录),然后修改该文件,在用户范围定制Maven的行为。

  • lib目录:

该目录包含了所有Maven运行时需要的Java类库,Maven本身是分模块开发的,因此用户能看到诸如maven-core-3.0.jar、maven-model-3.0.jar之类的文件,此外这里还包含一些Maven用到的第三方依赖如commons-cli-1.2.jar、commons-lang-2.6.jar等等。

四、第一个maven

① 创建约定的目录结构(maven工程必须按照约定的目录结构创建)

根目录:工程名
|—src:源码|—|—main:存放主程序|—|—|—java:java源码文件|—|—|—resource:存放框架的配置文件|—|—test:存放测试程序|—pop.xml:maven的核心配置文件

我们按照上面的文件夹目录结构手动创建一下,不用任何IDE环境(手动的其实最有助于我们理解maven)

文件内容如下

在src/main/java/com/hzg/maven目录下新建文件Hello.java,内容如下

1
2
3
4
5
6
7
package com.hzg.maven;
public class Hello {
public String sayHello(String name)
{
return "Hello "+name+"!";
}
}

POM文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hzg.maven</groupId>
<artifactId>Hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Hello</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

常用maven命令

  • mvn clean:表示运行清理操作(会默认把target文件夹中的数据清理)。
  • mvn compile:编译主程序,会将代码编译到target文件夹中。
  • mvn test-compile:编译测试程序
  • mvn test:执行测试
  • mvn package:打包
  • mvn install:安装

执行maven命令必须进入到pom.xml的目录中进行执行

进入到项目的pom.xml目录之后,就可以执行啦。

1、运行 mvn compile

OK,运行完毕,你在pom.xml配置的依赖的包已经导入到仓库了,问题来了,仓库默认的位置在哪?

仓库的默认位置:c:\Usrs[登录当前系统的用户名].m2\repository

刚才执行完compile之后,之前的文件夹发生了变化

我们发现Hello项目里里多了一个target文件夹。文件夹的内容为:

发现target里主要存放的就是编译后的字节码文件

2、运行mvn test-compile,target文件夹下面除了classes之外多了test-classes文件夹

3、运行mvn package,target文件夹下面又多了一个打好的jar包

4、运行mvn clean,发现整个target文件夹都没了。又回到了编译之前我们手动创建的文件夹

五、Maven插件安装,基于IDEA

  1. 引入下载好的maven。网上教程很多

Maven使用

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tengj</groupId>
<artifactId>springBootDemo1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springBootDemo1</name>
</project>

代码的第一行是XML头,指定了该xml文档的版本和编码方式。

  • project是所有pom.xml的根元素,它还声明了一些POM相关的命名空间及xsd元素。

  • 根元素下的第一个子元素modelVersion指定了当前的POM模型的版本,对于Maven3来说,它只能是4.0.0 代码中最重要是包含了groupId,artifactId和version了。这三个元素定义了一个项目基本的坐标,在Maven的世界,任何的jar、pom或者jar都是以基于这些基本的坐标进行区分的。

  • groupId定义了项目属于哪个组,随意命名,比如谷歌公司的myapp项目,就取名为 com.google.myapp

  • artifactId定义了当前Maven项目在组中唯一的ID,比如定义hello-world。

  • version指定了项目当前的版本0.0.1-SNAPSHOT,SNAPSHOT意为快照,说明该项目还处于开发中,是不稳定的。

  • name元素生命了一个对于用户更为友好的项目名称,虽然这不是必须的,但还是推荐为每个POM声明name,以方便信息交流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project>
...
<dependencies>
<dependency>
<groupId>实际项目</groupId>
     <artifactId>模块</artifactId>
     <version>版本</version>
     <type>依赖类型</type>
     <scope>依赖范围</scope>
     <optional>依赖是否可选</optional>
     <!—主要用于排除传递性依赖-->
     <exclusions>
     <exclusion>
        <groupId></groupId>
          <artifactId></artifactId>
       </exclusion>
     </exclusions>
  </dependency>
<dependencies>
...
</project>
  • dependencies 可以包含一个或者多个dependency元素,以声明一个或者多个项目依赖。
  • grounpId、artifactId和version 组成了依赖的基本坐标。
  • type 指定了依赖的类型,默认为 jar。
  • scope 指定了依赖的范围(详情见下面依赖范围部分)。
  • optional 标记了依赖是否是可选的(详情见下面依赖可选部分)。
  • exclusions 用来排除传递性依赖(详情见下面依赖排除部分)。

六、仓库

仓库的分类:

本地仓库

一般来说,在Maven项目目录下,没有诸如lib/这样用来存放依赖文件的目录。当Maven在执行编译或测试时,如果需要使用依赖文件,它总是基于坐标使用本地仓库的依赖文件。

默认情况下,不管是Window还是macOS,或者是 Linux,每个用户都会在自己的用户目录下有一个路径名为 .m2/repository/ 的仓库目录。

如果你想自定义本地仓库目录地址,可以编辑文件~/.m2/settings.xml,设置localRepository元素的值为你想要的仓库地址,例如:

1
2
3
<settings>
<localRepository>D:\java\repository\</localRepository>
</settings>

这样,该用户的本地仓库地址就被设置成了 D:\java\repository\。 需要注意的是,默认情况下,~/.m2/settings.xml文件不存在,用户需要从Maven安装目录复制$M2_HOME/conf/settings.xml文件再进行编辑。

远程仓库-中央仓库

默认情况下,本地仓库是被注释掉的,也就是空的,那么就必须得给 Maven 配置一个可用的远程仓库,否则 Maven 在 build(构建)的时候就无法去下载依赖。

中央仓库就是这样一个可用的远程仓库,里面包含了这个世界上绝大多数流行的开源 Java 类库,以及源码、作者信息、许可证信息等等。

远程仓库-aliyun仓库

不过,默认的中央仓库访问速度比较慢,通常我们会选择使用阿里的 Maven 远程仓库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<repositories>
<repository>
<id>ali-maven</id>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>
复制代码
  • repositories 可以包含一个或者多个repository元素,以声明一个或者多个仓库。
  • id,仓库声明的唯一id,需要注意的是,Maven自带的中央仓库使用的id为central,如果其他仓库也使用了该id,就会覆盖中央仓库的配置。
  • url,指向了仓库的地址。
  • releases和snapshots,用来控制Maven对于发布版构件和快照版构件的下载权限。
  • enabled子元素为 true 时表示可以从仓库下载发布版构件和快照版构件。
  • updatePolicy 子元素用来配置Maven从远处仓库检查更新的频率。
    • 默认值是daily,表示每天检查一次;
    • 可选值 never 表示从不检查;
    • 可选值always表示每次构建时检查更新;
    • 可选值interval表示每隔X分钟检查一次更新(X为任意整数)。
  • checksumPolicy 子元素用来配置Maven检查校验的策略。在下载构件的时候,Maven会去校验,如果校验失败,
    • 当checksumPolicy的值为默认的warn时,Maven会在执行构建时输出警告信息;
    • 值为fail 时,Maven遇到校验错误就让构建失败;
    • 值为ignore时,Maven将完全忽略校验。

远程仓库-私服

私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的Maven用户使用。当Maven需要下载构件的时候,它从私服请求,如果私服上不存在该构件,则从外部的远程仓库下载,缓存在私服上之后,再为Maven的下载请求提供服务。因此,一些无法从外部仓库下载到的构件也能从本地上传到私服上供大家使用。 私服的好处:

  • 节省自己的外网速度
  • 加速Maven构建
  • 部署第三方构建
  • 提高稳定性,增强控制
  • 降低中央仓库的负荷

远程仓库的配置

在平时的开发中,我们往往不会使用默认的中央仓库,默认的中央仓库访问的速度比较慢,访问的人或许很多,有时候也无法满足我们项目的需求,可能项目需要的某些构件中央仓库中是没有的,而在其他远程仓库中有,如JBoss Maven仓库。这时,可以在pom.xml中配置该仓库,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 配置远程仓库 -->
<repositories>
<repository>
<id>jboss</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.com/maven2/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
  • **repository:**在repositories元素下,可以使用repository子元素声明一个或者多个远程仓库。
  • id:仓库声明的唯一id,尤其需要注意的是,Maven自带的中央仓库使用的id为central,如果其他仓库声明也使用该id,就会覆盖中央仓库的配置。
  • name:仓库的名称,让我们直观方便的知道仓库是哪个,暂时没发现其他太大的含义。
  • url:指向了仓库的地址,一般来说,该地址都基于http协议,Maven用户都可以在浏览器中打开仓库地址浏览构件。
  • **releases和snapshots:**用来控制Maven对于发布版构件和快照版构件的下载权限。需要注意的是**enabled**子元素,该例中releases的enabled值为true,表示开启JBoss仓库的发布版本下载支持,而snapshots的enabled值为false,表示关闭JBoss仓库的快照版本的下载支持。根据该配置,Maven只会从JBoss仓库下载发布版的构件,而不会下载快照版的构件。
  • layout:元素值default表示仓库的布局是Maven2及Maven3的默认布局,而不是Maven1的布局。基本不会用到Maven1的布局。
  • 其他:对于releases和snapshots来说,除了enabled,它们还包含另外两个子元素updatePolicy和checksumPolicy。

1:元素updatePolicy用来配置Maven从远处仓库检查更新的频率,默认值是daily,表示Maven每天检查一次。其他可用的值包括:never-从不检查更新;always-每次构建都检查更新;interval:X-每隔X分钟检查一次更新(X为任意整数)。
2:元素checksumPolicy用来配置Maven检查校验和文件的策略。当构建被部署到Maven仓库中时,会同时部署对应的检验和文件。在下载构件的时候,Maven会验证校验和文件,如果校验和验证失败,当checksumPolicy的值为默认的warn时,Maven会在执行构建时输出警告信息,其他可用的值包括:fail-Maven遇到校验和错误就让构建失败;ignore-使Maven完全忽略校验和错误。

远程仓库的认证

大部分的远程仓库不需要认证,但是如果是自己内部使用,为了安全起见,还是要配置认证信息的。 配置认证信息和配置远程仓库不同,远程仓库可以直接在pom.xml中配置,但是认证信息必须配置在settings.xml文件中。这是因为pom往往是被提交到代码仓库中供所有成员访问的,而settings.xml一般只存在于本机。因此,在settings.xml中配置认证信息更为安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
<settings>
...
<!--配置远程仓库认证信息-->
<servers>
<server>
<id>releases</id>
<username>admin</username>
<password>admin123</password>
</server>
</servers>
...
</settings>

这里除了配置账号密码之外,值关键的就是id了,这个id要跟你在pom.xml里面配置的远程仓库repository的id一致,正是这个id将认证信息与仓库配置联系在了一起。

部署构件至远程仓库

我们自己搭建远程仓库的目的就是为了可以方便部署我们自己项目的构件以及一些无法从外部仓库直接获取的构件,供其他团队成员使用。 Maven除了能对项目进行编译、测试、打包之外,还能将项目生成的构件部署到远程仓库中。首先,需要编辑项目的pom.xml文件。配置distributionManagement元素,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<distributionManagement>
<repository>
<id>releases</id>
<name>public</name>
<url>http://59.50.95.66:8081/nexus/content/repositories/releases</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<name>Snapshots</name>
<url>http://59.50.95.66:8081/nexus/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>
  • repository表示表示发布版本(稳定版本)构件的仓库
  • snapshotRepository表示快照版本(开发测试版本)的仓库。
  • 这两个元素都需要配置id、name和url.
    • id为远程仓库的唯一标识
    • name是为了方便人阅读
    • url表示该仓库的地址。

配置好了就运行命令mvn clean deploy,Maven就会将项目构建输出的构件部署到配置对应的远程仓库,如果项目当前的版本是快照版本,则部署到快照版本的仓库地址,否则就部署到发布版本的仓库地址。 当前项目是快照还是发布版本是通过 true 这个来区分的。

镜像

如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。用过Maven的都知道,国外的中央仓库用起来太慢了,所以选择一个国内的镜像就很有必要,我推荐国内的阿里云镜像。 阿里云镜像:配置很简单,修改conf文件夹下的settings.xml文件,添加如下镜像配置:

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

上例子中,的值为central,表示该配置为中央库的镜像,任何对于中央仓库的请求都会转至该镜像,用户也可以用同样的方法配置其他仓库的镜像

这里介绍下<mirrorOf>配置的各种选项

  • <mirrorOf>*<mirrorOf>:匹配所有远程仓库。
  • <mirrorOf>external:*<mirrorOf>:匹配所有远程仓库,使用localhost的除外,使用file://协议的除外。也就是说,匹配所有不在本机上的远程仓库。
  • <mirrorOf>repo1,repo2<mirrorOf>:匹配仓库repo1h和repo2,使用逗号分隔多个远程仓库。
  • <mirrorOf>*,!repo1<mirrorOf>:匹配所有远程仓库,repo1除外,使用感叹号将仓库从匹配中排除。

需要注意的是,由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven仍将无法访问被镜像仓库,因而将无法下载构件。

maven 坐标

在 maven 中,根据 groupIdartifactIdversion 组合成 groupId:artifactId:version 来唯一识别一个 jar 包。

  • groupId - 团体、组织的标识符。团体标识的约定是,它以创建这个项目的组织名称的逆向域名(reverse domain name)开头。一般对应着 java 的包结构。

  • artifactId - 单独项目的唯一标识符。比如我们的 tomcat、commons 等。不要在 artifactId 中包含点号(.)。

  • version

    - 一个项目的特定版本。

    • maven 有自己的版本规范,一般是如下定义 major version、minor version、incremental version-qualifier ,比如 1.2.3-beta-01。要说明的是,maven 自己判断版本的算法是 major、minor、incremental 部分用数字比较,qualifier 部分用字符串比较,所以要小心 alpha-2 和 alpha-15 的比较关系,最好用 alpha-02 的格式。

    • maven 在版本管理时候可以使用几个特殊的字符串 SNAPSHOT、LATEST、RELEASE。比如

      1
      1.0-SNAPSHOT

      。各个部分的含义和处理逻辑如下说明:

      • SNAPSHOT - 这个版本一般用于开发过程中,表示不稳定的版本。
      • LATEST - 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个 snapshot 版,具体看哪个时间最后。
      • RELEASE :指最后一个发布版。
  • packaging - 项目的类型,描述了项目打包后的输出,默认是 jar。常见的输出类型为:pom, jar, maven-plugin, ejb, war, ear, rar, par。

maven工程的坐标与仓库中路径的关系:

maven坐标和仓库对应的映射关系:[groupId][artifactId][version][artifactId]-[version].jar

去本地仓库看一下此目录:org\springframework\spring-core\4.3.4.RELEASE\spring-core-4.3.4.RELEASE.jar

在Maven世界中,任何一个依赖、插件或者项目构建的输出,都可以称为构件。得益于坐标机制,任何Maven项目使用任何一个构件的方式都是完全相同的。在此基础上,Maven可以在某个位置统一存储所有Maven项目共享的构件,这个统一的位置就是仓库。

实际的Maven项目将不再各自存储其依赖文件,它们只需要声明这些依赖的坐标,在需要的时候(例如,编译项目的时候需要将依赖加入到classpath中),Maven会自动根据坐标找到仓库中的构件,并使用它们。

为了实现重用,项目构建完毕后可生成的构件也可以安装或者部署到仓库中,供其他项目使用。

七、配置文件说明

参看链接:https://blog.csdn.net/qq877507054/article/details/79138294

1、pom.xml文件

2、settings.xml文件

  • 用来设置Maven参数的配置文件。
  • 是Maven的全局配置文件。
  • 包含类似本地仓库、远程仓库和联网使用的代理信息等配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository/>
<interactiveMode/>
<offline/>
<pluginGroups/>
<servers/>
<mirrors/>
<proxies/>
<profiles/>
<activeProfiles/>
</settings>

在Maven安装目录的conf子目录下面的settings.xml才是真正的全局的配置。

用户目录的.m2子目录下面的settings.xml的配置只是针对当前用户的。

当这两个文件同时存在的时候,那么对于相同的配置信息用户目录下面的settings.xml中定义的会覆盖Maven安装目录下面的settings.xml中的定义。用户目录下的settings.xml文件一般是不存在的,但是Maven允许我们在这里定义我们自己的settings.xml,如果需要在这里定义我们自己的settings.xml的时候就可以把Maven安装目录下面的settings.xml文件拷贝到用户目录的.m2目录下,然后改成自己想要的样子。

通过配置文件中的注释,我们可以看到,有两种配置此文件的方法

1
2
3
4
5
6
7
1.用户级别
${user.home}/.m2/settings.xml
可以通过指令 -s /path/to/user/settings.xml

2.全局级别
${maven.home}/conf/settings.xml.
可以通过指令 -gs /path/to/global/settings.xml

一般情况下,只需要如下两个就够了:

1
2
3
4
5
6
7
<localRepository>E:\config\maven\repository-spring</localRepository>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>

但是在实际开发中,一般都配有自己的私服,所以会加上私服的用户名称及密码

1
2
3
4
5
6
7
8
9
10
11
<offline>true</offline>
<server>
<id>nexus-releases</id>
<username>admin</username>
<password>xxx</password>
</server>
<server>
<id>nexus-snapshots</id>
<username>admin</username>
<password>xxx</password>
</server>

2.1 localRepository

localRepository用于构建系统的本地仓库的路径。
默认的值是${user.home}/.m2/repository。

1
2
3
Default: ${user.home}/.m2/repository

<localRepository>/path/to/local/repo</localRepository>

2.2 interactiveMode

interactiveMode 用于决定maven是否在需要输出的时候提示你,默认true。如果是false,它将使用合理的默认值,或者基于一些设置。

2.3 offline

决定maven是否在构建的时候进行网络传输。
默认false,表示联网状态,true为取消联网。
在某些情况下设置为true是很有用的,比如jar无法从网上下载等

2.4 pluginGroups

pluginGroups 插件组

1
2
3
<pluginGroups>
<pluginGroup>org.mortbay.jetty</pluginGroup>
</pluginGroups>

这样Maven可以使用简单的命令执行org.morbay.jetty:jetty-maven-plugin:run

1
mvn jetty run

2.5 proxies

此项用于设置http代理
有时候由于安全问题,需要配置http代理,通过代理服务才能正常访问外部仓库下载资源可以ping repo1.maven.org来访问中央仓库
telnet 218.14.227.197 3128 来查看代理地址以及端口是否畅通

1
2
3
4
5
6
7
8
9
10
11
12
<proxies>
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol><!--代理协议-->
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
</proxies>
  • id:proxy的唯一标识,用来区别proxy元素。
  • active:表示是否激活代理,如果配置多个,默认是第一个生效
  • username,password:提供连接代理服务器时的认证。
  • host,port:主机地址,端口号
  • nonProxyHosts:用来表示哪些主机名不需要代理,可以用|来分割多个,此外也支持通配符,如:*.goole.com表示所有以goole.com结尾的都不需要通过代理
设置http代理

编辑setting.xml文件 有时候你所在的公司基于安全因素考虑,要求你使用通过安全认证的代理访问因特网。这种情况下,就需要为Maven配置HTTP代理,才能让它正常访问外部仓库,以下载所需要的资源。首先确认自己无法直接访问公共的maven中央仓库,直接运行命令ping repo1.maven.org可以检查网络。如果真的需要代理,先检查一下代理服务器是否畅通。比如现在有一个IP地址为218.14.227.197,端口为3128的代理服务,我们可以运行telnet 218.14.227.197 3128来检测该地址的该端口是否畅通。如果得到出错信息,需要先获取正确的代理服务信息,如果telnet连接正确,则输入ctrl+],然后q,回车,退出即可。

检查完毕之后,编辑~/.m2/settings.xml文件(如果没有该文件,则复制$M2_HOME/conf/settings.xml)。添加代理配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<settings>  
...
<proxies>
<proxy>
<id>my-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>218.14.227.197</host>
<port>3128</port>
<!--
<username>***</username>
<password>***</password>
<nonProxyHosts>
repository.mycom.com|*.google.com
</nonProxyHosts>
-->
</proxy>
</proxies>
...
</settings>

这段配置十分简单,proxies下可以有多个proxy元素,如果声明了多个proxy元素,则默认情况下第一个被激活的proxy会生效。这里声明了一个id为my-proxy的代理,active的值为true表示激活该代理,protocol表示使用的代理协议,这里是http。当然,最重要的是指定正确的主机名(host元素)和端口(port元素)。上述xml配置中注释掉了username,password,nonProxyHosts几个元素。当代理服务需要认证时,就需要配置username和password。nonProxyHost元素用来指定哪些主机不需要代理,可以使用”|”符号来分隔多个主机名。此外,该配置也支持通配符,如:*.google.com表示所有以google.com结尾的域名访问都不要通过代理。

2.6 servers

这是一个认证配置的列表,根据系统中使用的server-id控制。认证配置在maven连接到远程服务时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<servers>
<!--使用登录方式-->
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>

<!-- 使用秘钥认证 -->
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>可空</passphrase>
</server>
</servers>

2.7 mirrors

指定镜像仓库位置用于从远程仓库下载资源

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
</mirrors>
  • id:用于继承和直接查找,唯一
  • mirrorOf:镜像所包含的仓库的Id
  • name:唯一标识,用于区分镜像站
  • url:镜像路径

2.8 profiles

  • settings.xml中时意味着该profile是全局的,所以只能配置范围宽泛一点配置信息,比如远程仓库等。而一些比较细致一点的需要定义在项目的pom.xml中。
  • profile可以让我们定义一系列的配置信息,然后指定其激活条件。
    根据每个profile对应不同的激活条件和配置信息,从而达到不同环境使用不同配置。
  • 例子:通过profile定义jdk1.5以上使用一套配置,jdk1.5以下使用另外一套配置;或者通过操作系统来使用不同的配置信息。
  • settings.xml中的信息有repositories、pluginRepositories和properties。定义在properties的值可以在pom.xml中使用。
activation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<profiles>
//可定义多个profile,使用activeProfiles激活
<profile>
<id>test</id>
<activation>
<activeByDefault>false</activeByDefault>
<jdk>1.5</jdk>
<os>
<name>Windows XP</name>
<family>Windows</family>
<arch>x86</arch>
<version>5.1.2600</version>
</os>
<property>
<name>mavenVersion</name>
<value>2.0.3</value>
</property>
<file>
<exists>${basedir}/file2.properties</exists>
<missing>${basedir}/file1.properties</missing>
</file>
</activation>
</profile>
</profiles>
  • jdk:检测到对应jdk版本就激活

  • os:针对不同操作系统

  • property:当maven检测到property(pom中如${name}这样的)profile将被激活

  • file:如果存在文件,激活,不存在文件激活

通过以下命令查看哪些profile将生效

1
mvn help:active-profiles
properites

Maven的属性是值占位符,如果X是一个属性的话,在POM中可以使用${X}来进行任意地方的访问。他们来自于五种不同的风格,所有都可以从settings.xml文件中访问到。

1.env.x:“env.”前缀会返回当前的环境变量。如${env.PATH}就是使用了$path环境变量(windosws中的%PATH%)。

2.project.x:一个点“.”分割的路径,在POM中就是相关的元素的值。例如:1.0就可以通过${project.version}来访问。

3.settings.x:一个点“.”分割的路径,在settings.xml中就是相对应的元素的值,例如:false就可以通过${settings.offline}来访问。

4.Java系统属性:通过java.lang.System.getProperties()来访问的属性都可以像POM中的属性一样访问,例如:${java.home}

5.x:被或者外部文件定义的属性,值可以这样访问${someVar}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<profiles>

<profile>

...

<properties>

<user.install>${user.home}/our-project</user.install>

</properties>

...

</profile>

</profiles>

上面这个profile如果被激活,那么在pom中${user.install}就可以被访问了。

实战:

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
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<p.jdbc.url>172.11.2.112:3306/yh2_qr</p.jdbc.url>
<p.jdbc.username>root</p.jdbc.username>
<p.jdbc.password>root</p.jdbc.password>
</properties>
</profile>
<profile>
<id>uat</id>
<properties>
<p.jdbc.url>172.11.2.131:3306/yh2_qr</p.jdbc.url>
<p.jdbc.username>root</p.jdbc.username>
<p.jdbc.password>Cheryfs5t6y89kL</p.jdbc.password>
</properties>
</profile>
</profiles>

jdbc.url=jdbc:mysql://${p.jdbc.url}?characterEncoding=utf-8&allowMultiQueries=true
jdbc.username=${p.jdbc.username}
jdbc.password=${p.jdbc.password}
repositories

repositories是远程项目集合maven用来移植到本地仓库用于构建系统。如果来自本地仓库,Maven调用它的插件和依赖关系。不同的远程仓库可能包含不同的项目,当profile被激活,他们就会需找匹配的release或者snapshot构件。

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
<profiles>

<profile>

...

<repositories>

<repository>

<id>codehausSnapshots</id>

<name>Codehaus Snapshots</name>

<releases>

//发行版

<enabled>false</enabled>

<updatePolicy>always</updatePolicy>

<checksumPolicy>warn</checksumPolicy>

</releases>

<snapshots>

//快照版

<enabled>true</enabled>

<updatePolicy>never</updatePolicy>

<checksumPolicy>fail</checksumPolicy>

</snapshots>

<url>http://snapshots.maven.codehaus.org/maven2</url>

<layout>default</layout>

</repository>

</repositories>

<pluginRepositories>

...

</pluginRepositories>

...

</profile>

</profiles>

1.releases,snapshots:这是各种构件的策略,release或者snapshot。这两个集合,POM就可以根据独立仓库任意类型的依赖改变策略。如:一个人可能只激活下载snapshot用来开发。

2.enable:true或者false,决定仓库是否对于各自的类型激活(release或者snapshot)。

3.updatePolicy:这个元素决定更新频率。maven将比较本地pom的时间戳(存储在仓库的maven数据文件中)和远程的.有以下选择: always, daily (默认), interval:X (x是代表分钟的整型),never.

4.checksumPolicy:当Maven向仓库部署文件的时候,它也部署了相应的校验和文件。可选的为:ignore,fail,warn,或者不正确的校验和。

5.layout:在上面描述仓库的时候,提到他们有统一的布局。Maven 2有它仓库默认布局。然而,Maven 1.x有不同布局。使用这个元素来表明它是default还是legacy。

2.9 activeProfiles

1
2
3
4
5
6
7
<activeProfiles>

<activeProfile>alwaysActiveProfile</activeProfile>

<activeProfile>anotherAlwaysActiveProfile</activeProfile>

</activeProfiles>

每个activeProfile元素对应一个profile id的值,任何profile id被定义到activeProfile的profile将被激活。

参看链接:

https://www.cnblogs.com/hongmoshui/p/10762272.html

https://www.cnblogs.com/szrs/p/15251037.html

4.3、两者的关系

pom.xml文件针对于具体的项目,settings.xml文件作为maven的全局配置文件;满足条件时,会采用settings.xml文件中的配置,如settingsxml文件中的Profiles标签。还有一些比较私密的配置可以写在settings.xml文件中。

  1. Setting.xml中repository的配置与pom.xml中repository的配置有什么不同?

Setting.xml中配置repository与pom.xml中配置repository的作用是相同的,都是为了指定多个存储库的使用(you can specify the use of multiple repositories)。但在pom.xml中配置只对当前项目与子项目有用,而在setting.xml中配置为全局性配置,用于所用的项目。

参看链接

https://www.cnblogs.com/xrq730/p/5530069.html

https://blog.csdn.net/qq877507054/article/details/79138294

监听器

监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行。

监听器组件

监听器涉及三个组件:事件源,事件对象,事件监听器

当事件源发生某个动作的时候,它会调用事件监听器的方法,并在调用事件监听器方法的时候把事件对象传递进去。

我们在监听器中就可以通过事件对象获取得到事件源,从而对事件源进行操作!

简单的监听器

监听器定义为接口,监听的方法需要事件对象传递进来,从而在监听器上通过事件对象获取得到事件源,对事件源进行修改

1
2
3
4
5
6
7
8
9
10
/**
* 事件监听器
*
* 监听Person事件源的eat和sleep方法
*/
interface PersonListener{

void doEat(Event event);
void doSleep(Event event);
}

事件源

事件源是一个Person类,它有eat和sleep()方法。

事件源需要注册监听器(即在事件源上关联监听器对象)

如果触发了eat或sleep()方法的时候,会调用监听器的方法,并将事件对象传递进去

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
   /**
*
* 事件源Person
*
* 事件源要提供方法注册监听器(即在事件源上关联监听器对象)
*/

public class Person {

//在成员变量定义一个监听器对象
private PersonListener personListener ;

//在事件源中定义两个方法
public void Eat() {

/**
* 调用监听器的doeat方法监听Person类对象eat(吃)这个动作,将事件对象Event传递给doeat方法,
* 事件对象封装了事件源,new Event(this)中的this代表的就是事件源
*/
if(personListener!=null)
personListener.doEat(new Event(this));
}

public void sleep() {


if(personListener!=null)
personListener.doSleep(new Event(this));
}

//注册监听器,该类没有监听器对象啊,那么就传递进来吧。
public void registerLister(PersonListener personListener) {
this.personListener = personListener;
}

}

事件对象

事件对象封装了事件源。

监听器可以从事件对象上获取得到事件源的对象(信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 事件对象Even
*
* 事件对象封装了事件源
*
* 在监听器上能够通过事件对象获取得到事件源
*
*
*/
class Event{
private Person person;

public Event() {
}

public Event(Person person) {
this.person = person;
}

public Person getResource() {
return person;
}

Servle监听器

在Servlet规范中定义了多种类型的监听器,它们用于监听的事件源分别 ServletContext, HttpSession和ServletRequest这三个域对象

Servlet规范针对这三个对象上的操作,又把多种类型的监听器划分为三种类型:

  1. 监听域对象自身的创建和销毁的事件监听器。
  2. 监听域对象中的属性的增加和删除的事件监听器。
  3. 监听绑定到HttpSession域中的某个对象的状态的事件监听器。

和其它事件监听器略有不同的是,servlet监听器的注册不是直接注册在事件源上,而是由WEB容器负责注册,开发人员只需在web.xml文件中使用<listener>标签配置好监听器.

监听对象的创建和销毁

HttpSessionListener、ServletContextListener、ServletRequestListener分别监控着Session、Context、Request对象的创建和销毁

  • HttpSessionListener(可以用来收集在线者信息)
  • ServletContextListener(可以获取web.xml里面的参数配置)
  • ServletRequestListener

监听对象属性变化

ServletContextAttributeListener、HttpSessionAttributeListener、ServletRequestAttributeListener分别监听着Context、Session、Request对象属性的变化

这三个接口中都定义了三个方法来处理被监听对象中的属性的增加,删除和替换的事件,同一个事件在这三个接口中对应的方法名称完全相同,只是接受的参数类型不同

  • attributeAdded()

    当向被监听对象中增加一个属性时,web容器就调用事件监听器的attributeAdded方法进行响应,这个方法接收一个事件类型的参数,监听器可以通过这个参数来获得正在增加属性的域对象和被保存到域中的属性对象

  • attributeRemoved()

    当删除被监听对象中的一个属性时,web容器调用事件监听器的attributeRemoved方法进行响应

  • attributeReplaced()

    当监听器的域对象中的某个属性被替换时,web容器调用事件监听器的attributeReplaced方法进行响应

监听Session内的对象

除了上面的6种Listener,还有两种Linstener监听Session内的对象,分别是HttpSessionBindingListener和HttpSessionActivationListener,实现这两个接口并不需要在web.xml文件中注册

  • 实现HttpSessionBindingListener接口,JavaBean 对象可以感知自己被绑定到 Session 中和从 Session 中删除的事件【和HttpSessionAttributeListener的作用是差不多的】

  • 实现HttpSessionActivationListener接口,JavaBean 对象可以感知自己被活化和钝化的事件(当服务器关闭时,会将Session的内容保存在硬盘上【钝化】,当服务器开启时,会将Session的内容在硬盘式重新加载【活化】)

    活化:javabean对象和Session一起被反序列化(活化)到内存中。
    钝化:javabean对象存在Session中,当服务器把session序列化到硬盘上时,如果Session中的javabean对象实现了Serializable接口
    那么服务器会把session中的javabean对象一起序列化到硬盘上,javabean对象和Session一起被序列化到硬盘中的这个操作称之为钝化
    如果Session中的javabean对象没有实现Serializable接口,那么服务器会先把Session中没有实现Serializable接口的javabean对象移除
    然后再把Session序列化(钝化)到硬盘中
    当绑定到 HttpSession对象中的javabean对象将要随 HttpSession对象被钝化之前,
    web服务器调用该javabean对象对象的 void sessionWillPassivate(HttpSessionEvent event)方法
    这样javabean对象就可以知道自己将要和 HttpSession对象一起被序列化(钝化)到硬盘中
    当绑定到HttpSession对象中的javabean对象将要随 HttpSession对象被活化之后,
    web服务器调用该javabean对象的 void sessionDidActive(HttpSessionEvent event)方法
    这样javabean对象就可以知道自己将要和 HttpSession对象一起被反序列化(活化)回到内存中

应用

统计网站在线人数

  1. 监听Session是否被创建了:在网站中一般使用Session来标识某用户是否登陆了,如果登陆了,就在Session域中保存相对应的属性。如果没有登陆,那么Session的属性就应该为空。
  2. 如果Session被创建了,那么在Context的域对象的值就应该+1:在线人数用context对象保存
  3. 如果Session从内存中移除了,那么在Context的域对象的值就应该-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
30
public class CountOnline implements HttpSessionListener {

public void sessionCreated(HttpSessionEvent se) {

//获取得到Context对象,使用Context域对象保存用户在线的个数
ServletContext context = se.getSession().getServletContext();

//直接判断Context对象是否存在这个域,如果存在就人数+1,如果不存在,那么就将属性设置到Context域中
Integer num = (Integer) context.getAttribute("num");

if (num == null) {
context.setAttribute("num", 1);
} else {
num++;
context.setAttribute("num", num);
}
}
public void sessionDestroyed(HttpSessionEvent se) {

ServletContext context = se.getSession().getServletContext();
Integer num = (Integer) se.getSession().getAttribute("num");

if (num == null) {
context.setAttribute("num", 1);
} else {
num--;
context.setAttribute("num", num);
}
}
}
  • 显示页面代码:
1
在线人数:${num}

自定义Session扫描器

移除长时间没有人使用的session。

  1. 创建一个容器装在站点的所有session。
  2. 隔一段时间去扫描一下全部的session,当长时间没有使用的话就将其从内存中移除。
  3. 并发访问的问题:监听Session创建的方法就会被并发访问了定时器扫描容器的时候,可能是获取不到所有的Session的
  • 监听器代码:
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
public class Listener1 implements ServletContextListener,
HttpSessionListener {



//服务器一启动,就应该创建容器。我们使用的是LinkList(涉及到增删)。容器也应该是线程安全的。
List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());

//定义一把锁(Session添加到容器和扫描容器这两个操作应该同步起来)
private Object lock = 1;

public void contextInitialized(ServletContextEvent sce) {


Timer timer = new Timer();
//执行我想要的任务,0秒延时,每10秒执行一次
timer.schedule(new MyTask(list, lock), 0, 10 * 1000);

}
public void sessionCreated(HttpSessionEvent se) {

//只要Session一创建了,就应该添加到容器中
synchronized (lock) {
list.add(se.getSession());
}
System.out.println("Session被创建啦");

}

public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("Session被销毁啦。");
}
public void contextDestroyed(ServletContextEvent sce) {

}
}
  • 任务代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 在任务中应该扫描容器,容器在监听器上,只能传递进来了。
*
* 要想得到在监听器上的锁,也只能是传递进来
*
* */
class MyTask extends TimerTask {

private List<HttpSession> sessions;
private Object lock;

public MyTask(List<HttpSession> sessions, Object lock) {
this.sessions = sessions;
this.lock = lock;
}

@Override
public void run() {

synchronized (lock) {
//遍历容器
for (HttpSession session : sessions) {

面试题

过滤器监听器面试题都在这里 - SegmentFault 思否