洋蔥

贪婪,找不到比这更好的词了,是件好事。

垃圾回收介绍

jvm内存管理

什么是垃圾回收(Garbage Collection)

  • 把不用的内存回收掉
  • java采用自动内存管理技术,内存分配后由虚拟机自动管理

优缺点:

  • 优点:程序员不需要自己释放内存,只管new对象即可
  • 缺点:GC本身有开销,会挤占业务执行资源。

什么是垃圾:

  • 不会被访问到的对象是垃圾

引用计数法

原理

  • 记录每个对象被引用的数量,当被引用的数量为0时,则标记为垃圾
  • 缺点:无法处理循环引用的问题

可达性分析

原理

  • 从GC Roots开始遍历对象,没有被遍历到的对象为垃圾

GC Roots:

  • 方法栈使用到的参数、局部变量、临时变量等
  • 方法区中类静态属性引用的变量
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

垃圾回收算法

清除(sweep)

原理

  • 将垃圾对象所占据的内存标记为空闲内存,然后存在一个空闲列表(free list)中。当需要创建对象时,从空闲列表中寻找空闲内存,分配给新创建的对象

优缺点:

  • 优点:速度快
  • 缺点:容易造成内存碎片,分配效率低

整理(compact)

原理

  • 把存活的对象搬到内存的起始位置,然后在连续的空间内顺序分配

优缺点:

  • 优点:分配速度快,局部性好
  • 缺点:搬运对象麻烦,性能开销大

复制(copy)

原理

  • 将内存分为两个部分,并分别用from和to指针来维护。每次只在from指向的内存中分配内存,当发生垃圾回收时,将from指向区域中存活的对象复制到to指向的内存区域,然后将from指针和to指针互换位置。

优缺点:

  • 优点:同压缩算法,没有内存碎片。分配速度快,局部性好
  • 缺点:可用内存变少,堆空间使用效率低

垃圾收集器

jvm堆划分

jvm将堆划分为新生代和老年代。新生代存放新创建的对象,当对象生存超过一定时间时,会被移动至老年代。新生代采用的 GC 称为minor GC,老年代发生的 GC 称为 full GC 或 major GC,发生full GC会伴随至少一次minor GC

Minor GC

特点:发生次数多,采用时间短,回收掉大量对象

收集器:serial, Parallel Scavenge, Parallel New.均采用复制算法. Serial是单线程,Parallel New可以看成Serial多线程版本. Parallel Scanvenge和Parallel New类似,但更注重吞吐率,且不能与CMS一起使用

Full GC

特点:发生次数少,耗时长

收集器:Serial Old(整理), Parallel Old(整理), CMS(清除). Serial Old是单线程的,Parallel Old可以看成Serial Old的多线程版本. CMS是并发收集器,除了初始标记和重新标记操作需要Stop the world,其它时间可以与应用程序一起并发执行

垃圾回收触发条件

Minor GC

  • Eden区空间不足

Full GC

  • 老年代空间不足
  • 方法区(Metaspace)空间不足
  • 通过minor GC进入老年代的平均大小大于老年代的可用内存
  • 老年代被写满
  • 调用System.GC,系统建议执行full GC,但不一定执行。 《华为Java语言通用编程规范》规则8.7.5:禁止使用主动GC(除非在密码、RMI等方面),尤其是在频繁/周期性的逻辑中

SecurityManager应用场景

当运行未知的Java程序的时候,该程序可能有恶意代码(删除系统文件、重启系统等),为了防止运行恶意代码对系统产生影响,需要对运行的代码的权限进行控制,这时候就要启用Java安全管理器。

Java 安全管理器允许应用程序设置一个安全管理策略,通过安全管理策略实现对应用程序中敏感操作的管理。

安全策略配置文件

默认的安全管理器配置文件

$JAVA_HOME/jre/lib/security/java.policy (Java 8)

$JAVA_HOME/lib/security/default.policy(Java 9+)

当未指定配置文件时,将会使用该配置。

不同版本的 Java 默认的权限配置有所差异,导致部分原来在低版本 Java 上运行的程序,在高版本中可能出现 access denied 的权限异常。

Java 8 内容如下:

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
// Standard extensions get all permissions by default

grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};

// default permissions granted to all domains

