1.java并发原子类AtomicBoolean解析
2.Netty源码探究1:事件驱动原理
3.基本功Java魔法类:Unsafe应用解析
4.Java ä¸LockSupportç±»å¨C#ä¸çå®ç°
5.Java之Unsafe-越迷人的类源越危险
6.JUC原子类: CAS, Unsafe和原子类详解
java并发原子类AtomicBoolean解析
本文针对Java并发包下的原子类AtomicBoolean进行深入解析。在多线程环境中,码j码分传统的类源布尔变量`boolean`并非线程安全,容易导致数据竞争问题。码j码分为解决这一问题,类源引入了AtomicBoolean类,码j码分如何上传游戏源码该类提供了一种线程安全的类源布尔值封装。
使用`AtomicBoolean`的码j码分主要原因在于其提供的原子操作保证了多线程环境下的线程安全。在`AtomicBoolean`内部实现中,类源主要依赖于`compareAndSet`方法和CAS(Compare and Swap)机制。码j码分通过CAS操作,类源`AtomicBoolean`能够在多线程环境下实现原子的码j码分更新操作,有效避免了数据竞争和并发问题。类源
在`AtomicBoolean`的码j码分源码中,`compareAndSet`方法使用了`Unsafe`类的类源`compareAndSwapInt`方法进行底层操作。CAS机制的核心思想是:在不进行锁操作的情况下,检查指定内存位置的预期值是否与当前值相等,若相等,则更新该位置的值为预期值;若不相等,则操作失败,返回原值。
为了理解这一机制,我们可以通过一个简单例子进行说明。假设我们希望在多线程环境下实现一个“先来后到”的规则,例如:一个人完成起床、上班和下班三件事后,另一个人才能开始。在单线程下,这一逻辑自然无问题,但在多线程环境下,`AtomicBoolean`可以确保这一顺序得到实现。影视app源码官方
在实际应用中,`AtomicBoolean`类提供了丰富的原子操作方法,包括但不限于`compareAndSet`、`getAndSet`、`compareAndExchange`等。这些方法允许开发人员在多线程环境下安全地执行原子操作,简化了多线程编程的复杂性。
总结而言,`AtomicBoolean`是一个在Java并发编程中非常实用的工具类,它通过原子操作保证了多线程环境下的线程安全。对于开发者而言,掌握`AtomicBoolean`的使用方法和原理,可以有效避免数据竞争问题,提升程序的并发性能和稳定性。
Netty源码探究1:事件驱动原理
Netty源码探究1:事件驱动原理
Netty借鉴了Reactor设计模式,这是一种事件处理模式,用于管理并发服务请求。在模式中,服务处理器对请求进行I/O多路复用,并同步分发给相应的请求处理器。Netty的核心内容是Reactor,因此深入分析其在Netty中的应用至关重要。Netty吸收了前人优秀经验,构建出这款优秀的技术框架。
在Reactor设计模式中,Demultiplexer和Dispatcher是关键概念。Netty中的Demultiplexer是如何实现的?答案在于其Server端的架构设计。Netty通过Bootstrap(ServerBootstrap也适用)来构建Server,其中bind方法是启动Reactor运行的关键。在bind方法中,源码抓取工具appNetty创建并注册Channel到EventLoopGroup,从而实现Demultiplexer的功能。
Netty中的Channel与JDK中的Channel有何不同?Netty通过NioServerSocketChannel构建Server,其内部封装了Java NIO的Channel,但Netty的Channel与JDK中的Channel在注册到Selector时有所不同。Netty中的Channel注册到NioEventLoop中的Selector上,只关注OP_ACCEPT事件。当客户端连接时,事件被触发,Server响应客户端连接。这涉及NioServerSocketChannel的构造过程和Selector的创建。
Dispatcher在Java NIO中负责事件分发,Netty中如何实现这一功能?在NioEventLoop中,Selector.select()方法配合run()函数,共同实现事件监听循环。run函数中包含事件状态机和事件分派逻辑。当有事件到来时,状态机触发processSelectedKeys()方法,根据事件类型调用相应处理器进行处理。
Netty中的事件驱动原理最终如何与自定义handler关联?在NioEventLoop的processSelectedKey()方法中,事件处理逻辑与Channel.Unsafe接口相关联。Channel.Unsafe接口用于封装Socket的最终操作,Netty通过此接口与业务层Handler建立关联。通过调用handler的read方法,Netty将事件与业务处理逻辑关联起来。
总之,Netty通过Reactor设计模式实现了事件驱动原理,借助Demultiplexer和Dispatcher的机制,实现了对并发请求的高效处理。理解Netty的游资接力指标源码源码结构和事件驱动原理,对于深入掌握Netty技术框架至关重要。
基本功Java魔法类:Unsafe应用解析
Unsafe是Java中sun.misc包下的一个类,提供低级操作内存资源的方法。这些方法在提升Java运行效率与增强底层资源操作能力方面起到关键作用。然而,由于其允许类似C语言指针的操作,程序中不当使用可能导致指针相关问题,从而增加程序出错风险。因此,使用时需谨慎。
本文介绍sun.misc.Unsafe的公共API功能与应用场景。
基本介绍
Unsafe为单例实现,提供静态方法getUnsafe用于获取实例。合法获取条件为调用方法的类必须由引导类加载器加载,否则将抛出SecurityException异常。
获取Unsafe实例有两个方案:通过-Xbootclasspath/a参数追加调用相关方法类的jar包路径,使类被引导类加载器加载;或通过反射获取单例对象theUnsafe。
功能介绍
Unsafe提供内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等API。下文详细说明。
内存操作
这部分包含堆外内存的分配、拷贝、释放与指定地址值操作。通常,农场经营游戏源码Java中的对象在堆内存中,而堆外内存存在于JVM控制之外。Java中使用Unsafe实现堆外内存操作。
使用堆外内存的原因在于特定应用场景,如Netty、MINA等NIO框架中的缓冲池。
堆外内存释放通过构建Cleaner对象完成,Cleaner与虚引用结合,实现对象被垃圾回收时堆外内存的自动释放。
CAS相关
这部分提供CAS操作方法,实现并发算法中数据一致性控制。CAS操作包含内存位置、预期原值与新值,处理器在比较相匹配时自动更新内存值。
典型应用在AtomicInteger、Java AQS、CurrentHashMap等实现中。
线程调度
包括线程挂起、恢复与锁机制。通过Unsafe的park、unpark方法实现线程挂起与恢复。
Class相关
提供Class及其静态字段操作,包含内存定位、类定义、匿名类定义与类初始化。
典型应用为Java 8中Lambda表达式实现。
对象操作
包含对象成员属性操作与非常规对象实例化方式。
数组相关
涉及与数据操作相关的arrayBaseOffset与arrayIndexScale方法,用于定位数组元素。
内存屏障
用于定义内存屏障,避免代码重排序。
系统相关
包含获取系统信息的pageSize方法。
总结
Unsafe提供多种便捷API,但在不当使用下可能导致程序出错。因此,其使用需谨慎。
Java ä¸LockSupportç±»å¨C#ä¸çå®ç°
ããJava ä¹åæä¾ä¼ç§ç并ååºncurrent Netä¸ç¼ºä¹ç±»ä¼¼çåè½ ç±äºç¡¬ä»¶ä½ç³»åçäºåå å¤æ ¸æ¶ä»£æ¥ä¸´ NETä¸ç¼ºä¹å¹¶åç±»åºæ¾ç¶ä¸åæ¶å® ç¼è§£è¿ä¸çç¾çå ¶ä¸ä¸ä¸ªåæ³å°±æ¯å¨å¾C#ä¸ç§»æ¤javaçncurrentããjavaä¸çncurrentå ä¸æä¾äºä¸ä¸ªç±»LockSupport ncurrentå å¾å¤å ³é®å®ç°éè¦è°ç¨LockSupport å¦æéè¦æjavaçncurrentå è¿ç§»å°C#ä¸ LockSupportç±»çè¿ç§»æ¯ä¸å¯é¿å çé®é¢
ããå¨javaä¸ LockSupportç±»æå¦ä¸æ¹æ³
ãã以ä¸æ¯å¼ç¨ç段
ãã
ããpublic static void park(Object blocker) { ããThread t = Thread currentThread(); ããsetBlocker(t blocker); ããunsafe park(false L); ããsetBlocker(t null); ãã}
ããå½ä¸ä¸ªçº¿ç¨è°ç¨LockSupport parkä¹å 线ç¨å°±ä¼åä¸è½½ 类似äºObject wait æè NETä¸çSystem Threading Monitor Wait ä½é®é¢æ¯javaä¸çObject waitå NETä¸çMonitor wait é½éè¦ä¸ä¸ªwaitObject è¿ä¸ªé®é¢æ¾ç»å°æ°æ 为æ¤ç¿»äºä¸éJDK å®ç°æºç å°æååç°ç解å³åæ³å´æ¯å¾ç®å ä¹æ éäºè§£JDKçåºå±å®ç°æºç
ãã以ä¸æ¯å¼ç¨ç段
ãã
ããpublic class LockSupport ãã{ ããprivate static LocalDataStoreSlot slot = Thread GetNamedDataSlot ( LockSupport Park ); ããpublic static void Park(Object blocker) ãã{ ããThread thread = Thread CurrentThread; ããThread SetData(slot blocker); ããlock (thread) ãã{ ããMonitor Wait(thread); ãã} ãã} ããpublic static void Unpark(Thread thread) ãã{ ããif (thread == null) return; ããlock (thread) ãã{ ããMonitor Pulse(thread); ãã} ãã} ãã}
lishixinzhi/Article/program/net//Java之Unsafe-越迷人的越危险
简要介绍:
Java语言先比较与C和C++有一个非常大的不同点在于Java语言无法直接操作内存,实际开发中,默认都是由JVM来进行内存分配和垃圾回收,而JVM在进行垃圾回收的时候,绝大多数垃圾回收器都需要STW(stop the world)这个问题往往会导致服务短暂或者较长时间的暂停。因此Unsafe提供了通过Java直接操作内存的API,尽管Unsafe是JavaNIO和并发的核心类,但是其如其名,这是一个官方不推荐开发者使用的及其不安全的类!
主要作用:序号作用API1内存管理。(包括分配内存、释放内存等。)allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)2非常规的对象实例化allocateInstance()方法提供了另一种创建实例的途径3操作类、对象、变量staticFieldOffset(静态域偏移)、defineClass(定义类)、defineAnonymousClass(定义匿名类)、ensureClassInitialized(确保类初始化)、objectFieldOffset(对象域偏移)4数组操作arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法5多线程同步。包括锁机制、CAS操作等monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap6挂起与恢复park、unpark7内存屏障loadFence、storeFence、fullFence一、获取Unsafe源码-基于jdk1.8/**在Unsafe源码中限制了获取Unsafe的ClassLoader,如果这个方法调用实例不是由BootStrap类加载器加载的,则会报错*因此,我们如果需要使用Unsafe类,可以通过反射的方式来获取。*/@CallerSensitivepublicstaticUnsafegetUnsafe(){ Classvar0=Reflection.getCallerClass();//此处会判断ClassLoader是否为空,BootStrap由C语言编写,在Java中获取会返回null。if(!VM.isSystemDomainLoader(var0.getClassLoader())){ thrownewSecurityException("Unsafe");}else{ returntheUnsafe;}}获取方式/***反射获取Unsafe**@returnUnsafe*/publicstaticfinalUnsafegetUnsafe(){ Unsafeunsafe=null;try{ FieldtheUnsafe=Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);unsafe=(Unsafe)theUnsafe.get(null);}catch(NoSuchFieldException|IllegalAccessExceptione){ e.printStackTrace();}returnunsafe;}二、操作方法数组操作packagecom.liziba.unsafe;importsun.misc.Unsafe;/***<p>*操作数组示例*</p>**@Author:Liziba*@Date:/5/:*/publicclassOperateArrayExample{ /***1、publicnativeintarrayBaseOffset(Class<?>var1);获取数组第一个元素的偏移地址*2、publicnativeintarrayIndexScale(Class<?>var1);获取数组中元素的增量地址*3、publicObjectgetObject(Objectvar1,intvar2);通过对象和地址偏移量获取元素*/publicstaticvoidoperateArrayUseUnsafe(){ //测试数组String[]exampleArray=newString[]{ "李","子","捌"};Unsafeunsafe=UnsafeFactory.getUnsafe();//获取数组的基本偏移量intbaseOffset=unsafe.arrayBaseOffset(String[].class);System.out.println("String[]baseoffsetis:"+baseOffset);//获取数组中元素的增量地址intscale=unsafe.arrayIndexScale(String[].class);System.out.println("String[]indexscaleis:"+scale);//获取数组中第n个元素i=(baseOffset+(scale*n-1))System.out.println("thirdelementis:"+unsafe.getObject(exampleArray,baseOffset+(scale*2)));//修改数组中第n个元素i=(baseOffset+(scale*n-1))unsafe.putObject(exampleArray,baseOffset+scale*2,"柒");System.out.println("thirdelementis:"+unsafe.getObject(exampleArray,baseOffset+(scale*2)));}publicstaticvoidmain(String[]args){ OperateArrayExample.operateArrayUseUnsafe();}}输出结果
对象操作packagecom.liziba.unsafe;importcom.liziba.unsafe.pojo.User;importsun.misc.Unsafe;importjava.io.File;importjava.io.FileInputStream;importjava.lang.reflect.Constructor;importjava.lang.reflect.Field;/***<p>*操作对象示例*</p>**@Author:Liziba*@Date:/5/:*/publicclassOperateObjectExample{ /***1、publicnativeObjectallocateInstance(Class<?>var1);分配内存*2、publicnativeClass<?>defineClass(Stringvar1,byte[]var2,intvar3,intvar4,ClassLoadervar5,ProtectionDomainvar6);方法定义一个类用于动态的创建类*@throwsException*/publicstaticvoidoperateObjectUseUnsafe()throwsException{ Unsafeunsafe=UnsafeFactory.getUnsafe();//使用Unsafe的allocateInstance()方法,可以无需使用构造函数的情况下实例化对象Useruser=(User)unsafe.allocateInstance(User.class);user.setId(1);user.setName("李子捌");System.out.println(user);//返回对象成员属性在内存中相对于对象在内存中地址的偏移量Fieldname=User.class.getDeclaredField("name");longfieldOffset=unsafe.objectFieldOffset(name);//使用Unsafe的putXxx()方法,可以直接修改内存地址指向的数据(可以越过权限访问控制符)unsafe.putObject(user,fieldOffset,"李子柒");System.out.println(user);//使用Unsafe在运行时通过.class文件,创建类FileclassFile=newFile("E:\workspaceall\liziba-javap5\out\production\liziba-javap5\com\liziba\unsafe\pojo\User.class");FileInputStreamfis=newFileInputStream(classFile);byte[]classContent=newbyte[(int)classFile.length()];fis.read(classContent);Class<?>clazz=unsafe.defineClass(null,classContent,0,classContent.length,null,null);Constructor<?>constructor=clazz.getDeclaredConstructor(int.class,String.class);System.out.println(constructor.newInstance(1,"李子玖"));}publicstaticvoidmain(String[]args){ try{ OperateObjectExample.operateObjectUseUnsafe();}catch(Exceptione){ e.printStackTrace();}}}输出结果
内存操作packagecom.liziba.unsafe;importsun.misc.Unsafe;/***<p>*内存地址操作示例*</p>**@Author:Liziba*@Date:/5/:*/publicclassOperateMemoryExample{ /***1、publicnativelongallocateMemory(longvar1);分配var1字节大小的内存,返回起始地址偏移量*2、publicnativelongreallocateMemory(longvar1,longvar3);重新给var1起始地址的内存分配长度为var3字节的内存,返回新的内存起始地址偏移量*3、publicnativevoidfreeMemory(longvar1);释放起始地址为var1的地址**分配地址的方法还有重分配,都是分配在堆外内存,返回的是一个long类型的地址偏移量。这个偏移量在Java程序中的每一块内存都是唯一的**/publicstaticvoidoperateMemoryUseUnsafe(){ Unsafeunsafe=UnsafeFactory.getUnsafe();//申请分配8byte的内存longaddress=unsafe.allocateMemory(1L);//初始化内存填充值unsafe.putByte(address,(byte)1);//测试输出System.out.println(newStringBuilder().append("address:").append(address).append("bytevalue:").append(unsafe.getByte(address)));//重新分配一个地址longnewAddress=unsafe.reallocateMemory(address,8L);unsafe.putLong(newAddress,L);System.out.println(newStringBuilder().append("address:").append(newAddress).append("longvalue:").append(unsafe.getLong(newAddress)));//释放地址,注意地址可能被其他使用unsafe.freeMemory(newAddress);System.out.println(newStringBuilder().append("address:").append(newAddress).append("longvalue:").append(unsafe.getLong(newAddress)));}publicstaticvoidmain(String[]args){ OperateMemoryExample.operateMemoryUseUnsafe();}}输出结果
CAS操作packagecom.liziba.unsafe;importcom.liziba.unsafe.pojo.User;importsun.misc.Unsafe;importjava.lang.reflect.Field;/***<p>*CAS操作示例*</p>**@Author:Liziba*@Date:/5/:*/publicclassOperateCASExample{ /***CAS==compareandswap(比较并替换)*当需要改变的值为期望值的时候,就替换为新的值,是原子(不可再分割)操作。Java中大量的并发框架底层使用到了CAS操作。*优势:无锁操作,减少线程切换带来的开销*缺点:CAS容易在并发的情况下失败从而引发性能问题,也存在ABA问题。**Unsafe中提供了三个方法*1、compareAndSwapInt*2、compareAndSwapLong*3、compareAndSwapObject**/publicstaticvoidoperateCASUseUnsafe()throwsException{ Useruser=newUser(1,"李子捌");System.out.println("preuservalue:"+user);Unsafeunsafe=UnsafeFactory.getUnsafe();Fieldid=user.getClass().getDeclaredField("id");Fieldname=user.getClass().getDeclaredField("name");//获取ID字段的内存偏移量longidFieldOffset=unsafe.objectFieldOffset(id);//获取name字段的内存偏移量longnameFieldOffset=unsafe.objectFieldOffset(name);//如果ID的期望值是1,则修改为successunsafe.compareAndSwapInt(user,idFieldOffset,1,);//如果name的期望值是小荔枝,则修改为李子柒failunsafe.compareAndSwapObject(user,nameFieldOffset,"小荔枝","李子柒");//输出修改的user对象System.out.println("postuservalue:"+user);}publicstaticvoidmain(String[]args){ try{ OperateCASExample.operateCASUseUnsafe();}catch(Exceptione){ e.printStackTrace();}}}输出结果
线程的挂起和恢复/***查看Java的java.util.concurrent.locks.LockSupport源代码可以发现LockSupport类*中有各种版本的pack方法但是最终都是通过调用Unsafe.park()方法实现的。*/publicclassLockSupport{ publicstaticvoidunpark(Threadthread){ if(thread!=null)UNSAFE.unpark(thread);}publicstaticvoidpark(Objectblocker){ Threadt=Thread.currentThread();setBlocker(t,blocker);UNSAFE.park(false,0L);setBlocker(t,null);}publicstaticvoidparkNanos(Objectblocker,longnanos){ if(nanos>0){ Threadt=Thread.currentThread();setBlocker(t,blocker);UNSAFE.park(false,nanos);setBlocker(t,null);}}publicstaticvoidparkNanos(longnanos){ if(nanos>0)UNSAFE.park(false,nanos);}publicstaticvoidparkUntil(Objectblocker,longdeadline){ Threadt=Thread.currentThread();setBlocker(t,blocker);UNSAFE.park(true,deadline);setBlocker(t,null);}publicstaticvoidparkUntil(longdeadline){ UNSAFE.park(true,deadline);}}我们平时如何实现浅克隆?
实现Closeable接口
重写close()方法
一、Unsafe实现浅克隆浅克隆工具类packagecom.liziba.unsafe.clone;importcom.liziba.unsafe.UnsafeFactory;importsun.misc.Unsafe;importjava.lang.reflect.Field;importjava.lang.reflect.Modifier;importjava.util.Arrays;/***<p>*浅克隆工具类*</p>**@Author:Liziba*@Date:/5/:*/publicclassShallowCloneUtil{ /***获取对象的内存地址**@Description*Unsafe类没有提供直接获取实例对象内存地址的方法,但是可以通过以下方式间接获取。*构建对象A,A包含了我们需要获取内存地址的B对象的引用,这样只有获取到A对象持有的B对象的引用地址,就可以知道B对象的地址了。*我们可以通过Unsafe类获取内存地址的方法publicnativelonggetLong(Objectvar1,longvar2)来获取;*此处我们为了方便,通过数组Object[]添加Object元素,持有Object的引用**@return*/publicstaticLonggetAddress(Objectobj){ Object[]objects=newObject[]{ obj};Unsafeunsafe=UnsafeFactory.getUnsafe();intarrayBaseOffset=unsafe.arrayBaseOffset(Object[].class);returnunsafe.getLong(objects,arrayBaseOffset);}/***获取对象的大小**@Dscription*Java中实例化一个对象时,JVM会在堆中分配非static的Field的内存,其他的static属性或者method在类加载期间或者JVM启动时已经存放在内存中。*所以我们计算对象的大小的时候只需要求和Field的大小就行了,JVM分配内存时,单个实例对象中的Field内存是连续不断地,*因此我们只需获取最大偏移量Filed的偏移量+最大偏移量Filed本身的大小即可**Java中基本数据类型所占的字节数*byte/boolean1字节*char/short2字节*int/float4字节*long/double8字节*boolean理论上占1/8字节,实际上按照1byte处理。*Java采用的是Unicode编码,每一个字节占8位,一个字节由8个二进制位组成。**@paramclazz*@return*/publicstaticLongsize(Classclazz){ //最后一个Filed的内存偏移量longmaxOffset=0;ClasslastFiledClass=null;Unsafeunsafe=UnsafeFactory.getUnsafe();do{ for(Fieldfield:clazz.getDeclaredFields()){ if(!Modifier.isStatic(field.getModifiers())){ longtmpOffset=unsafe.objectFieldOffset(field);if(tmpOffset>maxOffset){ maxOffset=tmpOffset;lastFiledClass=field.getType();}}}}while((clazz=clazz.getSuperclass())!=null);//最后一个Field本身的大小intlastFiledSize=(boolean.class.equals(lastFiledClass)||byte.class.equals(lastFiledClass))?1:(short.class.equals(lastFiledClass)||char.class.equals(lastFiledClass))?2:(int.class.equals(lastFiledClass)||float.class.equals(lastFiledClass))?4:8;returnmaxOffset+lastFiledSize;}/***申请一块固定大小的内存空间**@Description*通过Unsafe的publicnatJUC原子类: CAS, Unsafe和原子类详解
JUC原子类: CAS, Unsafe和原子类详解
本文将深入探讨Java中的并发控制工具:CAS(Compare-And-Swap)、Unsafe类以及JDK内置的原子类,它们在处理多线程同步中的关键作用。
CAS是一种原子操作,它通过硬件支持直接对比并替换值,避免了加锁带来的性能开销。Java的AtomicInteger等类就是基于CAS的封装。例如,AtomicInteger在多线程环境下,即使不加锁也能保证数据一致性,类似于SQL的单行更新操作。
然而,CAS存在ABA问题,即值可能在检查和更新之间被改变回原来的值。为解决这个问题,Java引入了AtomicStampedReference,通过添加版本号并同步更新,确保操作的原子性。
UnSafe类提供了底层的内存操作,如直接访问内存和执行原子操作,但使用时需谨慎,因为它增加了程序出错的风险。Atomic类如AtomicInteger、AtomicLong等,通过Unsafe实现高效且无锁的原子操作,但仅限于单个共享变量,处理多个变量时可能需要配合锁或将多个变量合并。
总结来说,CAS和Unsafe为Java并发编程提供了强大工具,但需注意它们的局限性和潜在风险,以确保代码的正确性和性能。
2024-11-30 00:43
2024-11-30 00:37
2024-11-30 00:31
2024-11-30 00:23
2024-11-30 00:06
2024-11-29 23:57