NTFS Change Journal(USN Journal)详解

写在前面

最近又用了一下usn日志来获取所有文件列表,在分多次加载文件列表的时候发现有文件丢失的情况,后来发现一篇文章比较详细的讲了usn。


用cmd来读取usn日志

如图:
cmd fsutil usn


以下是转载内容:

还是那个文件监控的应用,发现使用Windows API(ReadDirectoryChangesW)还是不能满足要求,如果变化量大又密集时,丢失通知现象很严重。好在需要监控的大部分的Windows用户都转到NTFS系统,所以打算采用分析NTFS的Change Journal(更改日志)的方法实现监控功能。

Change Journal这名字挺直白。

很火的桌面搜索程序:Everything就是利用了NTFS系统的这个特性,通过读取和监控USN(后面会讲)而不是扫描文件来构建索引,所以搜索速度飞快,看来这个东西很好用。但是,相关的资料却是很少,特别是系统级介绍,而且一会USN,一会Change Journal,晕啊,找到代码都不敢用,还是老老实实做功课吧,找到了微软原始的两篇论文,来好好研究一下这个Change Journal。

==介绍==

NTFS是Windows 2000及其他基于Windows NT系统的标准文件系统,提供很多新特性(与FAT32比),而Change Journal是一个存储所有NTFS 5.0标卷(Volume)上文件和目录变化信息的数据库。每个标卷都有自己的Change Journal数据库,这些监控这些信息可以用来实现数据恢复,防止系统文件被篡改等系统级功能。在Windows NT 4.0中,这些功能,都是由之前两篇译文:[译]理解ReadDirectoryChangeW (理论部分)和(实现部分)中提到的Windows API来完成监控的,难用的程度真是谁用谁知道。一般的非系统级应用(如杀毒软件)也可以使用Change Journal,可以避免程序扫描整个硬盘,提高效率。

==细节==

事实上Change Journal是标卷上一个特殊的文件,系统将其隐藏,所以用资源管理器或者CMD Shell都看不到,当文件系统中的文件或者目录发生改变时,就会向日志中追加记录。记录一般包括:文件名,变化时间,变化类型,而实际的数据不会记录,这样也可以保持记录文件足够小。

最开始的时候,日志文件时一个磁盘标卷上的空文件,随着改变的发生,记录不断被追写进日志。每条日志有个64-bit标识,即USN(Update Sequence Number),这个USN是自增的,所以你可以通过比较USN来,找到事件发生的顺序(号码越小,事件越早),但不一定连续,有可能第一个USN是0,而第二个是128。

微软最开始构建Change Journal时,称其为USN Journal,所以winioctl.h头文里的结构定义都是这个命名,写程序的时候也将大量使用这个名词,所以下面不区分,Change Journal=USN Journal。

由于总是向文件末端添加记录,所以采用文件偏移的形式来存储USN,这样查询时只需要计算即可定位。但记录中的文件名是变长的,所以每条USN大小也不一定相同。考虑到性能问题,系统会将记录以4KB(可以参看winioctl.h中的USN_PAGE_SIZE宏)为块大小存放,每块通常会包含三四十条记录。操作系统不允许单条记录横跨两个块页,所以有时候会发生USN为空,用来填充块间隙。

在NTFS标卷上,文件和目录信息存储于Master File Table(MFT)中,其中的记录都描述了文件或目录名,位置,大小,属性等。NTFS 5.0中,每个MFT记录项都保存了该文件或者目录最后的USN记录。当Change Journal记录时,文件系统更新被更改的MFT中最后的USN值。

如果日志文件过大(大于定义的MaximumSize参数),系统将会清理掉文件开始部位较早的数据,通常截断开始数据需要大量的I/O操作,文件末端必须要被拷贝到新位置,这是一个耗时的过程。幸运的是,NTFS 5.0支持稀疏文件,这种机制允许删除文件中不需要的部分,而保留其余数据的逻辑偏移。所以Change Journal就是一个稀疏文件,允许清除早期记录,而不会损失太多性能,也不影响原先的文件偏移访问。更多关于稀疏文件信息可以参考A File System for the 21st Century: Previewing the Windows NT 5.0 File System。