grant {
// Allows any thread to stop itself using the java.lang.Thread.stop()
// method that takes no argument.
// Note that this permission is granted by default only to remain
// backwards compatible.
// It is strongly recommended that you either remove this permission
// from this policy file or further restrict it to code sources
// that you specify, because Thread.stop() is potentially unsafe.
// See the API specification of java.lang.Thread.stop() for more
// information.
permission java.lang.RuntimePermission "stopThread";

// allows anyone to listen on dynamic ports
permission java.net.SocketPermission "localhost:0", "listen";

// "standard" properies that can be read by anyone

permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";

permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";

permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";
};

配置文件详解见下文。

启动安全管理器

启动安全管理有两种方式,建议使用启动参数方式。

参数启动方式

-Djava.security.manager

指定配置文件

-Djava.security.manager -Djava.security.policy="E:/java.policy"

编码启动方式

System.setSecurityManager(new SecurityManager());

配置文件简单解释

基本配置原则

在启用安全管理器的时候,配置遵循以下基本原则:

  • 没有配置的权限表示没有。
  • 只能配置有什么权限,不能配置禁止做什么。
  • 同一种权限可多次配置,取并集。
  • 统一资源的多种权限可用逗号分割。

默认配置文件解释

第一部分授权:

1
2
3
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};

授权基于路径在 "file:${{java.ext.dirs}}/*" 的class和jar包,所有权限。

第二部分授权:

1
2
3
4
grant { 
permission java.lang.RuntimePermission "stopThread";
……
}

这是细粒度的授权,对某些资源的操作进行授权。具体不再解释,可以查看javadoc。如 RuntimePermission 的可授权操作经查看javadoc如下:

