杨柳亭

杨柳亭

ByteBuddy

1
2
3
4
5
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.11.12</version>
</dependency>

Hello World

1
2
3
4
5
6
7
8
9
10
11
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World"))
.make()
.load(HelloWorldBuddy.class.getClassLoader())
.getLoaded();
Object instance = dynamicType.newInstance();
String toString = instance.toString();
System.out.println(toString);
System.out.println(instance.getClass().getCanonicalName());

image

从例子可以看出,很简单就创建了一个动态类型。ByteBuddy提供了一套流式API,从ByteBuddy实例出发,可以流畅的完成所有的操作和数据定义。
上面的示例中

  • subclass 指定了新创建的类的父类
  • method 指定了 Object 的 toString 方法
  • intercept 拦截了 toString 方法并返回固定的 value
  • 最后 make 方法生产字节码,有类加载器加载到虚拟机中

此外,Byte Buddy不仅限于创建子类和操作类,还可以转换现有代码。Byte Buddy 还提供了一个方便的 API,用于定义所谓的 Java 代理,该代理允许在任何 Java 应用程序的运行期间进行代码转换

创建动态类

1
2
3
4
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.zlk.learning.bytebuddy.DynamicType")
.make();

上面的示例代码会创建一个继承至 Object 类型的类。这个动态创建的类型与直接扩展 Object 并且没有实现任何方法、属性和构造函数的类型是等价的,如下:

1
2
3
public class DynamicTYpe {

}

在创建类的时候,还提供了更多API来支持对类的定义,包括定义字段、方法等

1
2
3
4
5
6
7
8
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.zlk.learning.bytebuddy.DynamicType")
.defineField("name", String.class, 1)
.defineField("age", Integer.class, 1)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make();

image

上面的示例代码中,我们增加了两个字段name和age,同时拦截了toString方法,使其输出固定值 “Hello World!”。

保留父类实现的接口信息

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


@Override
public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
Class<?> beanClass = beanDefinition.getBeanClass();
ArrayList<TypeDescription.Generic> list = getGenerics(beanClass);
Class clazz = new ByteBuddy().subclass(beanClass).implement(list)
.make()
.load(getClass().getClassLoader())
.getLoaded();
try {
if (null == ctor) return clazz.getDeclaredConstructor().newInstance();
return clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new BeansException("Failed to instantiate [" + clazz.getName() + "]", e);
}
}
//获取父类的接口信息
private static ArrayList<TypeDescription.Generic> getGenerics(Class<?> beanClass) {
ArrayList<TypeDescription.Generic> list = new ArrayList<>();
try {
Type[] genericInterfaces = beanClass.getGenericInterfaces();
for (Type type : genericInterfaces) {
Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
Class<?>[] classes = new Class[typeArguments.length];
for (int i = 0; i < typeArguments.length; i++) {
classes[i] = Class.forName(typeArguments[i].getTypeName());
}
Class<?> aClass = Class.forName(((ParameterizedType) type).getRawType().getTypeName());
TypeDescription.Generic listType = TypeDescription.Generic.Builder.parameterizedType(aClass, classes).build();
list.add(listType);
}
} catch (ClassNotFoundException e) {
throw new BeansException("Failed to instantiate [" + beanClass.getName() + "]", e);
}
return list;
}

加载类

上节创建的 DynamicType.Unloaded,代表一个尚未加载的类,顾名思义,这些类型不会加载到 Java 虚拟机中,它仅仅表示创建好了类的字节码,通过 DynamicType.Unloaded 中的 getBytes 方法你可以获取到该字节码。

在应用程序中,可能需要将该字节码保存到文件,或者注入的现在的 jar 文件中,因此该类型还提供了一个 saveIn(File) 方法,可以将类存储在给定的文件夹中; inject(File) 方法将类注入到现有的 Jar 文件中,另外你只需要将该字节码直接加载到虚拟机使用,你可以通过 ClassLoadingStrategy 来加载。

如果不指定ClassLoadingStrategy,Byte Buffer根据你提供的ClassLoader来推导出一个策略,内置的策略定义在枚举ClassLoadingStrategy.Default中

  • WRAPPER:创建一个新的Wrapping类加载器
  • CHILD_FIRST:类似上面,但是子加载器优先负责加载目标类
  • INJECTION:利用反射机制注入动态类型
