【Java原理系列】 Java可序列化接口Serializable原理全面用法示例源码分析
实现Serializable接口的类表示该类可以进行序列化。未实现此接口的数据实现数据类将不会被序列化或反序列化。所有实现Serializable接口的分析分析agplayer源码子类也是可序列化的。这个序列化接口没有方法或字段,源码仅用于标识可序列化的平台语义。
为了使非可序列化的数据实现数据类的子类能够进行序列化,子类需要承担保存和恢复父类的分析分析公共、受保护以及(如果可访问)包级字段状态的源码责任。只有当扩展的平台类具有可访问的无参构造函数来初始化类的状态时,子类才能承担这种责任。数据实现数据如果不满足这个条件,分析分析则声明类为可序列化是源码错误的,错误会在运行时被检测到。
在反序列化过程中,非可序列化类的字段将使用类的公共或受保护的无参构造函数进行初始化。无参构造函数必须对可序列化的子类可访问。可序列化子类的字段将从流中恢复。
在遍历图形结构时,可能会遇到不支持Serializable接口的对象。在这种情况下,将抛出NotSerializableException异常,并标识非可序列化对象的类。
实现Serializable接口的类需要显式指定自己的serialVersionUID,以确保在不同的java编译器实现中获得一致的值。如果未显式声明serialVersionUID,则序列化运行时会根据类的各个方面计算出一个默认的serialVersionUID值。
在使用Serializable接口时,有一些注意事项需要注意。例如,writeObject方法适用于以下场景:在覆写writeObject方法时,必须调用out.defaultWriteObject()来使用默认的序列化机制将对象的非瞬态字段写入输出流。只有在确实需要自定义序列化行为或保存额外的字段时,才需要覆写writeObject方法。
可以使用Externalizable接口替代Serializable接口,以实现更细粒度的控制,但需要更多的开源斗地主源码开发工作。Externalizable接口允许在序列化时指定额外的字段,但需要在类中实现writeExternal和readExternal方法。
序列化和反序列化的过程是通过ObjectOutputStream和ObjectInputStream来完成的。可以使用这两个类的writeObject和readObject方法来手动控制序列化和反序列化的过程。
序列化示例:定义了一个Person类,并实现了Serializable接口。Person类有两个字段:name和age。age字段使用了transient关键字修饰,表示该字段不会被序列化。在main方法中,创建了一个Person对象并将其序列化到文件中。从文件中读取序列化的数据,并使用强制类型转换将其转换为Person对象。输出原始的person对象和恢复后的对象,验证序列化和反序列化的结果。
序列化兼容性示例:在类进行了修改后,可以通过显式声明serialVersionUID来解决之前序列化的对象无法被正确反序列化的问题。
加密和验证示例:在进行网络传输或持久化存储时,可以使用加密算法对序列化的数据进行加密,或使用数字签名来验证数据的完整性。
自定义序列化行为示例:如果需要对对象的状态进行特殊处理,或以不同于默认机制的方式序列化对象的字段,可以通过覆写writeObject方法来控制序列化过程。
使用Externalizable接口的示例:定义一个类,实现Externalizable接口,并在类中实现writeExternal和readExternal方法,用于保存和恢复额外的字段。
序列化和反序列化的源码分析:序列化示例中的writeObject方法用于将指定的对象写入ObjectOutputStream中进行序列化。而readObject方法用于从ObjectInputStream中读取一个对象进行反序列化。
序列化和反序列化的核心代码段展示了如何在序列化和反序列化过程中处理对象的类、类的签名以及类和其所有超类的非瞬态和非静态字段的值。确保了对象的完整恢复和验证过程的执行。
源码分析: Java中锁的种类与特性详解
在Java中存在多种锁,包括ReentrantLock、Synchronized等,它们根据特性与使用场景可划分为多种类型,如乐观锁与悲观锁、可重入锁与不可重入锁等。任务网源码手机本文将结合源码深入分析这些锁的设计思想与应用场景。
锁存在的意义在于保护资源,防止多线程访问同步资源时出现预期之外的错误。举例来说,当张三操作同一张银行卡进行转账,如果银行不锁定账户余额,可能会导致两笔转账同时成功,违背用户意图。因此,在多线程环境下,锁机制是必要的。
乐观锁认为访问资源时不会立即加锁,仅在获取失败时重试,通常适用于竞争频率不高的场景。乐观锁可能影响系统性能,故在竞争激烈的场景下不建议使用。Java中的乐观锁实现方式多基于CAS(比较并交换)操作,如AQS的锁、ReentrantLock、CountDownLatch、Semaphore等。CAS类实现不能完全保证线程安全,使用时需注意版本号管理等潜在问题。
悲观锁则始终在访问同步资源前加锁,确保无其他线程干预。ReentrantLock、Synchronized等都是典型的悲观锁实现。
自旋锁与自适应自旋锁是另一种锁机制。自旋锁在获取锁失败时采用循环等待策略,避免阻塞线程。自适应自旋锁则根据前一次自旋结果动态调整等待时间,提高效率。
无锁、偏向锁、轻量级锁与重量级锁是Synchronized的锁状态,从无锁到重量级锁,锁的竞争程度与性能逐渐增加。Java对象头包含了Mark Word与Klass Pointer,hdmi dsd源码 kodiMark Word存储对象状态信息,而Klass Pointer指向类元数据。
Monitor是实现线程同步的关键,与底层操作系统的Mutex Lock相互依赖。Synchronized通过Monitor实现,其效率在JDK 6前较低,但JDK 6引入了偏向锁与轻量级锁优化性能。
公平锁与非公平锁决定了锁的分配顺序。公平锁遵循申请顺序,非公平锁则允许插队,提高锁获取效率。
可重入锁允许线程在获取锁的同一节点多次获取锁,而不可重入锁不允许。共享锁与独占锁是另一种锁分类,前者允许多个线程共享资源,后者则确保资源的独占性。
本文通过源码分析,详细介绍了Java锁的种类与特性,以及它们在不同场景下的应用。了解这些机制对于多线程编程至关重要。此外,还有多种机制如volatile关键字、原子类以及线程安全的集合类等,需要根据具体场景逐步掌握。
记一次源码追踪分析,从Java到JNI,再到JVM的C++:fileChannel.map()为什么快;源码分析map方法,put方法
前言
在系统IO相关的系统调用有read/write,mmap,sendfile等这些。
其中read/write是普通的读写,每次都需要将buffer从用户空间拷贝到内核空间;
而mmap使用的是内存映射,会将磁盘文件对应的页映射(拷贝)到内核空间的page cache,并记录到用户进程的页表中,使得用户空间也可以像操作用户空间一样操作该文件的映射,最后再由操作系统来讲该映射(脏页)回写到磁盘;
sendfile则使用的是零拷贝技术,在mmap的基础上,当发送数据的发卡网源码6.0时候只拷贝fd和offset等元数据信息,而将数据主体直接拷贝至protocol buffer,实现了内核数据零冗余的零拷贝技术
本文地址:/post//
问题/目的问题1Java中哪些API使用到了mmap问题2怎么知道该API使用到了mmap,如何追踪程序的系统调用目的1源码中分析验证,从Java到JNI,再到C++:fileChannel.map()使用的是系统调用mmap目的2源码验证分析:调用mmapedByteBuffer.put(Byte[])时JVM在搞些什么?mmap比普通的read/write快在哪?揭晓答案1mmap在Java NIO中的体现/使用看一个例子
// 1GBpublic static final int _GB = 1**;File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer mmapedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB);for (int i = 0; i < _GB; i++) { count++;mmapedByteBuffer.put((byte)0);}其中fileChannel.map()底层使用的就是系统调用mmap,函数签名为: public abstract MappedByteBuffer map(MapMode mode,long position, long size)throws IOException
答案2程序执行的系统调用追踪/** * @author Tptogiar * @description * @date /5/ - : */public class TestMappedByteBuffer{ public static final int _4kb = 4*;public static final int _GB= 1**;public static void main(String[] args) throws IOException, InterruptedException { // 为了方便在日志中找到本段代码的开始位置和结束位置,这里利用文件io来打开始标记FileInputStream startInput = null;try { startInput = new FileInputStream("start1.txt");startInput.read();} catch (IOException e) { e.printStackTrace();}File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); //我们想分析的语句问题2for (int i = 0; i < _GB; i++) { map.put((byte)0); // 下文中需要分析的语句目的2}// 打结束标记FileInputStream endInput = null;try { endInput = new FileInputStream("end.txt");endInput.read();} catch (IOException e) { e.printStackTrace();}}}把上面这段代码编译后把“.class”文件拉到linux执行,并用linux上的strace工具记录其系统调用日志,拿到日志文件我们可以在日志中看到以下信息(关于怎么拿到日志可以参照我的博文:无(代写)):
注:日志有多行,这里只选取我们关注的
// ...// 看到了我们打的开始标志openat(AT_FDCWD, "start1.txt", O_RDONLY) = -1 ENOENT (No such file or directory)// ... // 打开文件,文件描述符fd为6openat(AT_FDCWD, "filename", O_RDWR|O_CREAT, ) = 6// 判断文件状态fstat(6, { st_mode=S_IFREG|, st_size=, ...}) = 0// ... // 判断文件状态fstat(6, { st_mode=S_IFREG|, st_size=, ...}) = 0// 进行内存映射mmap(NULL, , PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x7f2fd6cd// ...// 程序退出exit(0)// 看到了我们打的结束标志openat(AT_FDCWD, "end.txt", O_RDONLY) = -1 ENOENT (No such file or directory)在上面程序的系统调用日志中我们确实看到了我们打的开始标志,结束标志。在开始标志和结束标志之间我们看到了我们的文件"filename"确实被打开了,文件描述符fd = 6;在打开文件后紧接着又执行了系统调用mmap,这一点我们Java代码一致,这样,我们就验证了我们答案1中的结论,可以开始我们的下文了
源码追踪分析,从Java到JNI,再到JVM的C++目的1寻源之旅:fileChannel.map()我们知道我们执行Java代码fileChannel.map()确实会在底层调用系统调用,那怎么在源码中得到验证呢?怎么落脚于源码进行分析呢?下面开始我们的寻源之旅
FileChannelImpl.map() 注:由于代码较长,这里代码中略去了一些我们不关注的,比如异常捕获等
public MappedByteBuffer map(MapMode mode, long position, long size)throws IOException{ // ...try { // ...synchronized (positionLock) { // ...long mapPosition = position - pagePosition;mapSize = size + pagePosition;try { // !我们要找的语句就在这!addr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError x) { // 如果内存不足,先尝试进行GCSystem.gc();try { Thread.sleep();} catch (InterruptedException y) { Thread.currentThread().interrupt();}try { // 再次试着mmapaddr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError y) { // After a second OOME, failthrow new IOException("Map failed", y);}}} // ...} finally { // ...}}上面函数源码中真正执行mmap的语句是在addr = map0(imode, mapPosition, mapSize),于是我们寻着这里继续追踪
FileChannelImpl.map0()
// Creates a new mappingprivate native long map0(int prot, long position, long length)throws IOException;可以看到,该方法是一个native方法,所以后面的源码我们需要到这个FileChannelImpl.class对应的fileChannelImpl.c中去看,所以我们需要去找到JDK的源码
在JDK源码中我们找到fileChannelImpl.c文件
fileChannelImpl.c 根据JNI的对应规则,我们找到该文件内对应的Java_sun_nio_ch_FileChannelImpl_map0方法,其源码如下:
JNIEXPORT jlong JNICALLJava_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this, jint prot, jlong off, jlong len){ void *mapAddress = 0;jobject fdo = (*env)->GetObjectField(env, this, chan_fd);jint fd = fdval(env, fdo);int protections = 0;int flags = 0;if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) { protections = PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) { protections = PROT_WRITE | PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) { protections =PROT_WRITE | PROT_READ;flags = MAP_PRIVATE;}// !我们要找的语句就在这里!mapAddress = mmap(0,/* Let OS decide location */len,/* Number of bytes to map */protections,/* File permissions */flags,/* Changes are shared */fd, /* File descriptor of mapped file */off); /* Offset into file */if (mapAddress == MAP_FAILED) { if (errno == ENOMEM) { JNU_ThrowOutOfMemoryError(env, "Map failed");return IOS_THROWN;}return handle(env, -1, "Map failed");}return ((jlong) (unsigned long) mapAddress);}我们要找的语句就上面代码中的mapAddress = mmap(0,len,protections,flags,fd,off),至于为什么不是直接的mmap,而是mmap,是因为这里的mmap是一个宏,在文件上方有其定义,如下:
#define mmap mmap至此,我们就在源码中得到验证了我们问题2中的结论:fileChannelImpl.map()底层使用的是mmap系统调用
目的2寻源之旅:mmapedByteBuffer.put(Byte[ ])接着我们来看看当我们调用mmapedByteBuffer.put(Byte[])JVM底层在搞些什么动作
MappedByteBuffer ?首先我们得知道,当我们执行MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB)时,实际返回的对象是DirectByteBuffer类的实例,因为MappedByteBuffer为抽象类,且只有DirectByteBuffer继承了它,看下面两图就明白了
DirectByteBuffer 于是我们找到DirectByteBuffer内的put(Byte[ ])方法
public ByteBuffer put(byte x) { unsafe.putByte(ix(nextPutIndex()), ((x)));return this;}可以看到该方法内实际是调用Unsafe类内的putByte方法来实现功能的,所以我们还得去看Unsafe类
Unsafe.class
public native voidputByte(long address, byte x);该方法在Unsafe内是一个native方法,所以所以我们还得去看unsafe.cpp文件内对应的实现
unsafe.cpp
在JDK源码中,我们找到unsafe.cpp
在这份源码内,没有使用JNI内普通加前缀的方法来形成对应关系
不过我们还是能顺着源码的蛛丝轨迹找到我们要找的方法
注意到源码中有这样的注册机制,所以我们可以知道我们要找的代码就是上图中标注的代码
顺藤摸瓜,我们就找到了该方法的定义
UNSAFE_ENTRY(void, Unsafe_SetNative##Type(JNIEnv *env, jobject unsafe, jlong addr, java_type x)) \UnsafeWrapper("Unsafe_SetNative"#Type); \JavaThread* t = JavaThread::current(); \t->set_doing_unsafe_access(true); \void* p = addr_from_java(addr); \*(volatile native_type*)p = x; \t->set_doing_unsafe_access(false); \UNSAFE_END \该方法内主要的逻辑语句就是以下两句:
/** * @author Tptogiar * @description * @date /5/ - : */public class TestMappedByteBuffer{ public static final int _4kb = 4*;public static final int _GB= 1**;public static void main(String[] args) throws IOException, InterruptedException { // 为了方便在日志中找到本段代码的开始位置和结束位置,这里利用文件io来打开始标记FileInputStream startInput = null;try { startInput = new FileInputStream("start1.txt");startInput.read();} catch (IOException e) { e.printStackTrace();}File file = new File("filename");FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); //我们想分析的语句问题2for (int i = 0; i < _GB; i++) { map.put((byte)0); // 下文中需要分析的语句目的2}// 打结束标记FileInputStream endInput = null;try { endInput = new FileInputStream("end.txt");endInput.read();} catch (IOException e) { e.printStackTrace();}}}0至此,我们就知道:其实我们调用mmapedByteBuffer.put(Byte[ ])时,JVM底层并不需要涉及到系统调用(这里也可以用strace工具追踪从而得到验证)。也就是说通过mmap映射的空间在内核空间和用户空间是共享的,我们在用户空间只需要像平时使用用户空间那样就行了————获取地址,设置值,而不涉及用户态,内核态的切换
总结fileChannelImpl.map()底层用调用系统函数mmap
fileChannelImpl.map()返回的其实不是MappedByteBuffer类对象,而是DirectByteBuffer类对象
在linux上可以通过strace来追踪系统调用
JNI中“.class”文件内方法与“.cpp”文件内函数的对应关系不止是前缀对应的方法,还可以是注册的方式,这一点的追寻代码的时候有很大帮助
directByteBuffer.put()方法底层并没有涉及系统调用,也就不需要涉及切态的性能开销(其底层知识执行获取地址,设置值的操作),所以mmap的性能就比普通读写read/write好
...
原文:/post/我的RxJava源码解读笔记
RxJava是一个用于处理异步任务的库,其主要功能包括观察者模式、数据发送与接收、切换线程、数据变换等。在学习RxJava源码时,梳理了其工作流程,包括创建Observable、创建观察者(使用Subscriber)、订阅(使用subscribe方法)、变换操作(如map、compose)、线程切换(通过subscribeOn和observeOn方法)等关键步骤。从源码角度深入理解了RxJava的工作原理,如Observable的创建、Subscriber的实现、OnSubscribe的作用、Subscription的生命周期管理、变换操作的具体实现以及线程控制机制。通过分析RxJava的源码,不仅加强了记忆,也为实际应用提供了清晰的指导。RxJava通过观察者模式实现了数据的高效异步处理,支持在线程间灵活切换,通过变换操作符实现了数据的转换,是处理异步编程和事件流的理想工具。
Java并发系列 | Semaphore源码分析
在Java并发编程中,Semaphore(信号量)是AQS共享模式的实用工具,它能够控制多个线程对共享资源的并发访问,实现流量控制。Semaphore的核心概念是“许可证”,类似于公共汽车票,只有获取到票的线程才能进行操作。许可证数量有限,当数量耗尽时,后续线程需要等待,直到有线程释放其许可证。Semaphore构造器接受初始许可证数量,可以选择公平或非公平的获取方式。
Semaphore提供了获取和释放许可证的API,默认每次操作一个许可证。获取许可证有直接和尝试两种方式,直接获取可能阻塞,而尝试不会。acquire方法内部调用的是AQS的acquireSharedInterruptibly,它会尝试公平或非公平地获取,并在获取失败时决定是否阻塞。释放许可证则直接调用AQS的releaseShared方法,通过自旋循环确保同步状态的正确更新。
Semaphore的应用广泛,本文通过实现一个简单的数据库连接池,展示了Semaphore如何控制连接的并发使用。连接池初始化时创建固定数量的连接,每次线程请求连接时需要获取许可证,释放连接时则释放许可证。测试结果验证了Semaphore有效管理连接并发并确保了流量控制。
代码示例与测试结果表明,Semaphore通过控制许可证数量,确保了资源使用的合理调度,当连接池中所有连接被占用,后续请求将被阻塞,直到有连接被释放。这清楚地展示了Semaphore在并发控制中的作用。
基于java实现数据分析与简历生成系统
本项目是一套基于Java实现的学生管理系统,旨在提供学生和教师进行信息管理的便捷平台。系统由五个核心界面构成:初始界面、登陆界面、注册界面、学生主界面和教师主界面。首次进入系统,用户可选择登陆或注册。登录时,依据输入的姓名与密码,系统会跳转至对应的角色界面(学生或教师)。对于新用户,注册流程后会直接导向个人页面。
学生主界面中展示学生基本信息,包括姓名、班级、身份、个人简介、成绩和排名、获奖信息等,可对上述除姓名、班级、身份外的信息进行修改。系统提供了导出个人文件功能,学生可以下载关于个人信息的txt或md格式文件,作为简易简历使用。
在教师主界面,教师可以查看所对应学生组别的成绩信息,并能够对成绩数据进行修改,还有分析某一科目成绩分布的选项。
在项目实现中,我们精心组织系统架构与关键模块:包含异常处理、多线程技术、文件存储、网络编程以及Java FX图形界面应用。
MySQL作为后台数据存储,实现数据库的增删查改。使用了FXML进行界面设计,该技术提供了一种基于XML的、用于描述JavaFX应用程序用户界面的声明性标记语言,相比Swing,JavaFX的界面更丰富、功能更强大。对于Java JDBC数据库连接技术,系统通过调用MySQL接口完成数据操作。
主要创新点在于使用JavaFX实现的多窗口界面切换体验与文件导出功能,以及通过网络编程实现的用户名传递机制。面对多线程环境及资源管理,项目进行了深入优化,有效解决了并发访问与资源冲突问题。
项目实现过程中,还存在一些挑战与未解决的问题,如系统安全性优化、用户权限管理与数据隐私保护等问题。同时,持续学习新技术与持续迭代升级系统功能,以满足日益增长的需求,是团队持续努力的方向。
Java集合-Vector介绍、扩容机制、源码分析
Java集合框架中的Vector类是一种古老的线程安全的数组列表,本文将简要介绍Vector,深入剖析其扩容机制,以及源码层面的解析。
首先,我们来看创建Vector的方式。Vector提供了无参构造器和带初始容量和扩容增量的构造器。无参构造会设置initialCapacity为,capacityIncrement默认为数组长度的两倍。例如,调用this()或this(initialCapacity, 0),实际上是为元素数据(elementData)分配了初始容量,但后续扩容会根据capacityIncrement值调整,如未指定则每次翻倍。
当向Vector添加元素时,会触发add方法。例如,添加第一个元素1,若数组已满,会调用ensureCapacityHelper(elementCount + 1),确保空间。此处,由于初始容量为,添加1后不需要扩容,元素直接添加到0索引。后续添加时,由于需要个位置,会进行扩容。判断条件是:新的容量减去最小需求小于0时,才会进行扩容,通常是将容量扩大为当前容量的两倍或直接扩容到满足需求的最小值。
总的来说,Vector的扩容机制是动态的,确保在元素数量增长时,内存空间能相应扩展。源码中,add方法、ensureCapacityHelper函数和grow方法共同实现了这一机制,保证了Vector在高并发环境下的线程安全。通过理解这些细节,我们可以更好地运用Vector并优化程序性能。
2024-11-14 12:35
2024-11-14 12:10
2024-11-14 11:56
2024-11-14 11:49
2024-11-14 10:03