权限目标名称 权限所允许的操作 允许此权限所带来的风险
createClassLoader 创建类加载器 授予该权限极其危险。能够实例化自己的类加载器的恶意应用程序可能会在系统中装载自己的恶意类。这些新加载的类可能被类加载器置于任意保护域中,从而自动将该域的权限授予这些类。
getClassLoader 类加载器的获取(即调用类的类加载器) 这将授予攻击者得到具体类的加载器的权限。这很危险,由于攻击者能够访问类的类加载器,所以攻击者能够加载其他可用于该类加载器的类。通常攻击者不具备这些类的访问权限。
setContextClassLoader 线程使用的上下文类加载器的设置 在需要查找可能不存在于系统类加载器中的资源时,系统代码和扩展部分会使用上下文类加载器。授予 setContextClassLoader 权限将允许代码改变特定线程(包括系统线程)使用的上下文类加载器。
enableContextClassLoaderOverride 线程上下文类加载器方法的子类实现 在需要查找可能不存在于系统类加载器中的资源时,系统代码和扩展部分会使用上下文类加载器。授予 enableContextClassLoaderOverride 权限将允许线程的子类重写某些方法,这些方法用于得到或设置特定线程的上下文类加载器。
setSecurityManager 设置安全管理器(可能会替换现有的) 安全管理器是允许应用程序实现安全策略的类。授予 setSecurityManager 权限将通过安装一个不同的、可能限制更少的安全管理器,来允许代码改变所用的安全管理器,因此可跳过原有安全管理器所强制执行的某些检查。
createSecurityManager 创建新的安全管理器 授予代码对受保护的、敏感方法的访问权,可能会泄露有关其他类或执行堆栈的信息。
getenv.{variable name} 读取指定环境变量的值 此权限允许代码读取特定环境变量的值或确定它是否存在。如果该变量含有机密数据,则这项授权是很危险的。
exitVM.{exit status} 暂停带有指定退出状态的 Java 虚拟机 此权限允许攻击者通过自动强制暂停虚拟机来发起一次拒绝服务攻击。注意:自动为那些从应用程序类路径加载的全部代码授予 “exitVM.“ 权限,从而使这些应用程序能够自行中止。此外,”exitVM” 权限等于 “exitVM.“。
shutdownHooks 虚拟机关闭钩子 (hook) 的注册与取消 此权限允许攻击者注册一个妨碍虚拟机正常关闭的恶意关闭钩子 (hook)。
setFactory 设置由 ServerSocket 或 Socket 使用的套接字工厂,或 URL 使用的流处理程序工厂 此权限允许代码设置套接字、服务器套接字、流处理程序或 RMI 套接字工厂的实际实现。攻击者可能设置错误的实现,从而破坏数据流。
setIO System.out、System.in 和 System.err 的设置 此权限允许改变标准系统流的值。攻击者可以改变 System.in 来监视和窃取用户输入,或将 System.err 设置为 “null” OutputStream,从而隐藏发送到 System.err 的所有错误信息。
modifyThread 修改线程,例如通过调用线程的 interruptstopsuspendresumesetDaemonsetPrioritysetNamesetUncaughtExceptionHandler 方法 此权限允许攻击者修改系统中任意线程的行为。
stopThread 通过调用线程的 stop 方法停止线程 如果系统已授予代码访问该线程的权限,则此权限允许代码停止系统中的任何线程。此权限会造成一定的危险,因为该代码可能通过中止现有的线程来破坏系统。
modifyThreadGroup 修改线程组,例如通过调用 ThreadGroup 的 destroygetParentresumesetDaemonsetMaxPrioritystopsuspend 方法 此权限允许攻击者创建线程组并设置它们的运行优先级。
getProtectionDomain 获取类的 ProtectionDomain 此权限允许代码获得特定代码源的安全策略信息。虽然获得安全策略信息并不足以危及系统安全,但这确实会给攻击者提供了能够更好地定位攻击目标的其他信息,例如本地文件名称等。
getFileSystemAttributes 获取文件系统属性 此权限允许代码获得文件系统信息(如调用者可用的磁盘使用量或磁盘空间)。这存在潜在危险,因为它泄露了关于系统硬件配置的信息以及一些关于调用者写入文件特权的信息。
readFileDescriptor 读取文件描述符 此权限允许代码读取与文件描述符读取相关的特定文件。如果该文件包含机密数据,则此操作非常危险。
writeFileDescriptor 写入文件描述符 此权限允许代码写入与描述符相关的特定文件。此权限很危险,因为它可能允许恶意代码传播病毒,或者至少也会填满整个磁盘。
loadLibrary.{库名} 动态链接指定的库 允许 applet 具有加载本机代码库的权限是危险的,因为 Java 安全架构并未设计成可以防止恶意行为,并且也无法在本机代码的级别上防止恶意行为。
accessClassInPackage.{包名} 当类加载器调用 SecurityManager 的checkPackageAccess 方法时,通过类加载器的 loadClass 方法访问指定的包 此权限允许代码访问它们通常无法访问的那些包中的类。恶意代码可能利用这些类帮助它们实现破坏系统安全的企图。
defineClassInPackage.{包名} 当类加载器调用 SecurityManager 的 checkPackageDefinition 方法时,通过类加载器的 defineClass 方法定义指定的包中的类。 此权限允许代码在特定包中定义类。这样做很危险,因为具有此权限的恶意代码可能在受信任的包中定义恶意类,比如 java.securityjava.lang
accessDeclaredMembers 访问类的已声明成员 此权限允许代码查询类的公共、受保护、默认(包)访问和私有的字段和/或方法。尽管代码可以访问私有和受保护字段和方法名称,但它不能访问私有/受保护字段数据并且不能调用任何私有方法。此外,恶意代码可能使用该信息来更好地定位攻击目标。而且,它可以调用类中的任意公共方法和/或访问公共字段。如果代码不能用这些方法和字段将对象强制转换为类/接口,那么它通常无法调用这些方法和/或访问该字段,而这可能很危险。
queuePrintJob 打印作业请求的开始 这可能向打印机输出敏感信息,或者只是浪费纸张。
getStackTrace 获取另一个线程的堆栈追踪信息。 此权限允许获取另一个线程的堆栈追踪信息。此操作可能允许执行恶意代码监视线程并发现应用程序中的弱点。
setDefaultUncaughtExceptionHandler 在线程由于未捕获的异常而突然终止时,设置将要使用的默认处理程序 此权限允许攻击者注册恶意的未捕获异常处理程序,可能会妨碍线程的终止
Preferences 表示得到 java.util.prefs.Preferences 的访问权所需的权限。java.util.prefs.Preferences 实现了用户或系统的根,这反过来又允许获取或更新 Preferences 持久内部存储中的操作。 如果运行此代码的用户具有足够的读/写内部存储的 OS 特权,则此权限就允许用户读/写优先级内部存储。实际的内部存储可能位于传统的文件系统目录中或注册表中,这取决于平台 OS。