1
2
3
Class<?> dynamicClass = dynamicType
.load(Object.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();

我们使用 WRAPPER 策略来加载适合大多数情况的类,这样生产的动态类不会被ApplicationClassLoader加载到,不会影响到项目中已经存在的类
getLoaded 方法返回一个 Java Class 的实例,它就表示现在加载的动态类

拦截方法

在之前的例子中,我们拦截了toString方法,并使其输出固定值。不过在实际开发中很少会遇到如此简单的场景,我们可以通过指定拦截方法的形式来处理复杂的逻辑

通过匹配模式拦截

ByteBuddy 通过 net.bytebuddy.matcher.ElementMatcher 来定义配置策略,可以通过此接口实现自己定义的匹配策略

1
2
3
4
5
6
7
8
9
10
11
12
Foo dynamicFoo = new ByteBuddy()
.subclass(Foo.class)
// 匹配由Foo.class声明的方法
.method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
// 匹配名为foo的方法
.method(named("foo")).intercept(FixedValue.value("Two!"))
// 匹配名为foo,入参数量为1的方法
.method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance();

方法委托

使用MethodDelegation可以将方法调用委托给任意POJO。Byte Buddy不要求Source(被委托类)、Target类的方法名一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Source {
public String hello(String name) { return null; }
}

class Target {

public static String hello(String name) {
return "Hello " + name + "!";
}
}

String helloWorld = new ByteBuddy()
.subclass(Source.class)
.method(named("hello"))
// 此处委托 类只能委托静态方法 对象可使用非静态方法
.intercept(MethodDelegation.to(Target.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance()
.hello("World");

其中 Target 还可以如下实现:

1
2
3
4
5
class Target {
public static String intercept(String name) { return "Hello " + name + "!"; }
public static String intercept(int i) { return Integer.toString(i); }
public static String intercept(Object o) { return o.toString(); }
}

前一个实现因为只有一个方法,而且类型也匹配,很好理解,那么后一个呢,Byte Buddy到底会委托给哪个方法?Byte Buddy遵循一个最接近原则:

  • intercept(int)因为参数类型不匹配,直接Pass
  • 另外两个方法参数都匹配,但是 intercept(String)类型更加接近,因此会委托给它

同时需要注意的是被拦截的方法需要声明为 public,否则没法进行拦截增强。除此之外,还可以使用 @RuntimeType 注解来标注方法

1
2
3
4
5
6
7
8
9
@RuntimeType
public Object interceptor(@This Object proxy, @Origin Method method,
@SuperMethod Method superMethod,
@AllArguments Object[] args) throws Exception {
System.out.println("bytebuddy delegate proxy2 before sing ");
Object ret = superMethod.invoke(proxy, args);
System.out.println("bytebuddy delegate proxy2 after sing ");
return ret;
}

参数绑定

可以在拦截器(Target)的拦截方法 intercept 中使用注解注入参数,ByteBuddy 会根据注解给我们注入对于的参数值。比如

1
2
3
void intercept(Object o1, Object o2)
// 等同于
void intercept(@Argument(0) Object o1, @Argument(1) Object o2)

常用注解有以下这些:

  • @Argument 绑定单个参数
  • @AllArguments 绑定所有参数的数组
  • @This 当前被拦截的、动态生成的那个对象
  • @DefaultCall 调用默认方法而非super的方法
  • @SuperCall 用于调用父类版本的方法
  • @Origin 被拦截的源方法
  • @RuntimeType 可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查
  • @Super 当前被拦截的、动态生成的那个对象的父类对象
  • @FieldValue 注入被拦截对象的一个字段的值

Agent

Java 从 1.5 开始提供了 java.lang.instrument包,该包为检测 Java 程序提供 API,比如用于监控、收集性能信息、诊断问题。通过 java.lang.instrument 实现工具被称为 Java Agent。Java Agent 可以修改类文件的字节码,通常是,在字节码方法插入额外的字节码来完成检测

和通过ByteBuddy实例创建动态类型一样,bytebuddy也提供了AgentBuilder类使我们在agent中更优雅地编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ToStringAgent {
public static void premain(String arguments, Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(isAnnotatedWith(ToString.class))
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder transform(DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classloader) {
return builder.method(named("toString"))
.intercept(FixedValue.value("transformed"));
}
}).installOn(instrumentation);
}
}
  • type 通过ElementMatcher 来匹配我们加载的class,匹配到之后,将会使用
  • transform 指定的转换器来对匹配到的class进行操作

ElementMatcher

ElementMatcher可以定义匹配class的规则,在bytebuddy中,ElementMatchers类提供了许多常规的匹配方式,可以按照class name、注解、类型等来进行匹配,上面的实例中就是使用注解匹配的方式

Junction继承自ElementMatcher接口,定义了and 和 or 方法,可以使我们在定义Matcher时通过链式定义一连串的匹配规则

1
2
3
4
5
6
7
8
9
10
new AgentBuilder.Default()
.type(ElementMatchers.isAnnotatedWith(ToString.class)).and(ElementMatchers.isSubTypeOf(DynamicClass.class)).or(ElementMatchers.named("DynamicClass"))
.transform(new AgentBuilder.Transformer() {
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
return builder
.method(ElementMatchers.named("hello"))
.intercept(MethodDelegation.to(MyServiceInterceptor.class))
;
}
}).installOn(instrumentation);

Transformer

Transformer 接口定义了 transform方法,会传入DynamicType.Builder实例,通过该builder,就可以对匹配到的类进行操作,就和上面讲的 ByteBuddy创建动态类型时类似操作,可以定义字段以及对方法进行拦截操作等,上面的例子就是对匹配到的类的hello方法进行了方法委托,在调用hello方法时,将会委托给 MyServiceInterceptor类

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

@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
System.out.println("intercept:拦截了" + method.getName());
return callable.call();
}

}

END

参考文章
bytebuddy官方文档 https://bytebuddy.net/#/tutorial

https://juejin.cn/post/6844903965553852423#heading-12​​

ThreadLocal

ThreadLocal

ThreadLocal 提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal,比如数据库连接 Connection,每个请求处理线程都需要,但又不相互影响,就是用 ThreadLocal 实现。

为何要Entry使用用弱引用

如果使用强引用,情况会如下:

  • ThreadLocalMap 作为 Thread 的成员变量,会随 Thread 一直保留。
  • ThreadLocalMap 的 key 是 ThreadLocal 对象,如果使用强引用,ThreadLocal 对象会被 ThreadLocalMap 强引用,无法被回收。
  • 而 ThreadLocal 对象可能是唯一对某个对象的引用,这会导致这个对象也无法被回收,发生内存泄露。

使用弱引用可以避免这种情况:

  • 当 ThreadLocal 对象没有其他强引用时,由于 ThreadLocalMap 中的引用是弱引用,ThreadLocal 对象仍然可以被垃圾回收。
  • 垃圾回收 ThreadLocal 对象后,ThreadLocalMap 中对应的 Entry 的 key 变为 null。
  • 之后 ThreadLocalMap 在执行 get、set、remove 等操作时,会忽略 key 为 null 的 Entry,达到清理作用。
  • 这样就避免了 ThreadLocal 对象由于 ThreadLocalMap 的强引用而无法被回收的问题,解决了内存泄露。

ThreadLocal 内存泄漏条件

ThreadLocal被回收&&线程被复用&&线程复用后不再调用ThreadLocal的set/get/remove方法 才可能 发生内存泄露(条件还是相对苛刻)

thread

概述

并发与并行

并行:指两个或多个事件在同一时刻发生(同时发生)。
并发:指两个或多个事件在同一个时间段内发生。
1-1.png

进程、线程

进程是正在运行的程序的实例。
进程是线程的容器,即一个进程中可以开启多个线程。

线程是进程内部的一个独立执行单元;
一个进程可以同时并发运行多个线程;

线程生命周期