标卷上的Change Journal功能可以关闭,这样系统就不会记录变化信息,默认情况下,NTFS标卷上的Change Journal功能是关闭的,必须明确的开启才能使用,开启和关闭可以由任意程序,任意时间完成。问题来了,如果两个程序操作时发生冲突怎么办?当一个程序禁用标卷的Change Journal,系统会清理所有先前的记录,以防止其他程序读取不可靠的数据。总的来说,Change Journal启用时会创建日志文件,禁用时会删除日志文件。

每一个Change Journal会被分配一个唯一的64-bit标识(与USN标识不同),系统将会在禁用/启用之后改变这个标识,这样程序可以通过读取这个标识,来确定读取信息的可靠性。这个标识在重启后也不会变化,换句话说,如果标识不变,Change Journal会记录开机后所有文件的变化。其实这个标识是一个UTC时间戳,但是程序员不应该利用这个语义,万一微软有一点变了咋办

==使用==

所有Change Journal操作都可以通过下面函数完成:

C++:

BOOL DeviceIoControl(
HANDLE hDevice,          // handle to device/file/
// directory
DWORD dwIoControlCode,   // control code of operation
// to perform
LPVOID lpInBuffer,       // pointer to buffer of
// input data
DWORD nInBufferSize,     // size, in bytes, of input
// buffer
LPVOID lpOutBuffer,      // pointer to buffer for
// output data
DWORD nOutBufferSize,    // size, in bytes, of output
// buffer
LPDWORD lpBytesReturned, // receives number of bytes
// written to lpOutBuffer
LPOVERLAPPED lpOverlapped// for asynchronous
// operation
);

第一个参数是通过CreateFile获得的文件/目录/设备的句柄;DeviceIoControl是用来请求驱动对设备进行操作的常用方法,参数dwIoControlCode即指定执行什么操作并定义I/O缓冲区的结构;如果CreateFile使用FILE_FLAG_OVERLAPPED调用,DeviceIoControl将会异步操作,如果ReadFile/WriteFile一样。Change Journal由NTFS驱动管理,为了与之通信,需要获得标卷的句柄:

C++:

// Get a handle to access the Change Journal on the
// 'C' volume
HANDLE hcj = CreateFile("\.C:", GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);

访问标卷句柄必须具有管理员权限,所以普通用户无法运行涉及Change Journal操作的程序,更具体的操作可以查询MSDN。

程序可以通过调用DeviceIoControl传递FSCTL_QUERY_USN_JOURNAL,来查询特定的数据,如果DeviceIoControl返回TRUE,则USN_JOURNAL_DATA结构会被填充;如果返回FALSE可以利用GetLastError(具体查MSDN)获得错误信息。

C++:

typedef struct {
DWORDLONG UsnJournalID;  //64-bit标识。
USN FirstUsn;            //第一条记录,所有比它还小的ID,都会被清理。
USN NextUsn;             //下一条会被写入的记录。
USN LowestValidUsn;      //这个日志中最小的USN,不一定是零。
USN MaxUsn;              //最大日志,根据最大大小算出,NextUsn比它还大,那就要清理记录。
DWORDLONG MaximumSize;   //最大大小
DWORDLONG AllocationDelta; //增长大小,如果增长超过MaximuSize,开始清理记录。
} USN_JOURNAL_DATA, *PUSN_JOURNAL_DATA;

=USN记录=

下面是USN记录结构,注意磁盘具体数据并不是这样存储的,所以永远都是由系统来填充这个结构,下面一一解释各个成员:

C++:

// Version 2.0 USN_RECORD structure
typedef struct {
DWORD         RecordLength;
WORD          MajorVersion;
WORD          MinorVersion;
DWORDLONG     FileReferenceNumber;
DWORDLONG     ParentFileReferenceNumber;
USN Usn;
LARGE_INTEGER TimeStamp;
DWORD         Reason;
DWORD         SourceInfo;
DWORD         SecurityId;
DWORD         FileAttributes;
WORD          FileNameLength;
WORD          FileNameOffset;
WCHAR         FileName[1];
} USN_RECORD, *PUSN_RECORD;

