首页 文章 精选 留言 我的

精选列表

搜索[Java库],共10000篇文章
优秀的个人博客,低调大师

java面试-Java并发编程(九)——批量获取多条线程的执行结果

当向线程池提交callable任务后,我们可能需要一次性获取所有返回结果,有三种处理方法。 方法一:自己维护返回结果 // 创建一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); // 存储执行结果的List List<Future<String>> results = new ArrayList<Future<String>>(); // 提交10个任务 for ( int i=0; i<10; i++ ) { Future<String> result = executorService.submit( new Callable<String>(){ public String call(){ int sleepTime = new Random().nextInt(1000); Thread.sleep(sleepTime); return "线程"+i+"睡了"+sleepTime+"秒"; } } ); // 将执行结果存入results中 results.add( result ); } // 获取10个任务的返回结果 for ( int i=0; i<10; i++ ) { // 获取包含返回结果的future对象 Future<String> future = results.get(i); // 从future中取出执行结果(若尚未返回结果,则get方法被阻塞,直到结果被返回为止) String result = future.get(); System.out.println(result); } 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 此方法的弊端: 需要自己创建容器维护所有的返回结果,比较麻烦; 从list中遍历的每个Future对象并不一定处于完成状态,这时调用get()方法就会被阻塞住,如果系统是设计成每个线程完成后就能根据其结果继续做后面的事,这样对于处于list后面的但是先完成的线程就会增加了额外的等待时间。 方法二:使用ExecutorService的invokeAll函数 本方法能解决第一个弊端,即并不需要自己去维护一个存储返回结果的容器。当我们需要获取线程池所有的返回结果时,只需调用invokeAll函数即可。但是,这种方式需要你自己去维护一个用于存储任务的容器。 // 创建一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建存储任务的容器 List<Callable<String>> tasks = new ArrayList<Callable<String>>(); // 提交10个任务 for ( int i=0; i<10; i++ ) { Callable<String> task = new Callable<String>(){ public String call(){ int sleepTime = new Random().nextInt(1000); Thread.sleep(sleepTime); return "线程"+i+"睡了"+sleepTime+"秒"; } }; executorService.submit( task ); // 将task添加进任务队列 tasks.add( task ); } // 获取10个任务的返回结果 List<Future<String>> results = executorService.invokeAll( tasks ); // 输出结果 for ( int i=0; i<10; i++ ) { // 获取包含返回结果的future对象 Future<String> future = results.get(i); // 从future中取出执行结果(若尚未返回结果,则get方法被阻塞,直到结果被返回为止) String result = future.get(); System.out.println(result); } 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 方法三:使用CompletionService CompletionService内部维护了一个阻塞队列,只有执行完成的任务结果才会被放入该队列,这样就确保执行时间较短的任务率先被存入阻塞队列中。 ExecutorService exec = Executors.newFixedThreadPool(10); final BlockingQueue<Future<Integer>> queue = new LinkedBlockingDeque<Future<Integer>>( 10); //实例化CompletionService final CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>( exec, queue); // 提交10个任务 for ( int i=0; i<10; i++ ) { executorService.submit( new Callable<String>(){ public String call(){ int sleepTime = new Random().nextInt(1000); Thread.sleep(sleepTime); return "线程"+i+"睡了"+sleepTime+"秒"; } } ); } // 输出结果 for ( int i=0; i<10; i++ ) { // 获取包含返回结果的future对象(若整个阻塞队列中还没有一条线程返回结果,那么调用take将会被阻塞,当然你可以调用poll,不会被阻塞,若没有结果会返回null,poll和take返回正确的结果后会将该结果从队列中删除) Future<String> future = completionService.take(); // 从future中取出执行结果,这里存储的future已经拥有执行结果,get不会被阻塞 String result = future.get(); System.out.println(result); }

优秀的个人博客,低调大师

java面试-Java并发编程(八)——闭锁、同步屏障、信号量详解