1. 新建

  • new 关键字创建了一个线程之后,该线程就处于新建状态
  • JVM 为线程分配内存,初始化成员变量值
  1. 就绪

    • 当线程对象调用了 start()方法之后,该线程处于就绪状态
    • JVM 为线程创建方法栈和程序计数器,等待线程调度器调度
  2. 运行

    • 就绪状态的线程获得 CPU 资源,开始运行 run()方法,该线程进入运行状态
  3. 阻塞

    • 线程在等待进入临界区
  4. 无限期等待

    • 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
  5. 限期等待

    • 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
  6. 死亡

    • 线程会以如下 3 种方式结束,结束后就处于死亡状态:
    • run()或 call()方法执行完成,线程正常结束。
    • 线程抛出一个未捕获的 Exception 或 Error。>调用该线程 stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。

死锁

多个线程因竞争资源而造成的一种僵局(互相等待)

死锁产生必要条件

  1. 互斥条件
    在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待
  2. 不可剥夺条件
    进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
  3. 请求与保持
    进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件
    存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中 Pi 等 待的资源被 P(i+1)占有(i=0, 1, …, n-1),Pn 等待的资源被 P0 占有,如图所示

1-5.png1-4.png

死锁处理

死锁预防

  1. 破坏“互斥”条件
    互斥​条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏互斥​条件。

  2. 破坏“占有并等待”条件
    系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源

    • 方法一:一次性分配资源,即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。
    • 方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源 S 时,须先把它先前占有的资源 R 释放掉,然后才能提出对 S 的申请,即使它可能很快又要用到资源 R。
  3. 破坏“不可抢占”条件
    破坏“不可抢占”条件就是允许对资源实行抢夺。

    • 方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
    • 方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
  4. 破坏“循环等待”条件
    破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

死锁避免

  1. 有序资源分配法
    算法
1
2
3
必须为所有资源统一编号,
同类资源必须一次申请完,
不同类资源必须按顺序申请
  1. 银行家算法
    银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。
    设进程 i 提出请求 REQUEST [i],则银行家算法按如下规则进行判断。1-6.png
1
2
3
4
5
6
7
1. 如果REQUEST [i]<= NEED[i,j],则转(2);否则,出错。
2 .如果REQUEST [i]<= AVAILABLE[i],则转(3);否则,等待。
3 .系统试探分配资源,修改相关数据:
AVAILABLE[i]-=REQUEST[i];//可用资源数-请求资源数
ALLOCATION[i]+=REQUEST[i];//已分配资源数+请求资源数
NEED[i]-=REQUEST[i];//需要资源数-请求资源数
系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待
  1. 顺序加锁
    如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生,但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,所以该种方式只适合特定场景
  2. 限时加锁
    限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
    这种方式有两个缺点:
1
2
1) 当线程数量少时,该种方式可避免死锁,但当线程数量过多,这些线程的加锁时限相同的概率就高很多,可能会导致超时后重试的死循环。
2) Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具

死锁检测

死锁检测算法
1-7.png

1
2
3
4
E 是现有资源向量(existing resource vector),代码每种已存在资源的总数
A 是可用资源向量(available resource vector),那么Ai表示当前可供使用的资源数(即没有被分配的资源)
C 是当前分配矩阵(current allocation matrix),C的第i行代表Pi当前所持有的每一种类型资源的资源数
R 是请求矩阵(request matrix),R的每一行代表P所需要的资源的数量

死锁检测步骤:

1
2
3
4
寻找一个没有结束标记的进程Pi,对于它而言R矩阵的第i行向量小于或等于A。
如果找到了这样一个进程,执行该进程,然后将C矩阵的第i行向量加到A中,标记该进程,并转到第1步
如果没有这样的进程,那么算法终止
算法结束时,所有没有标记过的进程都是死锁进程

死锁恢复

  • 资源剥夺法
    剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除。

  • 进程回退法
    根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制。

  • 进程撤销法

    • 撤销陷入死锁的所有进程,解除死锁,继续运行。
    • 逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。

Java创建线程

  • 继承 Thread 类 extends Thread

  • 实现 Runnable 接口 implements Runnable

  • 实现 Callable 接口 implements Callable

    • 实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
    • 执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。 FutureTask 是 Future 接口的实现类,线程启动 new Thread(objectFutureTask1).start()

Future

  • cancel()方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。

    • 参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。
    • 如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false
    • 如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • isCancelled()方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。

  • isDone()方法表示任务是否已经完成,若任务完成,则返回true;

  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

FutureTask

实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable与Future接口,所以它既可以作为Runnable被线程中执行,又可以作为callable获得返回值。

  • 适合应用场景

    • 执行多任务计算
    • 在高并发环境下确保任务只执行一次

小结

  • 实现接口和继承 Thread 类比较

接口更适合多个相同的程序代码的线程去共享同一个资源。
接口可以避免 java 中的单继承的局限性。
接口代码可以被多个线程共享,代码和线程独立。
线程池只能放入实现 Runable 或 Callable 接口的线程,不能直接放入继承 Thread 的类。
扩充:在 java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。

  • Runnable 和 Callable 接口比较

    • 相同点:

两者都是接口;
两者都可用来编写多线程程序;
两者都需要调用 Thread.start()启动线程;

  • 不同点:

实现 Callable 接口的线程能返回执行结果;而实现 Runnable 接口的线程不能返回结果;
Callable 接口的 call()方法允许抛出异常;而 Runnable 接口的 run()方法的不允许抛异常;
实现 Callable 接口的线程可以调用 Future.cancel 取消执行 ,而实现 Runnable 接口的线程不能

  • 注意点:

Callable 接口支持返回执行结果,此时需要调用 FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

实现Runnable接口,在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。

并发处理


多线程特性

  • 原子性
    • 一个或多个操作要么全部执行并且执行过程中不会被任何因素打断,要么就不执行。
  • 可见性
    • 当多个线程同时访问一个变量时,一个线程修改了这个变量,其他线程能够立即看见修改的值。单线程不存在可见性问题。
  • 有序型
    • 程序的执行按照代码的先后顺序执行

对象的发布和逸出

发布

"发布( Publish)"一个对象的意思是指,使对象能够在当前作用域之外的代码中使用

  1. 一个指向该对象的引用保存到其他代码可以访问的地方