系统会一次读出多条记录缓存,RecordLength是记录总长度,包括文件名。所以利用长度来计算下一条记录位置。

C++:

PUSN_RECORD pNext;
pNext = (PUSN_RECORD) (((PBYTE) pRecord) +
pRecord->RecordLength);

请不要忽视MajorVersion和MinorVersion这两个参数,毕竟NTFS也在不断演化,Change Journal有自己版本控制,要知道最新的结构,不妨参看winioctl.h中的声明,而且可能要在程序中判断版本,区分处理,以免出错。瞧瞧,2.3版本是这个样子的:

C++:

// HYPOTHETICAL Version 2.3 USN_RECORD structure
typedef struct {
DWORD RecordLength;
WORD   MajorVersion;
WORD   MinorVersion;
DWORDLONG FileReferenceNumber;
DWORDLONG ParentFileReferenceNumber;
USN Usn;
LARGE_INTEGER TimeStamp;
DWORD Reason;
DWORD SourceInfo;
DWORD SecurityId;
DWORD FileAttributes;
WORD  FileNameLength;
WORD  FileNameOffset;  // penultimate of original version 2.0
DWORD ExtraInfo1;      // Hypothetically added in version 2.1
DWORD ExtraInfo2;      // Hypothetically added in version 2.2
DWORD ExtraInfo3;      // Hypothetically added in version 2.3
WCHAR FileName[1];     // variable length always at the end
} USN_RECORD, *PUSN_RECORD;

记录本身并不记录文件或者目录的全路径,而文件名由上面结构中的三个参数确定,FileNameOffset文件名偏移,FileNameLength文件名长度,FileName这个不能直接使用。

C++:

WCHAR szName[MAX_PATH];
CopyMemory(szName,
((PBYTE) pRecord) + pRecord->FileNameOffset,
pRecord->FileNameLength);
// Let's zero-terminate it
szName[pRecord->FileNameLength/sizeof(WCHAR)] = 0;

File Reference Number(FRN)是文件和目录在NTFS标卷上唯一的标识,可以通过ParentFileReferenceNumber获得全路径。

C++:

TCHAR szFullPath[MAX_PATH];
// Fill in the path of the parent directory
PathFromParentFRN(pRecord->ParentFileReferenceNumber,
szFullPath);// Append name to path using the Win32 function PathAppend
PathAppend(szFullPath, szName);

很遗憾没有一个API叫PathFromParentFRN,不然就可以直接读出目录名。现在你可能会奇怪,FileReferenceNumber是干什么的,如果我们能通过FRN得到全路径信息,那就不用上面的偏移+长度获得文件名了。事实上,找到一个目录的FRN比文件容易得多,FileReferenceNumber不一定是个文件还是目录,但是ParentFileReferenceNumber一定是个目录,所以采用偏移+长度的方式得到本名,再用Parent得到目录,这样就可以组合出全路径了。

没错,Usn就是记录标识了;TimeStamp是一个64bit,UTC时间戳;Reason成员表示文件或者目录发生了何种变化,一个文件打开后,系统将Reason变量置零,但不写入USN记录,当变化动作发生时,如果这是一个新的Reason Code,就设置Reason变量并向日志中写入记录。如果有多个程序同时操作同一个文件,也可能会发生同一条记录的Reason有多个Reason Code,直到USN_REASON_CLOSE被设置,文件被关闭。

C++:还可以通过调用DeviceIoControl传入FSCTL_WRITE_USN_CLOSE_RECORD,使得系统在打开文件时清理Reason变量为0。

DWORD cb;
USN usn;
// Force a close record for
// the open file specified
// by 'hFile'
DeviceIoControl(hFile, FSCTL_WRITE_USN_CLOSE_RECORD,
NULL, 0, &usn, sizeof(usn), &cb, NULL);

唯一特别的的一个Reason Code是USN_REASON_RENAME_OLD_NAME,当一个文件重命名,将会有两条记录被写入日志,分别一条记录老的文件/目录名,另一条记录新的文件/目录名,当然其Reason Code是USN_REASON_RENAME_NEW_NAME。