1. 闭锁:CountDownLatch 1.1 使用场景 若有多条线程,其中一条线程需要等到其他所有线程准备完所需的资源后才能运行,这样的情况可以使用闭锁。 1.2 代码实现 // 初始化闭锁,并设置资源个数 CountDownLatch latch = new CountDownLatch(2); Thread t1 = new Thread( new Runnable(){ public void run(){ // 加载资源1 加载资源的代码…… // 本资源加载完后,闭锁-1 latch.countDown(); } } ).start(); Thread t2 = new Thread( new Runnable(){ public void run(){ // 加载资源2 资源加载代码…… // 本资源加载完后,闭锁-1 latch.countDown(); } } ).start(); Thread t3 = new Thread( new Runnable(){ public void run(){ // 本线程必须等待所有资源加载完后才能执行 latch.await(); // 当闭锁数量为0时,await返回,执行接下来的任务 任务代码…… } } ).start(); 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 2. 同步屏障:CyclicBarrier 2.1 使用场景 若有多条线程,他们到达屏障时将会被阻塞,只有当所有线程都到达屏障时才能打开屏障,所有线程同时执行,若有这样的需求可以使用同步屏障。此外,当屏障打开的同时还能指定执行的任务。 2.2 闭锁 与 同步屏障 的区别 闭锁只会阻塞一条线程,目的是为了让该条任务线程满足条件后执行; 而同步屏障会阻塞所有线程,目的是为了让所有线程同时执行(实际上并不会同时执行,而是尽量把线程启动的时间间隔降为最少)。 2.3 代码实现 // 创建同步屏障对象,并制定需要等待的线程个数 和 打开屏障时需要执行的任务 CyclicBarrier barrier = new CyclicBarrier(3,new Runnable(){ public void run(){ //当所有线程准备完毕后触发此任务 } }); // 启动三条线程 for( int i=0; i<3; i++ ){ new Thread( new Runnable(){ public void run(){ // 等待,(每执行一次barrier.await,同步屏障数量-1,直到为0时,打开屏障) barrier.await(); // 任务 任务代码…… } } ).start(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 3. 信号量:Semaphore 3.1 使用场景 若有m个资源,但有n条线程(n>m),因此同一时刻只能允许m条线程访问资源,此时可以使用Semaphore控制访问该资源的线程数量。 3.2 代码实现 // 创建信号量对象,并给予3个资源 Semaphore semaphore = new Semaphore(3); // 开启10条线程 for ( int i=0; i<10; i++ ) { new Thread( new Runnbale(){ public void run(){ // 获取资源,若此时资源被用光,则阻塞,直到有线程归还资源 semaphore.acquire(); // 任务代码 …… // 释放资源 semaphore.release(); } } ).start(); }

优秀的个人博客,低调大师

库库插值是良方!(高频流数据版)