1
2
3
4
5
6
7
8
9
public class Person(){
private String name;
private int age;
}

public class Record{
Person p;
}

  1. 在某一个非私有的方法中返回该引用
1
2
3
4
5
6
7
8
public class Person(){
private String name;
private int age;

public Person get(){
return new Person();
}
}
  1. 将引用传递到其他类的方法中
1
2
3
4
5
6
7
8
9
10
public class Person(){
private String name;
private int age;
}

public class Record{
private void getPersonMessage(Person p){
.....
}
}

逸出

当某个不应该发布的对象被发布时,这种情况就被称为逸出( Escape).

  1. 内部的可变状态逸出
1
2
3
4
5
6
7
class UnsafeStates{
private String[] states = {"AK","AL",...};
public String[] getStates(){
return states;
}
}
//数组states本事私有的变量,但是以public公有的方式发布出去,导致states已经逸出了它所在的作用域,任何调用者都能修改这个数组的内容
  1. 隐式地使用this引用导致逸出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
dosomething(e);
}
})
//在这里count初始化为1
count = 1;
}
}
//当ThisEscape发布EventListener时,也隐含地发布了ThisEscape实例本身。因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用。
//this逸出会导致ThisEscape也发布出去,也就是ThisEscape还没有构建完成就发布出去,也就是count=1;这一句还没执行就发布了ThisEscape对象,如果要使用count时,很有可能会出现对象不一致的状态

使用工厂方法来防止this引用在构造函数过程中逸出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SafeListener{
private final EventListener listener;
private SafeListener(){
listener = new EventListener(){
public void onEvent(Event e){
dosomething(e);
}
};
}
public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
//保证在对象为构造完成之前,是不会发布该对象

线程封闭

  1. Ad-hoc线程封闭。

维护线程封闭性的职责完全由程序实现承担,可用性不高。

Ad-hoc 线程封闭下的一个特例适用于 volatile 变量。 只要确保 volatile 变量仅从单个线程写入,就可以安全地对共享 volatile 变量执读 - 改 - 写操作。

  1. 栈封闭。

局部变量,无并发问题,在项目中使用最多,简单说就是局部变量,方法的变量都拷贝到线程的堆栈中,只有这个线程能访问到。尽量少使用全局变量(变量不是常量)

  1. ThreadLocal类。

原子类

Java 的 java.util.concurrent.atomic 包里面提供了很多可以进行原子操作的类,分为以下四类:

  • 原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
  • 原子更新数组:AtomicIntegerArray、AtomicLongArray
  • 原子更新引用:AtomicReference、AtomicStampedReference 等
  • 原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater

提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。

非原子问题演示

i++并不是原子操作

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

public class AtomicClass {
static int n = 0;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = 0;
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最终值是:"+n);
j++;
}

}
}

结果不一定全是 2000

非原子问题的原子解决

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

public class AtomicClass {
static AtomicInteger n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicInteger(0);
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最终值是:"+n);
j++;
}

}
}

原理

1-10.png

CAS 的 ABA 问题

当前内存的值一开始是 A,被另外一个线程先改为 B 然后再改为 A,那么当前线程访问的时候发现是 A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。如下图:

1-11.png

AtomicStampedReference 解决 ABA 问题

1
2
3
4
AtomicStampedReference(初始值,时间戳):构造函数设置初始值和时间戳
getStamp:获取时间戳
getReference:获取预期值
compareAndSet(预期值,更新值,预期时间戳,更新时间戳):实现CAS时间戳和预期值对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicClass {
static AtomicStampedReference<Integer> n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicStampedReference<Integer>(0,0);
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
Integer reference;
do{
stamp = n.getStamp();
reference = n.getReference();
} while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
Integer reference;
do{
stamp = n.getStamp();
reference = n.getReference();

} while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最终值是:"+n.getReference());
j++;
}

}
}

注意:采用 AtomicStampedReference 会降低性能,慎用。

并发锁

根据分类标准我们把锁分为以下 7 大类别,分别是:

  • 偏向锁/轻量级锁/重量级锁:这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。

    • 偏向锁

      • 如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
    • 重量级锁

      • 重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
    • 轻量级锁

      • JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
  • 可重入锁/非可重入锁;

    • 不可重入锁:线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。
    • 可重入锁:线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。
  • 共享锁/独占锁;

    • 共享锁:同一把锁可以被多个线程同时获得,
    • 独占锁:锁只能同时被一个线程获得。
  • 公平锁/非公平锁;

    • 公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。
    • 非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。
  • 悲观锁/乐观锁;

    • 悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。
    • 乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。
  • 自旋锁/非自旋锁;

    • 自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。
    • 非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
  • 可中断锁/不可中断锁。

    • 在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。

Synchronized

原理:

每个对象都有一个monitor锁,当一个monitor被线程持有后会处于锁定状态,使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。当执行monitorenter指令时,monitor计数器加一,取锁成功,当monitor的计数器为0时释放锁。

Synchronized方法锁为该方法所在的对象本身,静态Synchronized方法从Class对象获取

Synchronized是可重入锁

当一个线程请求方法时,会去检查锁状态,如果锁状态是0,代表该锁没有被占用,直接进行CAS操作获取锁,将线程ID替换成自己的线程ID。如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法。如果是非重入锁,就会进入阻塞队列等待。释放锁时,可重入锁,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。释放锁时,非可重入锁,线程退出方法,直接就会释放该锁。

对象的内存布局

jvm默认开启class pointer压缩 为4字节
普通对象 markword 默认八个字节 instance data 若属性则为零 padding 补足被8整除
image.png
数组对象
image.png

实现

MONITORENTER 进入锁
MONITOREXIT 退出锁
在jvm执行过程中实现锁升级

锁升级

锁状态 25位 31位 1位 4bit 1bit(偏向锁位) 2bit锁标志
无锁(new) unused hashcode(如果调用) unused 分代年龄 0 0 1
锁状态 54位 2位 1位 4bit 1bit(偏向锁位) 2bit锁标志
偏向锁 当前线程指针 Epoch unused 分代年龄 1 0 1
锁状态 62位 2bit锁标志
轻量锁,自旋锁 指向线程栈中Lock Record的指针 0 0
重量级锁 指向互斥量(重量级锁)的指针 1 0
GC标记信息 CMS过程中用到的标记信息 1 1