如果SourceInfo成员非零,说明文件发生了改变,那这与Season有什么区别呢。比如“杀毒软件删除了一个你文档里面的病毒”,杀毒软件需要打开文件并覆盖受感染的部分。这会产生一个Reason=USN_REASON_DATA_OVERWRITE的记录,记录会因为一个数据覆盖操作(Reason),而完成这个工作是为了杀毒(SourceInfo)。也就是说SourceInfo更具有逻辑意义,这个信息并不是系统指出的,而是由操作文件的程序设置。

SecurityId是系统用来描述文件安全性的成员,与设备I/O控制FSCTL_SECURITY_ID_CHECK仪器使用;FileAttributes可以通过GetFileAttributes调用获得文件/目录的属性。

=读取记录=

有了上面对记录结构的认识,下面来读取Change Journal记录。首先准备两个变量,分别是标卷句柄,与日志结构(通过FSCTL_QUERY_USN_JOURNAL获得):

C++:

HANDLE hcj;
USN_JOURNAL_DATA ujd;

再通过调用FSCTL_READ_USN_JOURNAL调用DeviceIoControl,下面这个结构需要填充后作为参数输入:

C++:

typedef struct {
USN StartUsn;
DWORD ReasonMask;
DWORD ReturnOnlyOnClose;
DWORDLONG Timeout;
DWORDLONG BytesToWaitFor;
DWORDLONG UsnJournalID;
} READ_USN_JOURNAL_DATA, *PREAD_USN_JOURNAL_DATA;

StartUsn,第一条你想访问的Usn,如果标识存在就返回,否则返回下一条,如果StartUsn为0,系统将会返回最开始的记录。 ReasonMask和ReturnOnlyOnClose可以按照字面理解(后面会解释),StartUsn并不能保证时候满足这两个条件,所以需要调用者自己验证。系统是以4KB为一块(USN_PAGE_SIZE)写入日志,所有ujd.FirstUsn到ujd.NextUsn都会依据4KB对齐。

系统只会返回满足ReasonMask条件的记录,换句话说,你可以指定自己关心的Reason Code,不符合条件的记录不会包含在缓冲区中。ReturnOnlyOnClose是另一可以过滤记录的成员,如果其值非零,只有Reason=USN_REASON_CLOSE记录才会被返回,这个条件需要与ReasonMask相一致才行。

Timeout与BytesToWaitFor一起使用,作为查询时间的限制。并不是说明DeviceIoControl在指定的超时时间内返回,而是用来指定系统检查请求数据是否可用的周期。这个成员不像其他win32超时参数采用毫秒计时,而是使用FILETIME结构。当设置Timeout为0,即不指定超时时间;使用一个负数来指定超时时间,例如一个25秒的超时可以表示为-2500000000。如果是异步调用DeviceIoControl则超时成员被忽略。

不要混淆BytesToWaitFor成员和输出缓冲区大小,或者DeviceIoControl的返回值,若置零,则表示函数立即返回,即使没有找打匹配的日志,如果非零,至少找到一条匹配数据然后返回。BytesToWaitFor定义了系统检查是否匹配数据创建的周期,例如,如果定义16384,系统将会在新建16KB数据后验证,这样可以防止一个进程读取记录时使用太多资源。Timeout/BytesToWaitFor只有在使用ReasonMask/ReturnOnlyOnClose但没有找到数据时才有效果。

UsnJournalID应该被设为ujd.UsnJournalID,如果日志ID已经被改变,DeviceIoControl调用会失败(前面说过,禁用后数据都会删除,重启后会改变这个ID)。

调用FSCTL_READ_USN_JOURNAL是为了填充输出缓冲。

C++:

DeviceIOControl(hcj, FSCTL_READ_USN_JOURNAL, &InBuf,
sizeof(InBuf), pOut, cbOut, &cbReturned, NULL);

但却无法知道具体填充了几条数据,具体排列形式是这样:

下面的代码利用usnStart和usnEnd判断数据合法性:

C++:

// Read the raw data for USNs from usnStart up to but not including usnEnd
// This can be used to read all available records by using
// the USN_JOURNAL_DATA members FirstUsn and NextUsn
void GetRawRecordData(HANDLE hcj, DWORDLONG journalId,
USN usnStart, USN usnEnd){
READ_USN_JOURNAL_DATA rujd;
rujd.StartUsn          = usnStart;
rujd.ReasonMask        = 0xFFFFFFFF;  // All bits
rujd.ReturnOnlyOnClose = FALSE;       // All entries
rujd.Timeout           = 0;           // No timeout
rujd.BytesToWaitFor    = 0;           // Do not wait if no records
rujd.UsnJournalID      = journalId;   // The journal we expect to read fromwhile (rujd.StartUsn <usnEnd) {
DWORD cbRead;
BYTE pData[8192 + sizeof(USN)]; // read in 8 KB chunks
BOOL fOk = DeviceIoControl(hcj, FSCTL_READ_USN_JOURNAL,
&rujd, sizeof(rujd), pData, sizeof(pdata), &cbRead, NULL);
if (!fOk)
break; // handle error
// Get first USN to request next time
rujd.StartUsn = * ((PUSN) pData);
PUSN_RECORD pRecord = (PUSN_RECORD) &pData[sizeof(USN)];
while ((PBYTE) pRecord < (pData + cbRead)) {
// … do something with the record …
pRecord = (PUSN_RECORD)((PBYTE) pRecord + pRecord->RecordLength)
}
}}

==参考==

  1. Keeping an Eye on Your NTFS Drives: the Windows 2000 Change Journal Explained

  2. Keeping an Eye on Your NTFS Drives, Part II: Building a Change Journal Application

  3. Wikipedia: USN_Journal

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544676.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

绝,Java 中创建对象的 5 种方法!

我们日常生活中会创建很多对象&#xff0c;但是这个对象和你理解的那么对象不一样&#xff0c;因为作者不是女娲&#xff0c;不能造人。作者只是程序员&#xff0c;他只能在 Java 中创建对象。那么我问你一个问题&#xff0c;你知道 Java 中如何创建对象吗&#xff1f;这个问题…

C# Winform 窗体美化(十、自定义窗体)

十、自定义窗体 写在前面 最近在做 winform 应用程序&#xff0c;需要自定义一种窗口的样式&#xff0c;所以就随便搞了一个简单的窗口。 效果图 有两种样式&#xff0c;界面如下&#xff1a; 无标题&#xff1a; 有标题&#xff1a; 关键词 1、黑色描边边框 对于…

SpringBoot时间格式化的5种方法!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在我们日常工作中&#xff0c;时间格式化是一件经常遇到的事儿&#xff0c;所以本文我们就来盘点一下 Spring Boot 中时间格…

C#文件加密和解密

下载 CSDN下载&#xff1a;https://download.csdn.net/download/myinc/9913318 Github&#xff1a;GitHub 如果没有积分&#xff0c;也可以关注我获取哟~【文件加密】 // * 最近看了一下加密算法&#xff0c;对加密文件突然很感兴趣&#xff0c;就研究了一下&#xff1a;…

SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!

大家好&#xff0c;我是磊哥。今天我们来聊一聊在基于SpringBoot前后端分离开发模式下&#xff0c;如何友好的返回统一的标准格式以及如何优雅的处理全局异常。首先我们来看看为什么要返回统一的标准格式&#xff1f;为什么要对SpringBoot返回统一的标准格式在默认情况下&#…

zabbix企业应用之监控docker容器资源情况

关于docker的监控&#xff0c;无论开源的CAdvisor、Data Dog还是我自己写的监控&#xff08;http://dl528888.blog.51cto.com/2382721/1635951&#xff09;&#xff0c;不是通过docker的stats api就是使用socket来进行。单独看一个主机的监控项还行&#xff0c;比如只查看容器t…

使用了synchronized,竟然还有线程安全问题!

线程安全问题一直是系统亘古不变的痛点。这不&#xff0c;最近在项目中发了一个错误使用线程同步的案例。表面上看已经使用了同步机制&#xff0c;一切岁月静好&#xff0c;但实际上线程同步却毫无作用。关于线程安全的问题&#xff0c;基本上就是在挖坑与填坑之间博弈&#xf…