最近写了不少程序,记录一个软件上面的设计,目前是将解析好的数据通过 USB发送给上位机,其中数据打包的思路是把结构化的多通道数据转换为一维字节流,并加入“帧头和帧尾”以确保上位机能够准确断帧和对齐频道。 ## **固定帧长数据包(Packit)** 现在的数据属于连续的高频流数据,如果直接发送 ASCII 字符串(如使用 `printf`),会因为字符转换效率低、数据量翻倍而导致卡顿。因此,**必须发送原始二进制字节(Binary Data)**。 现在设计的数据包(共 38 个字节): |字节偏移|字段名称|数据类型|说明| | --- | --- | --- | --- | |0 ~ 1|帧头 (Header)|uint8_t|固定为 0xAA, 0x55(用于上位机对齐查找包头)| |2|包计数器 (Counter)|uint8_t|0 ~ 255 循环,用于上位机检测是否丢包| |3 ~ 34|通道数据 (Payload)|int32_t × 8|8 个通道的 32 位脑电数据(8 × 4 = 32 字节)| |35|校验和 (Checksum)|uint8_t|前 35 字节的累加和,用于检测数据传输错误| |36 ~ 37|帧尾 (Tail)|uint8_t|固定为 0x0D, 0x0A (\r\n,符合多数串口软件换行习惯)| 利用 C 语言结构体的内存连续性,可以直接打包: ``` // 强制编译器不进行内存对齐优化,确保结构体大小严格等于 38 字节 #pragma pack(1) typedef struct { uint8_t header[2]; // 帧头 0xAA, 0x55 uint8_t packet_cnt; // 包计数器 int32_t channels[8]; // 8通道数据 (32字节) uint8_t checksum; // 校验和 uint8_t tail[2]; // 帧尾 0x0D, 0x0A } EEG_Packet_t; #pragma pack() // 全局变量 EEG_Packet_t TxPacket; uint8_t global_packet_counter = 0; ``` 这段是数据的容器,要装在这里,接下来看数据的装载: ``` void Pack_And_Send_EEG(int32_t *parsed_channels) { // 装载帧头和帧尾 TxPacket.header[0] = 0xAA; TxPacket.header[1] = 0x55; TxPacket.tail[0] = 0x0D; TxPacket.tail[1] = 0x0A; // 装载计数器并自增 TxPacket.packet_cnt = global_packet_counter++; // 复制解析好的 8 通道 int32_t 数据 (共 32 字节) memcpy(TxPacket.channels, parsed_channels, sizeof(TxPacket.channels)); // 计算校验和 (从 header 到 channels 的所有字节累加) uint8_t sum = 0; uint8_t *ptr = (uint8_t *)&TxPacket; for(int i = 0; i 丢包补偿只能恢复时间位置,不能恢复真实信号,插值数据必须始终带有“无效/估计”标记,不能伪装成真实采样值。 ## **包计数器** ![](https://developer.qcloudimg.com/operation/attachment/12085877/20260722-40cbe91f.png) ### **跨越255时仍然成立** 例如上一包为254,当前包为1: 因此丢失2包,即255和0。 Python中可以写成: ``` distance = (current_seq - previous_seq) & 0xFF lost_packets = distance - 1 ``` 这里的 `& 0xFF` 相当于对256取模。 上面写的是: > 每当单片机成功组装并发送完一个数据包,计数器加1。 这句话不够严谨,更严谨的说法是: > 每次采集系统生成一个应当发送的数据包时,就为它分配唯一的序号。 例如: ``` packet.seq = packet_counter++; enqueue_tx_packet(&packet); ``` 如果 MCU 内部发生以下问题:ADC采集完成,但发送队列已经满了;DMA发送任务来不及处理;某个数据包在 MCU 内部被覆盖;BLE发送接口返回忙,程序直接放弃该包;如果计数器只在“实际发送成功”后递增,上位机看到的序号仍然连续,反而无法发现 MCU 内部已经丢失过采样数据。 ### **8位序号存在歧义** 8位序号最多只能表示256种状态。如果一次断开时间太长,丢失超过255包,上位机无法知道究竟循环了多少次。 > 例如上一包为5,当前包仍为5,可能表示:收到重复包;恰好丢失了255包;设备重新启动;丢失了511包,有条件使用最低16位序号;更推荐32位 `sample_index`。 32位计数器即使采样率为1 kSPS,也可以连续运行约: 天 如果每个数据包包含多个采样点,持续时间还会更长。 ## **丢包为什么会影响时间轴** 假设采样率为: 采样周期: 真实时间应该由采样序号决定: 假设采样序列是: ``` 100、101、102、105、106 ``` 说明103、104缺失;如果绘图程序只是把收到的数据依次放在相邻位置: ``` 显示位置:0、1、2、3、4 真实位置:100、101、102、105、106 ``` 那么时间轴被压缩了8 ms。后续所有数据都会向前错位,因此真正需要恢复的是时间位置: ``` 100、101、102、缺失、缺失、105、106 ``` 这里不一定必须填写一个数值,但必须保留两个缺失位置。 ## **对FFT影响的一点讨论** ### **情况一:丢失位置被删除** 如果丢失采样点直接从序列里消失,后面的时间轴被压缩。此时数据已经不再是严格均匀采样: 而普通FFT默认: 因此直接进行FFT,频率和相位分析都会产生误差。 ### **情况二:缺失位置补0** 补0可以恢复样本数量和时间位置,但在信号中引入了突然下降到0的边沿。这会产生宽带频谱泄漏,并激励IIR滤波器的瞬态响应。 ### **情况三:前值保持** 前值保持通常不会产生立刻跳到0的巨大阶跃,但会形成一段水平信号,并在真实数据恢复时产生跳变。 ### **情况四:线性插值** 短缺口下通常比较平滑,但会人为生成一段近似直线,相当于局部低通处理,高频成分会被压低。 ## **补偿方法** 既然可以找到哪里缺了数据,那就可以补回来,这里讨论三个方案 ### **前值保持** 公式为: 适合实时显示;缺口极短;信号变化相对缓慢;不能等待后一个样本到达;缺点是会产生平台;恢复时可能产生阶跃;高频信号误差较大;不能作为真实测量数据。 ### **线性插值** 若两端真实值为 和 ,中间缺失 个点,则第 个插值为: 例如缺失两个点: 则插值分别为: 适合:短缺口;平滑显示;低频趋势恢复;离线处理或允许等待下一包的实时显示;但插值值是估算,不是真实测量;这个缺口越长,可信度越低;肌电等高频、随机性较强的信号不适合长区间插值;可能人为减少功率谱中的高频能量;可能制造不存在的斜坡,插值结果最好保留浮点数,避免额外量化,只有在最后输出到整数格式时才转换。 ### **NaN加质量标记** 这是保存和科研分析中最可靠的基础方法: ``` data[missing_position] = np.nan valid[missing_position] = False ``` 明确: > 这里没有测量数据。 有了 NaN,不能简单把NaN直接送进IIR滤波器,如果把 NaN 直接送入普通 IIR 滤波器: ``` y = filter.process(np.nan) ``` 由于IIR滤波器有反馈状态,NaN可能进入内部状态,之后所有输出都变成NaN。 IIR滤波器一般形式: 一旦历史项中出现NaN,后续计算会持续受到污染。 ## **可以采用分级补偿策略** 不应该对所有长度的缺口都使用同一种插值,假设缺失时间为: 可以根据任务设置策略,如: |缺口|实时显示|原始保存|频谱/特征分析| | --- | --- | --- | --- | |1~2点|线性插值|NaN+无效标记|可酌情使用,但保留标记| |3~10点|插值但标色|NaN+无效标记|通常排除该区域| |较长缺口|显示断线|NaN+无效标记|分段分析| |连接中断|新建数据段|记录事件|不跨段滤波/FFT| ## **详细模拟** 我们使用Python 模拟插值,以及研究缺失数据在频域会有什么影响,展示的时候使用分辨率高的 FFT。 ![](https://developer.qcloudimg.com/http-save/yehe-6915208/3413f3d0d38d126ad6c7ba5cb1b19817.png) ### **仿真设置** ``` 采样点数 : 92,160 缺失样本数/比例 : 792 / 0.8594% 记录长度理论频率间隔 : 1/T = 0.005556 Hz Hann 窗 ENBW : 1.5/T = 0.008333 Hz 零填充 FFT 栅格间距 : fs/NFFT = 0.000488 Hz 注意:更细的 FFT 栅格不是更强的真实分辨能力;真实分辨率主要靠延长 T。 10 Hz附近主要谱峰: Reference : [(10.123046875, -0.005393100133749951), (10.1728515625, -5.18754105913096), (10.10986328125, -31.439126135762386), (10.13623046875, -31.494161173770728)] Zero fill : [(10.123046875, -0.06699910014441023), (10.1728515625, -5.216832638381734), (10.10986328125, -31.412882691863118), (10.13623046875, -32.08808645213892)] ZOH : [(10.123046875, -0.03559907459472774), (10.1728515625, -5.205575543568598), (10.10986328125, -31.199437020777808), (10.13623046875, -31.503423386907578)] Linear : [(10.123046875, -0.03330143431458907), (10.1728515625, -5.193685957757536), (10.10986328125, -31.188468711646827), (10.13623046875, -31.69564251215458)] Deleted : [(10.126953125, -5.586233149229062), (10.18359375, -6.546209263501059), (10.154296875, -14.058907252136022), (10.24755859375, -14.16637288270421)] 采样点数 : 92,160 缺失样本数/比例 : 792 / 0.8594% 记录长度理论频率间隔 : 1/T = 0.005556 Hz Hann 窗 ENBW : 1.5/T = 0.008333 Hz 零填充 FFT 栅格间距 : fs/NFFT = 0.000488 Hz 注意:更细的 FFT 栅格不是更强的真实分辨能力;真实分辨率主要靠延长 T。 10 Hz附近主要谱峰: Reference : [(10.123046875, -0.005393100133749951), (10.1728515625, -5.18754105913096), (10.10986328125, -31.439126135762386), (10.13623046875, -31.494161173770728)] Zero fill : [(10.123046875, -0.06699910014441023), (10.1728515625, -5.216832638381734), (10.10986328125, -31.412882691863118), (10.13623046875, -32.08808645213892)] ZOH : [(10.123046875, -0.03559907459472774), (10.1728515625, -5.205575543568598), (10.10986328125, -31.199437020777808), (10.13623046875, -31.503423386907578)] Linear : [(10.123046875, -0.03330143431458907), (10.1728515625, -5.193685957757536), (10.10986328125, -31.188468711646827), (10.13623046875, -31.69564251215458)] Deleted : [(10.126953125, -5.586233149229062), (10.18359375, -6.546209263501059), (10.154296875, -14.058907252136022), (10.24755859375, -14.16637288270421)] ``` 采用: 记录时间: 实际采样点数: 信号包含: 其中前两个信号只相差: > 丢包按照每包8个采样点产生,包括:随机整包丢失;连续3包丢失;连续10包丢失;连续25包丢失。 最终缺失: 点 丢失率: > 比较五种情况:完整参考数据;缺失位置补0;前值保持;线性插值;直接删除缺失点。 ![](https://developer.qcloudimg.com/http-save/yehe-6915208/e531131c376d8bb480a9c4743a028dc8.png) ### **“高分辨率”FFT** 程序使用: 因此FFT频率栅格间距为: > 这个数很小,但它主要来自零填充,只代表频谱曲线计算得更密。 真正由采集时间决定的基本频率间隔为: 使用Hann窗后,其等效噪声带宽约为: 因此: > `0.000488 Hz`:频谱显示栅格; `0.00556 Hz`:记录长度对应的基本分辨尺度; `0.00833 Hz`:Hann窗的ENBW。 不能把大点数零填充误认为真正提高了频率分辨率。真正提高分辨率,主要依靠延长采集时间。 ![](https://developer.qcloudimg.com/http-save/yehe-6915208/77ec67b9f915d0ece229a11e15fee992.png) ## **两个近邻单音能否分开?** 完整信号检测到: |设置信号|FFT检测谱峰| | --- | --- | |10.123 Hz|10.12305 Hz| |10.173 Hz|10.17285 Hz| 频率误差只有FFT栅格量级,两个相隔0.05 Hz的信号能够清楚分开,这说明180秒记录足以分辨它们,因为: 如果要分辨相隔0.001 Hz的两个信号,仅仅把FFT改成几百万点不够。记录时间至少应达到约: 实际考虑窗函数,通常还需要更长。 ## **缺失数据为什么产生频谱泄漏?** 把完整信号写成 ,有效性掩码写成 : 数据有效数据缺失 补零后的信号实际是: 时域相乘对应频域卷积: 如果没有丢包, 恒等于1,它的频谱主要集中在直流位置,不会明显扩散原信号频谱。 > 发生丢包后,掩码中出现很多缺口, 会包含宽带频率成分。原本集中的单音谱线与它卷积后,会出现:主谱峰幅度下降;谱峰附近出现裙边;宽带噪声底升高;产生不存在的伪频率分量;连续突发缺口产生类似 sinc 的频谱结构。 因此,即使丢失比例不到1%,高动态范围频谱也能清楚看到影响。 ### **各种处理方法的时域误差** 缺失区域RMSE结果: |方法|缺失区域RMSE|全记录RMSE| | --- | --- | --- | |补零|0.8172|0.07576| |前值保持|0.7470|0.06925| |线性插值|0.5280|0.04895| 在本次信号条件下,线性插值最好,因为它利用了缺口前后的两个真实值。 但这不意味着线性插值能恢复真实高频信号。它只是在两个端点之间构造直线: 其中 为缺失点数;缺口越长,真实信号越可能在缺口内发生多次振荡,直线插值与真实值的差别就越大。 ### **补零的频域影响** 补零相当于直接使用缺失掩码: 主要问题是:信号突然跳到0;缺口结束时又突然跳回真实值;两个边沿产生宽带频谱;单音幅度被略微压低;噪声底升高。 本次仿真中,10.123 Hz幅度从: 下降到: 大约下降: 虽然主谱峰变化不大,但在参考信号较弱、动态范围较高时,新增的宽带泄漏会遮盖微弱分量。 ## **前值保持的频域影响** 前值保持为: 它不会像补零一样立即跳到0,但会形成一段水平线;缺口结束时,保持值与当前真实值之间仍可能产生阶跃。对低频或缓慢变化信号比较适合;能用于实时处理,因为不需要等待未来数据;对高频信号产生幅度和相位误差;长缺口会形成明显平台;缺口恢复边沿会带来高频泄漏。 > 仿真中,前值保持比补零的时域RMSE稍低,但频谱误差仍很明显。 ### **线性插值的频域影响** 线性插值比补零和前值保持平滑,因此通常减少突变导致的宽带泄漏,仿真中,10.123 Hz幅度变为: 相对完整参考值的误差约: 10.173 Hz幅度几乎保持不变: 但是线性插值本质上具有低通倾向:缺失区被替换成直线;高频细节被消除;高频功率通常被低估;相位信息不再真实;长缺口可能生成不存在的慢斜坡。线性插值适合:实时波形显示;极短缺口;低频趋势观察;不要求缺失区真实频域特征的场景。 ### **直接删除最危险** 原始数据长度为: 删除缺失点后变为: 如果仍按512 Hz采样率解释,程序会认为记录长度为: 但真实过程持续了180秒。 相当于把180秒发生的信号压缩到了178.45秒,平均时间压缩比例为: 所以频率会整体向上偏移。仿真中10.123 Hz附近的主峰移动到约: 同时出现:10.1543 Hz;10.1836 Hz;10.2476 Hz;等多个伪峰或严重变形的峰。 ![](https://developer.qcloudimg.com/http-save/yehe-6915208/27bbb2c11c8c36e17741788add5cbccc.png) 这里没有严格按照平均压缩比例整体平移,是因为缺失点不是均匀删除,而是在不同时间发生随机和突发删除,所以时间轴是分段跳变,而不是均匀缩放。 结论是: > 绝对不要删除缺失点后,再假定剩余样本仍然是等间隔采样。 ## **小结** 上位机算力大,可以任意插值,另外高点数只能让FFT失真看得更细,但不能把插值数据变回真实测量数据。

资源下载

更多资源
Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册