image.png

锁升级流程(hashcode存储到Lock Record)

  1. 先new一个对象
  2. 若有一个线程进入,将线程id放入对象头中(偏向锁)
  3. 若再来一个线程,将使用CAS进行锁竞争,并这小偏向锁标识(自旋锁)(CAS等待自旋会消耗CPU)
  4. jdk1.6之前为自旋10次货自旋线程大于CPU核心的一半则进入重量锁 之后则为jvm自适应锁

为何要升级到重量锁:升级重量锁后,其他线程进入等待状态,不消耗CPU,但是申请重量锁需要与内核打交道,会消耗资源。

锁粗化

jvm检测到一连串操作都对同一对象加锁,此时jvm就会将锁范围加到一连串操作的外围(比如for循环append)

锁消除

一个资源为非共享资源,jvm会自动把锁去除(比如只在一个方法内使用StringBuffer的append方法)

显式锁

1-12.png

1
2
3
Lock和ReadWriteLock是两大锁的根接口
Lock 接口支持重入、公平等的锁规则:实现类 ReentrantLock、ReadLock和WriteLock。
ReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock。

Lock 类

ReentrantLock

ReentrantLock方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//传入boolean值,true时create一个公平锁,false为非公平锁
ReentrantLock(boolean fair)

//查看有多少线程等待锁
int getQueueLength()

//是否有线程等待抢锁
boolean hasQueuedThreads()

//是否有指定线程等待抢锁
boolean hasQueuedThread(Thread thread)

//当前线程是否抢到锁。返回0代表没有
int getHoldCount()

//查询此锁是否由任何线程持有
boolean isLocked()

//是否为公平锁
boolean isFair()

Condition 方法:

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
public interface Condition {
/**
*Condition线程进入阻塞状态,调用signal()或者signalAll()再次唤醒,
*允许中断如果在阻塞时锁持有线程中断,会抛出异常;
*重要一点是:在当前持有Lock的线程中,当外部调用会await()后,ReentrantLock就允许其他线程来抢夺锁当前锁,
*注意:通过创建Condition对象来使线程wait,必须先执行lock.lock方法获得锁
*/
void await() throws InterruptedException;

//Condition线程进入阻塞状态,调用signal()或者signalAll()再次唤醒,不允许中断,如果在阻塞时锁持有线程中断,继续等待唤醒
void awaitUninterruptibly();

//设置阻塞时间,超时继续,超时时间单位为纳秒,其他同await();返回时间大于零,表示是被唤醒,等待时间并且可以作为等待时间期望值,小于零表示超时
long awaitNanos(long nanosTimeout) throws InterruptedException;

//类似awaitNanos(long nanosTimeout);返回值:被唤醒true,超时false
boolean await(long time, TimeUnit unit) throws InterruptedException;

//类似await(long time, TimeUnit unit)
boolean awaitUntil(Date deadline) throws InterruptedException;

//唤醒指定线程
void signal();

//唤醒全部线程
void signalAll();
}

Condition是Lock上的一个条件,可以多次newCondition()获得多个条件,Condition可用于线程间通信,通过Condition能够更加精细的控制多线程的休眠与唤醒