可配置项详解

当批量配置的时候,有三种模式:

  • directory/ :表示directory目录下的所有.class文件,不包括.jar文件
  • directory/* :表示directory目录下的所有的.class及.jar文件
  • directory/- :表示directory目录下的所有的.class及.jar文件,包括子目录

可以通过${}来引用系统属性,如:

1
"file:${{java.ext.dirs}}/*"

顶层抽象类

java.security.Permission 用来定义类所拥有的权限,具体见其子类。

每个子类具体拥有哪些权限,请查看类注释。

问题解决

当出现关于安全管理的报错的时候,基本有两种方式来解决。

取消安全管理器

一般情况下都是无意启动安全管理器,所以这时候只需要把安全管理器进行关闭,去掉启动参数即可。

增加相应权限

若因为没有权限报错,则报错信息中会有请求的权限和请求什么权限,如下:

1
Exception in thread "main" java.security.AccessControlException: access denied (java.io.FilePermission c:/protect.txt write)

上面例子,请求资源 c:/protect.txt 的 FilePermission 的写权限没有,因此被拒绝。

也可以开放所有权限:

1
2
3
grant { 
permission java.security.AllPermission;
};

1
2
3
grant {
permissionjava.io.FilePermission "c:/protect.txt", "read", "write";
};

推荐:

视频教程:https://www.bilibili.com/video/BV1wh411e7nd

对应代码:https://github.com/hellozhaolq/2021-Java-ThreadPool-Tutorial

对于数据库连接,我们经常听到数据库连接池这个概念。因为建立数据库连接是非常耗时的一个操作,其中涉及到网络IO的一些操作。因此就想出把连接通过一个连接池来管理。需要连接的话,就从连接池里取一个。当使用完了,就“关闭”连接,这不是真正意义上的关闭,只是把连接放回池里,供其他人在使用。所以对于线程,也有了线程池这个概念,其中的原理和数据库连接池差不多。

阅读全文 »

合理的使用Java多线程可以更好地利用服务器资源。一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当需要多个线程间相互协作时,就需要掌握线程的通信方式。

阅读全文 »

Java中锁的分类有:

  • 可中断锁
  • 可重入锁(递归锁)
  • 公平锁/非公平锁
  • 独占锁(互斥锁)/共享锁
  • 乐观锁/悲观锁
  • 自旋锁
  • 偏向锁/轻量级锁/重量级锁(锁的状态)
  • 分段锁(锁的设计)

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

synchronized与Lock对比

synchronized Lock
别名 隐式锁 显式锁
实现方式 Java中的关键字,语言内置实现 是一个接口
锁的释放方式 同步代码执行结束或抛出异常时自动释放 finally 块中手动释放
多个锁嵌套的释放顺序 依次获取(释放)锁,
最先获取的最后释放
自由获取(释放)锁
发生异常时 自动释放线程占有的锁,不会导致死锁 需在 finally 块中手动释放锁,否则会导致死锁
定时阻塞获取锁 不支持 支持
判断是否已成功获取锁 无法办到 tryLock() 返回值是 boolean
性能 竞争激烈时(大量线程同时获取锁),性能更优
可中断锁
(获取锁时能否响应线程中断指令)

tryLock(long time, TimeUnit unit)
lockInterruptibly()
可重入锁
公平锁 非公平锁,无法判断锁是否公平 默认非公平锁。
实现类的构造方法可指定锁的公平性。isFair() 判断锁是否公平。
独占锁(互斥锁)/共享锁 独占锁(互斥锁) 独占锁:synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock
共享锁:ReentrantReadWriteLock.ReadLock
通过 AQS(AbstractQueuedSynchronizer)实现独占和共享。
读写锁 ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

可中断锁

中断锁指的是可中断获取锁的等待状态,也就是在获取锁时可以响应线程中断指令,从而中断获取锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

在 Java 中,synchronized 是不可中断锁,Lock 是可中断锁

可重入锁(递归锁)(特性)

可重入互斥锁 - 维基百科

reentrant :可重入; 可重入的; 重入; 可再入的; 重进入;

可重入锁又名递归锁。特点:

  • 可重复进入持有相同锁的同步作用域(同步代码块、同步方法、Lock锁同步代码)。
  • 在一个线程中可以多次获取同一把锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导致锁不被释放。

如果不设计成可重入锁,反复给自己加锁就会产生死锁。

锁类型 锁对象
可重入锁 synchronized
可重入锁 ReentrantLock(Re entrant Lock 重新进入锁)
可重入锁 ReentrantReadWriteLock.ReadLock
可重入锁 ReentrantReadWriteLock.WriteLock
不可重入锁 没有内置对象,需自定义

可重入锁演示

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
public class Test {
public static void main(String[] args) {
new Test().同步代码块();
new Test().同步方法();
Test.静态同步方法();
Test.Lock锁();
}

// 可选择锁对象
public void 同步代码块() {
synchronized (this) {
System.out.println("同步代码块,第一次获取锁");
synchronized (this) {
System.out.println("同步代码块,第二次获取锁");
}
}
}

// 对象锁,锁对象是this
public synchronized void 同步方法() {
System.out.println("同步方法,第一次获取锁");
同步方法2();
}

public synchronized void 同步方法2() {
System.out.println("同步方法,第二次获取锁");
}


// 类锁,锁对象是当前类的Class对象
public static synchronized void 静态同步方法() {
System.out.println("静态同步方法,第一次获取锁");
静态同步方法2();
}

public static synchronized void 静态同步方法2() {
System.out.println("静态同步方法,第二次获取锁");
}

// Lock锁
public static void Lock锁() {
Lock lock = new ReentrantLock();
lock.lock();
System.out.println("ReentrantLock,第一次获取锁");
lock.lock();
System.out.println("ReentrantLock,第二次获取锁");
try {
// ... 同步代码
} finally {
lock.unlock();
lock.unlock();
}
}
}

不可重入锁演示

实际开发中几乎不会用到。

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
public class Test {
public static void main(String[] args) {
Lock lock = new UnReentrantLock();
lock.lock();
System.out.println("UnReentrantLock,第一次获取锁");
lock.lock();
System.out.println("UnReentrantLock,第二次获取锁");
try {
// ... 同步代码
} finally {
lock.unlock();
lock.unlock();
}
}
}

/**
* 自定义一个不可重入锁
*/
class UnReentrantLock implements Lock {
// 当前绑定的线程,记录以获取锁的线程
private Thread bindThread;

@Override
public void lock() {
synchronized (this) {
// 当已有线程拿到锁时
while (bindThread != null) {
try {
// 使当前线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 绑定当前线程
this.bindThread = Thread.currentThread();
}
}

@Override
public void unlock() {
synchronized (this) {
// 当绑定的线程不是当前线程时
if (bindThread != Thread.currentThread()) {
return;
}
// 解绑线程
bindThread = null;
// 唤醒所有线程,唤醒的线程可以继续绑定了
notifyAll();
}
}

@Override
public void lockInterruptibly() throws InterruptedException {

}

@Override
public boolean tryLock() {
return false;
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}

@Override
public Condition newCondition() {
return null;
}
}

公平锁/非公平锁

是否公平:指的是每个线程获取锁的机会是否平等。

公平锁:多个线程按照申请锁的顺序来获取锁。比如同时有多个线程在等待同一个锁,当这个锁被释放时,等待时间最久的线程(最先申请的线程)会获得该锁。

非公平锁:多个线程争抢同一个锁时,有可能后申请的线程比先申请的线程优先获取锁。可能造成优先级反转或者饥饿现象(导致某些线程永远获取不到锁)。优点是比公平锁吞吐量大。

对于 synchronized 而言,也是一种非公平锁。由于其并不像 ReentrantLock 是通过 AQS(AbstractQueuedSynchronizer) 的来实现线程调度,所以并没有任何办法使其变成公平锁。

对于 ReentrantLockReentrantReadWriteLock 实现而言,默认是非公平锁,构造方法可指定锁的公平性

在 ReentrantLock 和 ReentrantReadWriteLock 中都定义了两个静态内部类,分别用来实现非公平锁和公平锁。可以在创建对象时,通过构造方法指定锁的公平性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
// 同步锁的结构和 ReentrantLock 完全相同
}

public class ReentrantLock implements Lock, java.io.Serializable {
// 此锁的同步控制基础。下面分为公平和非公平版本。使用 AQS 状态来表示锁上的持有数量。
abstract static class Sync extends AbstractQueuedSynchronizer {
}
// 公平锁的同步实现
static final class FairSync extends Sync {
}
// 非公平锁的同步实现
static final class NonfairSync extends Sync {
}
}

非公平锁演示

下面示例中,每次都是 Thread-0 拿到锁,验证了 synchronized 是非公平锁。

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 class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread0.start();
thread1.start();
thread2.start();
}
}