序列图| 软件工程

什么是时序图&#xff1f; (What is Sequence Diagram?) Sequence Diagram is a "Connection Diagram" that represents a single structure or storyline executing in a system. It is the second most used UML diagram behind the class diagram. Sequence Diag…

终极解密输入网址按回车到底发生了什么?

详解输入网址点击回车&#xff0c;后台到底发生了什么。透析 HTTP 协议与 TCP 连接之间的千丝万缕的关系。掌握为何是三次握手四次挥手&#xff1f;time_wait 存在的意义是什么&#xff1f;全面图解重点问题&#xff0c;再也不用担心面试问这个问题。大致流程URL 解析&#xff…

unity, 相机空间 与 相机gameObject的局部空间

在unity里 相机空间 与 相机gameObject的局部空间 不重合。 Camera.worldToCameraMatrix的文档中有这样一句话&#xff1a; Note that camera space matches OpenGL convention: cameras forward is the negative Z axis. This is different from Unitys convention, where for…

Winform实现漂亮动画-小火车

一、起因 最近在做一个Winform的项目&#xff0c;其中需要一些加载动画&#xff0c;所以就搜索了一下找些思路&#xff0c;以下链接是本文的参考。 参考&#xff1a;Jeremie Martinez &#xff08;译文链接&#xff09; 注&#xff1a;原文中并没有给出图片资源&#xff0c;图…

synchronized 加锁 this 和 class 的区别!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;synchronized 是 Java 语言中处理并发问题的一种常用手段&#xff0c;它也被我们亲切的称之为“Java 内置锁”&#xff0c;由…

C# WinForm窗体四周阴影效果

一、起因 关于winform窗体无边框的问题很简单&#xff0c;只需要设置winform的窗体属性即可&#xff1a; FormBorderStyle FormBorderStyle.None; 但是这中无边框窗口实现的效果和背景完全没有层次的感觉&#xff0c;所以能加上阴影&#xff0c;突出窗口显示的感觉。 二、…

synchronized 优化手段之锁膨胀机制!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;synchronized 在 JDK 1.5 之前性能是比较低的&#xff0c;在那时我们通常会选择使用 Lock 来替代 synchronized。然而这个情…

NTFS USN的Create和工具代码汇总

1、 因为之前把相关代码放在了GitHub上&#xff0c;后来突然有人帮忙改了些个BUG&#xff0c;非常感谢 760193107&#xff0c;所以就写了个完整点的例子&#xff0c;希望对别人有所帮助。 GitHub项目地址 2、错误码&#xff1a;ERROR_JOURNAL_NOT_ACTIVE 在测试时&#xff…

在Java中,负数的绝对值不一定是正数!

作者 l Hollis来源 l Hollis&#xff08;ID&#xff1a;hollischuang&#xff09;绝对值是指一个数在数轴上所对应点到原点的距离&#xff0c;所以&#xff0c;在数学领域&#xff0c;正数的绝对值是这个数本身&#xff0c;负数的绝对值应该是他的相反数。这几乎是每个人都知道…

自己写着玩(二)

转载于:https://www.cnblogs.com/wangmengmeng/p/4572611.html

实战:隐藏SpringBoot中的私密数据!

这几天公司在排查内部数据账号泄漏&#xff0c;原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上&#xff0c;导致核心数据外漏&#xff0c;孩子还是没挨过社会毒打&#xff0c;这种事的后果可大可小。说起这个我是比较有感触的&#xff0c;之前我TM被删库…

JS的条形码和二维码生成

一、前言 最近做项目用到了JS生成条形码和二维码&#xff0c;内容不多&#xff0c;整理一下方便使用。 2018年7月5日更新&#xff1a; 二维码生成时&#xff0c;如果长度太长会有异常&#xff1a; Uncaught Error: code length overflow. (1604>1056) 创建的时候&#…

synchronized 中的 4 个优化,你知道几个?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;synchronized 在 JDK 1.5 时性能是比较低的&#xff0c;然而在后续的版本中经过各种优化迭代&#xff0c;它的性能也得到了前…