ReentrantLock.Condition线程通信注意点:

  1. 使用ReentrantLock.Condition的signal()、await()、signalAll()方法使用之前必须要先进行lock()操作[记得unlock()],类似使用Object的notify()、wait()、notifyAll()之前必须要对Object对象进行synchronized操作;否则就会抛IllegalMonitorStateException;
  2. 注意在使用**ReentrantLock.Condition中使用signal()、await()、signalAll()方法,不能和Object的notify()、wait()、notifyAll()方法混用,否则抛出IllegalMonitorStateException`;

ReentrantReadWriteLock

读写锁的实现分析

读写锁因为也是维护一个整数状态(volatile修饰),但是因为是一个整数,需要保证读写,两种状态,所以对32位整型进行了高16位和低16位的设计,其中高16为代表读锁,低16为代表写锁。

  • 写锁的获取与释放
    写锁是一个可重入的锁,允许同一个线程重复获取锁,所以在获取锁的逻辑中会进行是否当前线程以及是否有其他读线程的状态判断。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0),或者该线程不是已经获得了写锁的线程,则当前线程进入等待状态。
    写锁的释放和ReentrantLock的释放很像,当释放锁时,需要对状态递减一,并且前一次写线程修改对后续读线程可见。

  • 读锁的获取与释放
    读锁是一个可重入锁,在没有其他写线程时,读锁总是可以成功的获取,安全地进行状态加1。state这个值可以判断整个读锁被获得了多少次,而每个线程获取读锁的次数,是存储在ThreadLocal中的。
    读锁的每次释放均线程安全的减少读状态

锁降级

锁降级指的是一个线程先拥有写锁,然后拥有读锁,在对数据处理完成后,释放写锁,此时该线程所持有的锁是读锁,从写锁降级为读锁。

自旋锁

CAS (Compare and Swap(比较与交换))

CAS是实现自旋锁的基础

CAS的原理:

线程从内存中取值i 赋值给k 对k进行运算,运算完毕后与内存中的i值进行比较,若相等证明无线程修改i ,结果正确,更新结果值。若不相等则证明有其他线程修改过该值,结果不正确进行重试。

自旋锁

自旋锁和非自旋锁的获取锁的过程。

image

自旋锁,它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。

非自旋锁,如果它发现此时获取不到锁,它就把自己的线程切换状态,让线程休眠,然后 CPU 就可以在这段时间去做很多其他的事情,直到之前持有这把锁的线程释放了锁,于是 CPU 再把之前的线程恢复回来,让这个线程再去尝试获取这把锁。如果再次失败,就再次让线程休眠,如果成功,一样可以成功获取到同步资源的锁。

  • 优点

    • 自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。
  • 缺点

    • 虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。也就是说,虽然一开始自旋锁的开销低于线程切换,但是随着时间的增加,这种开销也是水涨船高,后期甚至会超过线程切换的开销,得不偿失。

注意事项

虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。

ABA问题

适应场景

适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。

问题

Synchronized 和 Lock 区别

1
2
3
4
5
6
synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

线程池

线程池

  1. Executor 接口:
    声明了 execute(Runnable runnable)方法,执行任务代码
  2. ExecutorService 接口:
    继承 Executor 接口,声明方法:submit、invokeAll、invokeAny 以及 shutDown 等
  3. AbstractExecutorService 抽象类:
    实现 ExecutorService 接口,基本实现 ExecutorService 中声明的所有方法
  4. ScheduledExecutorService 接口:
    继承 ExecutorService 接口,声明定时执行任务方法
  5. ThreadPoolExecutor 类:
    继承类 AbstractExecutorService,实现 execute、submit、shutdown、shutdownNow 方法
  6. ScheduledThreadPoolExecutor 类:
    继承 ThreadPoolExecutor 类,实现 ScheduledExecutorService 接口并实现其中的方法
  7. Executors 类:
    提供快速创建线程池的方法

Executor 框架与线程池

Executor 框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable 等

Executor 接口中之定义了一个方法 execute(Runnable command),该方法接收一个 Runable 实例,它用来执行一个任务

ExecutorService 接口继承自 Executor 接口,它提供了更丰富的实现多线程的方法,比如

  1. submit()方法可以返回Future对象,可以调用isDone()方法查询Future是否已完成,当任务完成时,它具有一个结果,可以通过get()方法获取到该结果,Future提供了cancel()方法用来取消执行pending中的任务。
  2. shutdown()方法允许之前已经提交的任务执行完毕之后再挂起关闭,shutdownNow()方法则阻止正在等待的任务启动,并尝试停止之前正在执行的任务。

多线程的缺点:

  • 处理任务的线程创建和销毁都非常耗时并消耗资源。
  • 多线程之间的切换也会非常耗时并消耗资源。
    解决方法:采用线程池
  • 使用时线程已存在,消除了线程创建的时耗
  • 通过设置线程数目,防止资源不足

ThreadPoolExecutor 的全参构造函数参数

在 Java 中创建线程池常用的类是 ThreadPoolExecutor,该类的全参构造函数如下:

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {

参数介绍:

  • corePoolSize​:线程池中核心线程数的最大值

  • maximumPoolSize​:线程池中能拥有最多线程数

  • workQueue​:用于缓存任务的阻塞队列,对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的阻塞队列,在线程池中常用的阻塞队列有以下 2 种:

    • SynchronousQueue<Runnable>​:此队列中不缓存任何一个任务。向线程池提交任务时,如果没有空闲线程来运行任务,则入列操作会阻塞。当有线程来获取任务时,出列操作会唤醒执行入列操作的线程。从这个特性来看,SynchronousQueue​ 是一个无界队列,因此当使用 SynchronousQueue​ 作为线程池的阻塞队列时,参数 maximumPoolSizes​ 没有任何作用。
    • LinkedBlockingQueue<Runnable>​:顾名思义是用链表实现的队列,可以是有界的,也可以是无界的,但在 Executors​ 中默认使用无界的。
  • keepAliveTime​:表示空闲线程的存活时间。

  • unit​:表示 keepAliveTime​ 的单位。

  • handler​:表示当 workQueue​ 已满,且池中的线程数达到 maximumPoolSize​ 时,线程池拒绝添加新任务时采取的策略。一般可以采取以下四种取值。

    1. 如果没有空闲的线程执行该任务且当前运行的线程数少于 corePoolSize​,则添加新的线程执行该任务。
    2. 如果没有空闲的线程执行该任务且当前的线程数等于 corePoolSize​ 同时阻塞队列未满,则将任务入队列,而不添加新的线程。
    3. 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于 maximumPoolSize​,则创建新的线程执行任务。
    4. 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于 maximumPoolSize​,则根据构造函数中的 handler​ 指定的拒绝策略来拒绝新的任务。
  • threadFactory​:指定创建线程的工厂

四种线程池拒绝策略

ThreadPoolExecutor.AbortPolicy()

抛出 RejectedExecutionException

ThreadPoolExecutor.CallerRunsPolicy()

由向线程池提交任务的线程来执行该任务

ThreadPoolExecutor.DiscardOldestPolicy()

抛弃最旧的任务(最先提交而没有得到执行的任务)

ThreadPoolExecutor.DiscardPolicy()

抛弃当前的任务

阻塞队列

  • ArrayBlockingQueue​:基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长的数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费者不能完全并行。长度是需要定义的,可以指定先进先出或者先进后出,因为长度是需要定义的,所以也叫有界队列,在很多场合非常适合使用。
  • LinkedBlockingQueue​:基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效地处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作完全并行运行。需要注意一下,它是一个无界队列。
  • SynchronousQueue​:一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且立刻消费。
  • PriorityBlockingQueue​:基于优先级别的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,需要注意的是它也是一个无界的队列。
  • DelayQueue​:带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue中的元素必须先实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、任务超时处理、空闲连接的关闭等等

image

四种常用线程池(ExecutorService的实现类)

ThreadPoolExecutor 构造函数的参数很多,使用起来很麻烦,为了方便的创建线程池,JavaSE 中又定义了 Executors 类,Eexcutors 类提供了四个创建线程池的方法,分别如下

newCachedThreadPool

该方法创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
此类型线程池特点是:

  1. .工作线程的创建数量几乎没有限制(其实也有限制的,数目为 Interger. MAX_VALUE)
  2. 空闲的工作线程会自动销毁,有新任务会重新创建
  3. 在使用 CachedThreadPool 时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

newFixedThreadPool

该方法创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
优点:具有线程池提高程序效率和节省创建线程时所耗的开销。
缺点:在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

newSingleThreadExecutor

该方法创建一个单线程化的 Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO,优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。
单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

newScheduleThreadPool

该方法创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

FixedThreadPool、SingleThreadPool:使用的是无界队列(LinkedBlockingQueue),当任务堆积很多时,会占用大量内存,最终导致OOM。ChachedTheadPool:可以无限创建线程(Integer.MAX_VALUE),任务过多时会导致创建线程达到操作系统上线或者发生OOM。ScheduledThreadPool、SingleThreadScheduledPool:使用的是DelayedWorkQueue队列,实质上也是一种无界队列,会导致OOM。

问题

为什么java的线程池当核心线程满了以后,先往blockingQueue中存任务,queue满了以后

起到一个缓冲的作用。最大线程数这个参数更像是无奈之举,在最坏的情况下做最后的努力,去新建线程去帮助消化任务。但是这种情况可能存在丢任务的情况。

线程间通讯

线程通信

线程间通信常用方式

  1. 休眠唤醒
    1. object 的 wait,notify,notifyAll
    2. Condition 的 await,signal,signalAll
  2. CountDownLatch:用于某个线程 A 等待若干个其他线程执行完之后,它才执行
    • CountDownLatch 这个类能够使一个线程等待其他线程完成各自的工作后再执行。
    • CountDownLatch 是通过一个计数器来实现的,计数器的初始值为线程的数量。 1-8.png

每当一个线程完成了自己的任务后,计数器的值就会减 1。当计数器值到达 0 时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
3. CyclicBarrier:一组线程等待至某个状态之后再全部同时执行

  • CyclicBarrier 实现让一组线程等待至某个状态之后再全部同时执行。
  • CyclicBarrier 底层是 ReentrantLock 和 Condition 实现
  1. Semaphore:用于控制对某组资源的访问权限

Object 和 Condition 休眠唤醒区别

  1. object wait()必须在 synchronized(同步锁)下使用,
  2. object wait()必须要通过 Nodify()方法进行唤醒
  3. condition await() 必须和 Lock(互斥锁/共享锁)配合使用
  4. condition await() 必须通过 signal() 方法进行唤醒

sleep 和 wait 区别

wait sleep
同步 只能在同步上下文中调用 wait 方法,否则会抛出 llegalMonitorstateException 异常 不需要在同步方法或同步代码块调用
作用对象 wait 方法定义在 Object 类中,作用于对象本身 sleep 方法定义在 java.lang.thread 中,作用于当前线程
释放锁资源
唤醒条件 其他线程调用对象的 notify()或者 notifyAll()方法 超时或调用 Interrupt()方法体
方法属性 wait 是实例方法 sleep 是静态方法

wait 和 notify 区别

  • wait 和 notify 都是 Object 中的方法
  • wait 和 notify 执行前线程都必须获得对象锁
  • wait 的作用是使当前线程进行等待
  • notify 的作用是通知其他等待当前线程的对象锁的线程

Volatile 关键字

非原子性的64位操作

非Volatile修饰的64位变量(double和long类型),JVM允许将64位的读操作和写操作分为两个32位的操作,如果这两个操作在不同线程执行,可能就会导致数据丢失。在多线程中使用共享且可变的long和double类型变量,必须使用Volatile修饰或加锁,否则会有线程安全问题。

原理

Java把处理器多级抽象化为JMM,及线程私有化的工作内存和线程共有的主内存,每个线程从主内村拷贝所需数据到自己的工作内存处理,在重新写回主内存。volatile原理就是当线程修改volatile修饰的变量时,要立即写入内存,当线程读取被volatile修饰的变量时,要立即到主存中读取,保证可见性。

作用

一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)

  • 禁止进行指令重排序。(保证变量所在行的有序性)

    • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
    • 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行

内存屏障

MESI CPU缓存一致性协议
屏障两边的指令不可重排
image.png
image.png应用场景

基于 volatile 的作用,使用 volatile 必须满足以下两个条件:

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中
  • 在访问变量时不需要加锁
    常见应用场景如下:
    状态量标记:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}


volatile boolean inited = false;
//线程1:
context = loadContext(); 
inited = true;           
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

双重校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

局限

Volatile修饰变量只能保证可见性,不能保证原子性

Java虚拟机栈栈帧

java虚拟机以方法作为最基本的执行单位。“栈帧”是用于支持虚拟机进行方法调用和方法执行背后的数据结构。栈帧存储了方法的局部变量表,操作数栈,动态链接和方法返回地址等信息。
image.png

栈帧的概念结构

局部变量表

  • 局部变量表是一组变量值的存储空间,局部变量表以变量槽为单位
  • java虚拟机规范中未规定一个变量槽应该占有内存空间大小,只是有导向的说明没个变量槽都应该存放一个boolean,byte,char,short,int,float,reference,returnAddress类型的数据

reference 表示对一个对象的引用,java虚拟机规范中并没有明确指出reference类型的长度。
虚拟机至少通过这个应用确认两件事:

  1. 根据引用直接或间接的查找到对象在java堆中的数据存放的起始地址或索引
  2. 根据引用直接或间接的查找对象所属数据类型在方法区中的存储的类型信息

returnAddress现在基本很少出现

操作数栈

image.png
_ 两个栈帧之间的数据共享 _

动态链接

方法返回地址

附加信息

Java虚拟机栈栈帧

java虚拟机以方法作为最基本的执行单位。“栈帧”是用于支持虚拟机进行方法调用和方法执行背后的数据结构。栈帧存储了方法的局部变量表,操作数栈,动态链接和方法返回地址等信息。
image.png

栈帧的概念结构

局部变量表

  • 局部变量表是一组变量值的存储空间,局部变量表以变量槽为单位
  • java虚拟机规范中未规定一个变量槽应该占有内存空间大小,只是有导向的说明没个变量槽都应该存放一个boolean,byte,char,short,int,float,reference,returnAddress类型的数据

reference 表示对一个对象的引用,java虚拟机规范中并没有明确指出reference类型的长度。
虚拟机至少通过这个应用确认两件事:

  1. 根据引用直接或间接的查找到对象在java堆中的数据存放的起始地址或索引
  2. 根据引用直接或间接的查找对象所属数据类型在方法区中的存储的类型信息

returnAddress现在基本很少出现

操作数栈

image.png
_ 两个栈帧之间的数据共享 _

动态链接

方法返回地址

附加信息

垃圾回收和内存分配

垃圾回收

垃圾判别算法

引用计数法

一个对象A,只要有任何一个对象引用了A ,则A 的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A 的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

  • 优点

    • 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点

    • 需要单独的字段存储计数器,增加了存储空间的开销。
    • 每次赋值伴随着计数器的增减,增加时间开销。
    • 无法处理循环依赖的问题

可达性分析

将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。

基本思路:

  • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

可作为"GC Roots"的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程调用的方法堆中的参数,局部变量,临时变量。
  • 方法区中类静态属性引用的对象,例如java 类中的引用类型的静态变量
  • 方法区中常量引用的对象,例如字符串常量池中的引用
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用,例如基础类型对应的Class对象,一些常驻的异常对象,还有系统类加载器
  • 所有被同步锁持有的对象。
  • 反应java虚拟机内部情况的JMXBean,JVMTI中的注册的回调,本地代码缓存等(???)

注:如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行,这点也是导致GC进行时必须“Stop The World”的一个重要原因。

垃圾回收算法

标记清除

标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

  • 效率比较低:递归与全堆对象遍历两次
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 这种方式清理出来的空闲内存是不连续的,产生内存碎片。

标记复制

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

  • 优点

    • 不会造成空间碎片。
  • 缺点

    • 需要两倍的内存空间
    • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。

注:如果系统中的存活对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

标记整理

从根节点开始标记所有被引用对象
将所有的存活对象压缩到内存的一端,按顺序排放,清理边界外所有的空间。

  • 优点

    • 消除了标记/清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
    • 消除了复制算法当中,内存减半的高额代价。
  • 缺点

    • 效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
    • 对于老年代每次都有大量对象存活的区域来说,极为负重。
    • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
    • 移动过程中,需要全程暂停用户应用程序。即:STW

分代收集

基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

增量回收

垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

注:线程上下文切换频繁,是的垃圾回收成本上升,导致吞吐量上升

分区算法

将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。

JVM四种引用

强引用

特点:GC时,永远不会被回收
使用场景

new 对象

软引用SoftReference(obj);

特点:内存不足时(自动触发GC),会被回收
使用场景

缓存

弱引用WeakReference(obj)

特点:无论内存是否充足,只要进行GC,都会被回收
使用场景

内部对象为弱引用 WeakReference为强引用

虚引用PhantomReference<>(new Object(),new ReferenceQueue<>())

特点:如同虚设,和没有引用没什么区别
使用场景

  1. 管理堆外面的引用

首先标记出所需要回收的对象,在标记完成后,统一回收掉所有被标记的对象

将可用内存划分为两部分,每次只使用其中一块,当一块内存用完时,将还存活对象复制到另外一块上面,然后再把已使用的内存空间清理一遍

标记出所需要回收的对象,将所有需要存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存

JVM GC 流程

JVM堆的内存分布

Minor GC 新生代GC

Major GC/Full GC

  1. 开始时,对象会先分配到eden区
  2. 引用运行,越来越多对象分配在eden区域
  3. 当eden区域放不下时,就会发生minor GC(young GC),利用可达性分析标记出垃圾对象,然后将有用对象移动到survivor0区域,将标记出来的垃圾对象全部清除,此时eden区域就全部清理干净了。整个过程使用了 mark-sweep(标记整理)方法回收eden区,使用mark-copy(标记复制) 方法将可用对象移动到 survivor0区域。
  4. 随着时间推移,eden如果又满了,再次触发minor GC,同样还是先做标记,这时eden和s0区可能都有垃圾对象了,注意:这时s1(即:to)区是空的,S0区和eden区的存活对象(S0 区域满了),将直接搬到s1区。然后将eden和S0区的垃圾清理掉,这一轮minor GC后,eden和S0区就变成了空的了。
  5. 随着对象的不断分配,eden空可能又满了,这时会重复刚才的minor GC过程,不过要注意的是,这时候s0是空的,所以s0与s1的角色其实会互换,即:存活的对象,会从eden和s1区,向s0区移动。然后再把eden和s1区中的垃圾清除,这一轮完成后,eden与s1区变成空的
  6. 对于那些比较“长寿”的对象一直在s0与s1中挪来挪去,一来很占地方,而且也会造成一定开销,降低gc效率,于是有了“代龄(age)”及“晋升”。对象在年青代的3个区(eden,s0,s1)之间,每次从1个区移到另1区,年龄+1,在young区达到一定的年龄阈值(-XX:MaxTenuringThreshold(默认15))后,将晋升到老年代。
  7. 如果老年代,最终也放满了,就会发生major GC(即Full GC),由于老年代的的对象通常会比较多,因为标记-清理-整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,

垃圾收集器

图像

  1. 两个收集器间有连线,表明它们可以搭配使用:
    Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  2. 其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP 173),并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除。
  4. (绿色虚线)JDK 14中:弃用Parallel Scavenge和SerialOld GC组合 (JEP 366)
  5. (青色虚线)JDK 14中:删除CMS垃圾回收器 (JEP 363)

经典垃圾收集器

Serial 收集器/Serial Old 收集器

简单高效,占用内存小 适合客户端

image.png

ParNew 收集器/Serial Old 收集器

适合多核CPU 单核由于上下文切换,收集并不理想

image.png

Parallel Scavenge/Parallel Old收集器

吞吐量优先的垃圾收集器,有自适应调节策略 jdk8的默认垃圾收集器

image.png

CMS收集器(老年代)

CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。

对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理

image.png

  • 初始标记(STW):暂时时间非常短,标记与GC Roots直接关联的对象。
  • 并发标记(最耗时):从GC Roots开始遍历整个对象图的过程。不会停顿用户线程
  • 重新标记:(STW):修复并发标记环节,因为用户线程的执行,导致数据的不一致性问题
  • 并发清理(最耗时):清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

G1 收集器

主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量,JDK9 以后的默认收集器

图像

回收过程
  1. 年轻代GC (Young GC)

  2. 老年代并发标记过程 (Concurrent Marking)

  3. 混合回收(Mixed GC)

  4. 如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。

图像 (2)

顺时针,young gc -> young gc + concurrent mark-> Mixed GC顺序,进行垃圾回收。

特点
  • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况

并行与并发

  • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW

  • 分代收集

    • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
    • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
  • 空间整合

    • CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
    • G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
  • 可预测的停顿时间模型

    • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
    • G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
    • 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

注:G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

0%