class Task implements Runnable {
@Override
public void run() {
while (true) {
// 同步锁为this的同步代码块
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

修改上面示例 Task 类如下,验证 Lock 的非公平锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Task implements Runnable {
/**
* 默认非公平锁
*/
private Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}

公平锁演示

修改上面示例 private Lock lock = new ReentrantLock(true); 即可。

判断锁是否公平

1
2
3
public final boolean isFair() {
return sync instanceof FairSync;
}

该方法只能判断 ReentrantLock、ReentrantReadWriteLock:

1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
System.out.println(lock.isFair()); // false
}
}

独占锁(互斥锁)/共享锁

排他锁 - 维基百科互斥锁 - 维基百科

共享锁 - 百度百科

读写锁 - 维基百科

独占锁(互斥):该锁一次只能被一个线程所持有。例如:synchronized、ReentrantLock、读写锁中的写锁 ReentrantReadWriteLock.WriteLock

共享锁(非互斥):该锁可被多个线程所持有。例如:读写锁中的读锁 ReentrantReadWriteLock.ReadLock,但读写、写读 、写写的过程是互斥的。

Lock 锁的 独占共享 是通过 AQS(AbstractQueuedSynchronizer)来实现的。

共享锁演示

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 Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread0.start();
thread1.start();
thread2.start();
}
}

