精华内容
下载资源
问答
  • 其实这是一道Java笔试题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志要求:程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志...

    其实这是一道Java笔试题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志

    要求:程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象

    上述要求,主要是考察应聘者对队列和多线程两方面的综合理解和运用!

    实现如下:import java.util.concurrent.ArrayBlockingQueue;

    /**

    * 现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志

    *

    * 要求实现功能:

    * 请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象

    *

    * @author thinker

    */

    public class MultiThreadPrintLogDemo

    {

    private static boolean bFlag;

    private static long t1, t2;

    // 线程安全的日志队列【Add】

    private static ArrayBlockingQueue logQueue = new ArrayBlockingQueue(16);

    public static void main(String[] args)

    {

    // 四个线程处理16个日志对象【Add】

    t1 = System.currentTimeMillis();

    for (int i = 0; i 

    {

    // 匿名线程类

    new Thread(new Runnable()

    {

    @Override

    public void run()

    {

    while (!bFlag)

    {

    try

    {

    parseLog(logQueue.take());

    }

    catch (InterruptedException e)

    {

    e.printStackTrace();

    }

    }

    }

    }).start();

    }

    /**

    * 模拟处理16行日志,下面的代码产生了16个日志对象,当前代码需要运行16秒才能打印完这些日志。

    */

    for (int k = 0; k 

    {

    String log = String.valueOf(k);

    //parseLog(log);【Mod】

    // 【Add】

    try

    {

    logQueue.put(log);

    if (k % 4 == 0)

    {

    System.out.println("成功添加4个日志对象到日志队列!");

    }

    }

    catch (InterruptedException e)

    {

    e.printStackTrace();

    }

    }

    }

    public static void parseLog(String log)

    {

    try

    {

    Thread.sleep(1000);

    System.out.println("打印日志对象...");

    t2 = System.currentTimeMillis();

    {

    int t3 = (int)(t2 - t1) / 1000;

    System.out.println("打印日志对象截止当前共花费时间(秒):" + t3);

    if (t3 >= 4)

    {

    bFlag = true;

    }

    }

    }

    catch (InterruptedException e)

    {

    e.printStackTrace();

    }

    }

    }

    程序的运行结果:

    成功添加4个日志对象到日志队列!

    成功添加4个日志对象到日志队列!

    成功添加4个日志对象到日志队列!

    成功添加4个日志对象到日志队列!

    打印日志对象...

    打印日志对象...

    打印日志对象...

    打印日志对象...

    打印日志对象截止当前共花费时间(秒):1

    打印日志对象截止当前共花费时间(秒):1

    打印日志对象截止当前共花费时间(秒):1

    打印日志对象截止当前共花费时间(秒):1

    打印日志对象...

    打印日志对象...

    打印日志对象...

    打印日志对象...

    打印日志对象截止当前共花费时间(秒):2

    打印日志对象截止当前共花费时间(秒):2

    打印日志对象截止当前共花费时间(秒):2

    打印日志对象截止当前共花费时间(秒):2

    打印日志对象...

    打印日志对象...

    打印日志对象截止当前共花费时间(秒):3

    打印日志对象...

    打印日志对象...

    打印日志对象截止当前共花费时间(秒):3

    打印日志对象截止当前共花费时间(秒):3

    打印日志对象截止当前共花费时间(秒):3

    打印日志对象...

    打印日志对象...

    打印日志对象截止当前共花费时间(秒):4

    打印日志对象...

    打印日志对象...

    打印日志对象截止当前共花费时间(秒):4

    打印日志对象截止当前共花费时间(秒):4

    打印日志对象截止当前共花费时间(秒):4

    展开全文
  • 最近项目中一些异步执行的逻辑没有运行异常却没有打出日志 给定位问题带来麻烦??问题分析接下来我们来看一下java中的线程池是如何运行我们提交的任务的,详细流程比较复杂,这里我们不关注,我们只关注任务执行的...

    最近项目中一些异步执行的逻辑没有运行异常却没有打出日志 给定位问题带来麻烦??

    问题分析

    接下来我们来看一下java中的线程池是如何运行我们提交的任务的,详细流程比较复杂,这里我们不关注,我们只关注任务执行的部分。java中的线程池用的是ThreadPoolExecutor,真正执行代码的部分是runWorker方法:final void runWorker(Worker w)

    //省略无关部分

    try{

    beforeExecute(wt,task);

    Throwablethrown=null;

    try{

    task.run();//执行程序逻辑

    }catch(RuntimeExceptionx){//捕获RuntimeException

    thrown=x;throwx;

    }catch(Errorx){//捕获Error

    thrown=x;throwx;

    }catch(Throwablex){//捕获Throwable

    thrown=x;thrownewError(x);

    }finally{

    afterExecute(task,thrown);//运行完成,进行后续处理

    }

    }finally{

    task=null;

    w.completedTasks++;

    w.unlock();

    }

    //省略无关部分

    可以看到,程序会捕获包括Error在内的所有异常,并且在程序最后,将出现过的异常和当前任务传递给afterExecute方法。

    而ThreadPoolExecutor中的afterExecute方法是没有任何实现的:

    protectedvoidafterExecute(Runnabler,Throwablet){}

    也就是说,默认情况下,线程池会捕获任务抛出的所有异常,但是不做任何处理。

    存在问题

    想象下ThreadPoolExecutor这种处理方式会有什么问题?  这样做能够保证我们提交的任务抛出了异常不会影响其他任务的执行,同时也不会对用来执行该任务的线程产生任何影响。  问题就在afterExecute方法上,这个方法没有做任何处理,所以如果我们的任务抛出了异常,我们也无法立刻感知到。即使感知到了,也无法查看异常信息。

    所以,作为一名好的开发者,是不应该允许这种情况出现的。

    如何避免这种问题

    思路很简单。  1. 在提交的任务中将异常捕获并处理,不抛给线程池。  2. 异常抛给线程池,但是我们要及时处理抛出的异常。

    第一种思路很简单,就是我们提交任务的时候,将所有可能的异常都Catch住,并且自己处理,任务的大致代码如下:

    @Override

    publicvoidrun(){

    try{

    //处理所有的业务逻辑

    }catch(Throwablee){

    //打印日志等

    }finally{

    //其他处理

    }

    }

    说白了就是把业务逻辑都trycatch起来。  但是这种思路的缺点就是:1)所有的不同任务类型都要trycatch,增加了代码量。2)不存在checkedexception的地方也需要都trycatch起来,代码丑陋。

    第二种思路就可以避免上面的两个问题。  第二种思路又有以下几种实现方式:  1. 自定义线程池,继承ThreadPoolExecutor并复写其afterExecute(Runnable r, Throwable t)方法。  2. 实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e);方法,并将该handler传递给线程池的ThreadFactory  3. 采用Future模式,将返回结果以及异常放到Future中,在Future中处理  4. 继承ThreadGroup,覆盖其uncaughtException方法。(与第二种方式类似,因为ThreadGroup类本身就实现了Thread.UncaughtExceptionHandler接口)

    下面是以上几种方式的代码

    方式1

    自定义线程池:

    finalclassPoolService{

    // The values have been hard-coded for brevity

    ExecutorServicepool=newCustomThreadPoolExecutor(

    10,10,10,TimeUnit.SECONDS,newArrayBlockingQueue(10));

    // ...

    }

    classCustomThreadPoolExecutorextendsThreadPoolExecutor{

    // ... Constructor ...

    publicCustomThreadPoolExecutor(

    intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,

    TimeUnitunit,BlockingQueueworkQueue){

    super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);

    }

    @Override

    publicvoidafterExecute(Runnabler,Throwablet){

    super.afterExecute(r,t);

    if(t!=null){

    // Exception occurred, forward to handler

    }

    // ... Perform task-specific cleanup actions

    }

    @Override

    publicvoidterminated(){

    super.terminated();

    // ... Perform final clean-up actions

    }

    }

    方式2

    实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e);方法,并将该handler传递给线程池的ThreadFactory

    finalclassPoolService{

    privatestaticfinalThreadFactoryfactory=

    newExceptionThreadFactory(newMyExceptionHandler());

    privatestaticfinalExecutorServicepool=

    Executors.newFixedThreadPool(10,factory);

    publicvoiddoSomething(){

    pool.execute(newTask());// Task is a runnable class

    }

    publicstaticclassExceptionThreadFactoryimplementsThreadFactory{

    privatestaticfinalThreadFactorydefaultFactory=

    Executors.defaultThreadFactory();

    privatefinalThread.UncaughtExceptionHandlerhandler;

    publicExceptionThreadFactory(

    Thread.UncaughtExceptionHandlerhandler)

    {

    this.handler=handler;

    }

    @OverridepublicThreadnewThread(Runnablerun){

    Threadthread=defaultFactory.newThread(run);

    thread.setUncaughtExceptionHandler(handler);

    returnthread;

    }

    }

    publicstaticclassMyExceptionHandlerextendsExceptionReporter

    implementsThread.UncaughtExceptionHandler{

    // ...

    @OverridepublicvoiduncaughtException(Threadthread,Throwablet){

    // Recovery or logging code

    }

    }

    }

    方式3

    继承ThreadGroup,覆盖其uncaughtException方法

    publicclassThreadGroupExample{

    publicstaticclassMyThreadGroupextendsThreadGroup{

    publicMyThreadGroup(Strings){

    super(s);

    }

    publicvoiduncaughtException(Threadthread,Throwablethrowable){

    System.out.println("Thread "+thread.getName()

    +" died, exception was: ");

    throwable.printStackTrace();

    }

    }

    publicstaticThreadGroupworkerThreads=

    newMyThreadGroup("Worker Threads");

    publicstaticclassWorkerThreadextendsThread{

    publicWorkerThread(Strings){

    super(workerThreads,s);

    }

    publicvoidrun(){

    thrownewRuntimeException();

    }

    }

    publicstaticvoidmain(String[]args){

    Threadt=newWorkerThread("Worker Thread");

    t.start();

    }

    }

    确实这种方式与上面通过ThreadFactory来指定UncaughtExceptionHandler是一样的,只是代码逻辑不同,但原理上都是一样的,即给线程池中的每个线程都指定一个UncaughtExceptionHandler。

    ** 注意:上面三种方式针对的都是通过execute(xx)的方式提交任务,如果你提交任务用的是submit()方法,那么上面的三种方式都将不起作用,而应该使用下面的方式 **

    方式4

    如果提交任务的时候使用的方法是submit,那么该方法将返回一个Future对象,所有的异常以及处理结果都可以通过future对象获取。  采用Future模式,将返回结果以及异常放到Future中,在Future中处理

    finalclassPoolService{

    privatefinalExecutorServicepool=Executors.newFixedThreadPool(10);

    publicvoiddoSomething(){

    Future>future=pool.submit(newTask());

    // ...

    try{

    future.get();

    }catch(InterruptedExceptione){

    Thread.currentThread().interrupt();// Reset interrupted status

    }catch(ExecutionExceptione){

    Throwableexception=e.getCause();

    // Forward to exception reporter

    }

    }

    }

    总结

    java线程池会捕获任务抛出的异常和错误,但不做任何处理

    好的程序设计应该考虑到对于类异常的处理

    处理线程池中的异常有两种思路:  1)提交到线程池中的任务自己捕获异常并处理,不抛给线程池  2)由线程池统一处理

    对于execute方法提交的线程,有两种处理方式  1)自定义线程池并实现afterExecute方法  2)给线程池中的每个线程指定一个UncaughtExceptionHandler,由handler来统一处理异常。

    对于submit方法提交的任务,异常处理是通过返回的Future对象进行的。

    展开全文
  • log4j2 异步多线程打印日志Maven依赖org.apache.logging.log4jlog4j-1.2-api2.3org.apache.logging.log4jlog4j-api2.3org.apache.logging.log4jlog4j-core2.3...

    log4j2 异步多线程打印日志

    Maven依赖

    org.apache.logging.log4j

    log4j-1.2-api

    2.3

    org.apache.logging.log4j

    log4j-api

    2.3

    org.apache.logging.log4j

    log4j-core

    2.3

    com.lmax

    disruptor

    3.3.4

    log4j2.xml

    packages="com.hoperun.zhulongxiang.asnc_print_different_logfile">

    fileName="log/${thread:threadName}.log"

    filePattern="log/${thread:threadName}-%d{MM-dd-yyyy}-%i.log">

    核心java类

    package com.hoperun.zhulongxiang.asnc_print_different_logfile;

    import org.apache.logging.log4j.core.LogEvent;

    import org.apache.logging.log4j.core.config.plugins.Plugin;

    import org.apache.logging.log4j.core.lookup.StrLookup;

    @Plugin(name = "thread", category = StrLookup.CATEGORY)

    public class ThreadLookup implements StrLookup {

    public String lookup(String key) {

    return Thread.currentThread().getName();

    }

    public String lookup(LogEvent event, String key) {

    return event.getThreadName() == null ? Thread.currentThread().getName() : event.getThreadName();

    }

    }

    这里@Plugin中的name的值对应log4j2.xml中配置中的thread

    准备两个线程类

    package com.hoperun.zhulongxiang.asnc_print_different_logfile;

    import org.apache.logging.log4j.LogManager;

    import org.apache.logging.log4j.Logger;

    public class LogThread implements Runnable {

    private static final Logger LOGGER = LogManager.getLogger(LogThread.class);

    public String threadName;

    public LogThread(String threadName) {

    this.threadName = threadName;

    }

    public void run() {

    Thread.currentThread().setName(threadName);

    LOGGER.info("1");

    }

    }

    package com.hoperun.zhulongxiang.asnc_print_different_logfile;

    import org.apache.logging.log4j.LogManager;

    import org.apache.logging.log4j.Logger;

    public class OtherLogThread implements Runnable {

    private static final Logger LOGGER = LogManager.getLogger(LogThread.class);

    public String threadName;

    public OtherLogThread(String threadName) {

    this.threadName = threadName;

    }

    public void run() {

    Thread.currentThread().setName(threadName);

    LOGGER.info("2");

    }

    }

    测试

    package com.hoperun.zhulongxiang.asnc_print_different_logfile;

    import java.util.concurrent.ExecutorService;

    import java.util.concurrent.Executors;

    import org.apache.logging.log4j.LogManager;

    import org.apache.logging.log4j.Logger;

    import org.apache.logging.log4j.ThreadContext;

    import org.nutz.ioc.Ioc;

    import com.hoperun.base.IocMaster;

    public class LogApp {

    static {

    System.setProperty("log4j.configurationFile", "etc/log4j2.xml");

    }

    private static final Logger LOGGER = LogManager.getLogger(LogApp.class);

    public static void main(String[] args) {

    ThreadContext.put("threadName", Thread.currentThread().getName());

    LogThread t1 = new LogThread("123123123");

    OtherLogThread t2 = new OtherLogThread("4564564564");

    ExecutorService pool = Executors.newFixedThreadPool(20);

    pool.submit(t1);

    pool.submit(t2);

    }

    }

    这里ThreadContext.put("threadName", Thread.currentThread().getName())设置的参数threadName对应log4j2.xml中配置中的threadName

    日志

    09:44:39.704 [123123123] INFO com.hoperun.zhulongxiang.asnc_print_different_logfile.LogThread - 1

    09:44:39.704 [4564564564] INFO com.hoperun.zhulongxiang.asnc_print_different_logfile.LogThread - 2

    日志中的线程名已经修改成我们设定的名称。log下已经有了我们想要的以线程名命名的日志文件了。

    展开全文
  • 最近在做项目时遇到一个很头疼的问题,我扩展了org.apache.log4j.DailyRollingFileAppender,用来实现日志名自定义,但出现了个很诡异的问题:日志第一次打印打印一遍,第二次时同一...最近在做项目时遇到一个很...

    最近在做项目时遇到一个很头疼的问题,我扩展了org.apache.log4j.DailyRollingFileAppender,用来实现日志名自定义,但出现了个很诡异的问题:日志第一次打印时打印一遍,第二次时同一...

    最近在做项目时遇到一个很头疼的问题,我扩展了org.apache.log4j.DailyRollingFileAppender,用来实现日志名自定义,但出现了个很诡异的问题:日志第一次打印时打印一遍,第二次时同一行打印两遍,第三次三遍,……,第N次N遍。我已经设置了additivity="false

    下面是第三次时的日志:

    16:16:21,203 (BdTest.java:189) DEBUG pool-1-thread-1 [pool-1-thread-1] 开始打印!

    16:16:21,203 (BdTest.java:189) DEBUG pool-1-thread-1 [pool-1-thread-1] 开始打印!

    16:16:21,203 (BdTest.java:189) DEBUG pool-1-thread-1 [pool-1-thread-1] 开始打印!

    16:16:21,296 (BdTest.java:259) DEBUG pool-1-thread-1 [pool-1-thread-1] 测试打印!

    16:16:21,296 (BdTest.java:259) DEBUG pool-1-thread-1 [pool-1-thread-1] 测试打印!

    16:16:21,296 (BdTest.java:259) DEBUG pool-1-thread-1 [pool-1-thread-1] 测试打印!

    16:16:21,406 (BdTest.java:268) DEBUG pool-1-thread-1 [pool-1-thread-1] 结束打印!

    16:16:21,406 (BdTest.java:268) DEBUG pool-1-thread-1 [pool-1-thread-1] 结束打印!

    16:16:21,406 (BdTest.java:268) DEBUG pool-1-thread-1 [pool-1-thread-1] 结束打印!

    很怪异,不是开始打印-->测试打印-->结束打印 这种整个log的重复打印,而是每一行的重复打印,不知道各位有没有遇到过类似问题?或者能不能提供下解决思路? 大家一起提高下, 谢谢了

    展开

    展开全文
  • 一、前言最近刚刚结束转岗以来的第一次双11压测,收获颇,难言言表, 本文就先...二、日志打印模型同步日志模型如上图,个业务线程打印日志时候要等把内容写入磁盘后才会返回,所以打日志的rt就是写入磁盘的耗时...
  • log4j2 异步多线程打印日志Maven依赖org.apache.logging.log4jlog4j-1.2-api2.3org.apache.logging.log4jlog4j-api2.3org.apache.logging.log4jlog4j-core2.3...
  • 经常做线上问题排查的可能会有感受,由于日志打印一般是无序的,多线程下想要串行拿到一次请求中的相关日志简直是大海捞针。那么MDC是一种很好的解决办法。SLF4J的MDCSLF4J 提供了MDC ( Mapped Diagnostic Contexts ...
  • log4j2支持日志的异步打印日志异步输出的好处在于,使用单独的进程来执行日志打印的功能,可以提高日志执行效率,减少日志功能对正常业务的影响。异步日志在程序的classpath需要加载disruptor-3.0.0.jar或者更高的...
  • 题目描述:先来分析一下这个题目,关于这16条日志记录,我们可以在主线程中产生出来,这没用什么难度,关键是开启4个线程去执行,现在有两种思路:一种是日志的产生和打印日志线程在逻辑上分开;一种是日志的产生...
  • 背景多线程情况下,子线程的sl4j打印日志缺少traceId等信息,导致定位问题不方便解决方案打印日志时添加用户ID、trackId等信息,缺点是每个日志都要手动添加使用mdc直接拷贝父线程值实现// 新建线程时:Map ...
  • 线程名最有用的时候应该就是多线程的情况下了。许多日志框架都会记录当前方法调用所在线程的名字。不幸的是,一般看起来都是这样的:“http-nio-8080-exec-3″,这是线程池或者容器自动分配的线程名。我经常听到有...
  • 在我们编写程序的时候,日志是必不可少的,但是在并发较大或者涉及多线程的系统中,每个用户的请求所产生的日志交差打印,也会导致日志显示的混乱(如图1 普通日志),因此将每个日志标志为分类成一个用户访问所导致的...
  • 三道多线程练习题接下来,我们来做三道题:第一题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要...
  • 第一题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。...
  • 第一题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。...
  • Java 多线程操作

    2013-04-30 23:14:00
    第一题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。...
  • 线程名最有用的时候应该就是多线程的情况下了。许多日志框架都会记录当前方法调用所在线程的名字。不幸的是,一般看起来都是这样的:“http-nio-8080-exec-3″,这是线程池或者容器自动分配的线程名。我经常听到有...
  • 第一题:现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。...
  • JAVA多线程机试试题

    千次阅读 2013-07-19 20:45:09
    以下题目答案来自传智张孝祥老师多线程讲解视频。 题目一: 原题 package com.fei; /** * 现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志, * 请在程序中增加4个线程去...
  • Java 多线程 之常见练习题整理

    千次阅读 2019-05-24 21:14:36
    问题一:如何同时处理日志打印 现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印...
  • Java多线程的支持还是比较给力的,JDK1.6里面有现成的API可用,一般的多线程应用足够了,使用的时候注意最好加一层壳子,至少方便日志打印和后续扩展,以下是一个简单的启用的例子1. 定义线程池变量//线程池维护...
  • 中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印 玩这些日志对象。原始代码如下: package cn.edu.hpu.test; public class ReadTest { public static void main(String[]
  • Java日志打印

    千次阅读 2017-09-14 15:05:14
    前言:写项目时经常需要打印日志,便于记录数据,容易调试。相较于syso打印信息时,由于syso是硬编码,打印输出只能显示在控制台上,而且它是内部是线程同步,容易造成系统阻塞。而日志输出可以输出在控制台,也可以...
  • 一、前言最近刚刚结束转岗以来的第一次双11压测,收获颇,难言言表, 本文就先谈谈异步...二、日志打印模型同步日志模型image.png如上图,个业务线程打印日志时候要等把内容写入磁盘后才会返回,所以打日志的rt...
  • import java.util.concurrent.ArrayBlockingQueue;... *程序代码模拟产生16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程4s完成日志日志输入 * *public static void main(String[] args) {
  • Java多线程编程-使用JDK1.6的线程池

    千次阅读 2015-01-21 10:27:50
    Java多线程的支持还是比较给力的,JDK1.6里面有现成的API可用,一般的多线程应用足够了, 使用的时候注意最好加一层壳子,至少方便日志打印和后续扩展,以下是一个简单的启用的例子 1. 定义线程池变量 //线程池...
  • 1.两个消费者消费消息都到100了,但是下图中的日志打印出来 这个问题看代码public classConsumerObjectOne implementsRunnable {@Overridepublic voidrun() {while(true) {if(PudConThread.arrayBlockingQueue....

空空如也

空空如也

1 2 3 4 5 ... 10
收藏数 187
精华内容 74
关键字:

java多线程打印日志

java 订阅