class Task implements Runnable {
/**
* 读写锁
*/
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

/**
* 读锁
*/
private Lock readLock = this.readWriteLock.readLock();

@Override
public void run() {
this.readLock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
}

独占锁演示

修改上述代码:

1
2
3
4
5
6
class Task implements Runnable {
/**
* 写锁
*/
private Lock writeLock = this.readWriteLock.writeLock();
}

乐观锁/悲观锁

乐观锁与悲观锁指的不是锁类型,而是指 并发策略 是乐观的还是悲观的。

乐观并发控制 - 维基百科

悲观并发控制 - 维基百科

悲观锁:只要编码中 利用到锁,并发策略就是悲观的。

乐观锁:无锁编程,通常采用 CAS 等原子操作来实现。典型的例子就是原子类,通过 CAS 自旋实现原子操作。

自旋锁

自旋锁 - 维基百科

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,避免线程上下文切换带来的开销。但由于获取锁的线程一直处在运行状态,所以不适合单核单线程的CPU。

通常采用 CAS 等原子操作来实现,可以参考 自旋锁的实现

偏向锁/轻量级锁/重量级锁(锁的状态)

这三种锁是指锁的状态,并且是针对 synchronized。在 Java 5 通过引入锁升级的机制来实现高效 synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

分段锁(锁的设计)

分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以 ConcurrentHashMap 来说一下分段锁的含义以及设计思想,ConcurrentHashMap 中的分段锁称为 Segment,它即类似于 HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。

当需要 put 元素的时候,并不是对整个 HashMap 进行加锁,而是先通过 hashcode 来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计 size 的时候,就是获取 HashMap 全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

推荐链接:

Java 实例 - 死锁及解决方法 - 菜鸟

概述

死锁:两个(或两个以上)线程在执行的过程中,因争夺资源产生的一种互相等待现象。

产生条件:

  • 两个(或两个以上)线程
  • 两个(或两个以上)锁
  • 两个(或两个以上)线程持有不同锁
  • 争夺对方的锁

编写死锁代码

synchronized嵌套

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
public class Test {
public static void main(String[] args) {
Thread threadA = new Thread(new LockA());
Thread threadB = new Thread(new LockB());
threadA.start();
threadB.start();
}
}

class LockA implements Runnable {
@Override
public void run() {
printA();
}

public static synchronized void printA() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("A");
// 持有类锁(LockA.class),争夺对方的锁(LockB.class)
LockB.printB();
}
}

class LockB implements Runnable {
@Override
public void run() {
printB();
}

public static synchronized void printB() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("B");
// 持有类锁(LockB.class),争夺对方的锁(LockA.class)
LockA.printA();
}
}

Lock未主动释放锁

Semaphore信号嵌套

卖票

取钱

synchronized

继承Thread类

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
public class Test {
public static void main(String[] args) {
Account account = new Account("260731000", 100L);

Draw yourself = new Draw(account, 50L, "Yourself");
Draw girlFriend = new Draw(account, 31L, "GirlFriend");

yourself.start();
girlFriend.start();
}
}

@Data
class Account {
private String name;
private long money;

public Account(String name, long money) {
this.name = name;
this.money = money;
}
}

class Draw extends Thread {
private Account account;

private long drawMoney; // 取款金额

public Draw(Account account, long drawMoney, String threadName) {
super(threadName);
this.account = account;
this.drawMoney = drawMoney;
}

@Override
public void run() {
// synchronized(被锁对象):锁的对象就是数据修改的对象
synchronized (account) {
if (account.getMoney() - drawMoney < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足");
return;
}

// sleep模拟取钱过程
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.printf("%s 账户余额: %d \t 取款金额: %d \t 剩余金额: %d \t 取款人: %s \n",
account.getName(),
account.getMoney(),
drawMoney,
account.getMoney() - drawMoney,
this.getName());

account.setMoney(account.getMoney() - drawMoney);
}
}
}

实现Runnable接口

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
public class Test {
public static void main(String[] args) {
Account account = new Account("260731000", 100L);

Draw yourselfDraw = new Draw(account, 50L);
Thread yourself = new Thread(yourselfDraw, "Yourself");

Draw girlFriendDraw = new Draw(account, 31L);
Thread girlFriend = new Thread(girlFriendDraw, "GirlFriend");

yourself.start();
girlFriend.start();
}
}

@Data
class Account {
private String name;
private long money;

public Account(String name, long money) {
this.name = name;
this.money = money;
}
}

class Draw implements Runnable {
private Account account;

private long drawMoney; // 取款金额

public Draw(Account account, long drawMoney) {
this.account = account;
this.drawMoney = drawMoney;
}

@Override
public void run() {
// synchronized(被锁对象):锁的对象就是数据修改的对象
synchronized (account) {
if (account.getMoney() - drawMoney < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足");
return;
}

// sleep模拟取钱过程
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.printf("%s 账户余额: %d \t 取款金额: %d \t 剩余金额: %d \t 取款人: %s \n",
account.getName(),
account.getMoney(),
drawMoney,
account.getMoney() - drawMoney,
Thread.currentThread().getName());

account.setMoney(account.getMoney() - drawMoney);
}
}
}

ReentrantLock

有时候继承 Thread 类会更方便。

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
public class Test {
public static void main(String[] args) {
Account account = new Account("260731000", 100L);

Draw yourselfDraw = new Draw(account, 50L);
Thread yourself = new Thread(yourselfDraw, "Yourself");

Draw girlFriendDraw = new Draw(account, 31L);
Thread girlFriend = new Thread(girlFriendDraw, "GirlFriend");

yourself.start();
girlFriend.start();
}
}

@Data
class Account {
private String name;
private long money;
private final ReentrantLock lock = new ReentrantLock(true);

public Account(String name, long money) {
this.name = name;
this.money = money;
}
}

class Draw implements Runnable {
private Account account;

private long drawMoney; // 取款金额

public Draw(Account account, long drawMoney) {
this.account = account;
this.drawMoney = drawMoney;
}

@Override
public void run() {
// synchronized(被锁对象):锁的对象就是数据修改的对象
try {
account.getLock().lockInterruptibly();
if (account.getMoney() - drawMoney < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足");
return;
}

// sleep模拟取钱过程
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.printf("%s 账户余额: %d \t 取款金额: %d \t 剩余金额: %d \t 取款人: %s \n",
account.getName(),
account.getMoney(),
drawMoney,
account.getMoney() - drawMoney,
Thread.currentThread().getName());

account.setMoney(account.getMoney() - drawMoney);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
account.getLock().unlock();
account.getLock().unlock(); // 抛出: IllegalMonitorStateException – 如果当前线程没有持有这个锁
}
}
}
0%