Java應用線程泄漏緣由分析與避免


原由-日誌丟失java

生產上出現過幾回日誌丟失的問題,咱們日誌每小時生成一個文件,而後每一個小時剛到整點切換的時候會生成新文件而後正常輸出日誌,到了固定時點就空了,只有一個定時清理數據的線程打的幾行日誌。web

經過分析,是由於咱們的應用部署在weblogic上,每次從新發war包並不會重啓weblogic,只是中止以前的應用,從新啓動一個新的,而以前的應用有個別線程沒能關閉,與新應用同時打日誌,出現了問題。spring

泄漏的線程與新應用的線程各自持有一個log4j的appender,關鍵這兩個appender的規則徹底一致。apache

wKioL1jFbBqjUBcpAACto1TwXtk741.jpg

新應用的線程一直在打印日誌,到了整點就切換,而泄漏的線程每半個小時才被喚醒一次,而後打印幾句日誌。tomcat

咱們來看一下log4j切換日誌的代碼:服務器

 /**
     Rollover the current file to a new file.
  */
  void rollOver() throws IOException {

    /* Compute filename, but only if datePattern is specified */
    if (datePattern == null) {
      errorHandler.error("Missing DatePattern option in rollOver().");
      return;
    }

    String datedFilename = fileName+sdf.format(now);
    // It is too early to roll over because we are still within the
    // bounds of the current interval. Rollover will occur once the
    // next interval is reached.
    if (scheduledFilename.equals(datedFilename)) {
      return;
    }

    // close current file, and rename it to datedFilename
    this.closeFile();

    //!!!!!!!!!!重點在這!!!!
    //若是存在已經重名的就給刪掉。
    File target  = new File(scheduledFilename);
    if (target.exists()) {
      target.delete();
    }

    File file = new File(fileName);
    boolean result = file.renameTo(target);
    if(result) {
      LogLog.debug(fileName +" -> "+ scheduledFilename);
    } else {
      LogLog.error("Failed to rename ["+fileName+"] to ["+scheduledFilename+"].");
    }

    try {
      // This will also close the file. This is OK since multiple
      // close operations are safe.
      this.setFile(fileName, true, this.bufferedIO, this.bufferSize);
    }
    catch(IOException e) {
      errorHandler.error("setFile("+fileName+", true) call failed.");
    }
    scheduledFilename = datedFilename;

  }


假如如今剛到10點了,由於新應用一直在打印日誌,10點時切換了一個新日誌,而後不停的打日誌,結果到了10:15,另外一個appender也要打日誌了,它發現過了10點了,本身原來持有的日誌仍是9點點,就切換一個,發現已經有重名點,就刪掉重建了,這就是緣由。但是有人會說前一個appender持有的文件句柄文件被刪了,它不該該報異常嗎?通過個人實驗,沒有任何異常反應。app

public static void main(String[] args) throws IOException {
    File a = new File("test.txt");
    BufferedWriter bw1 = new BufferedWriter(
			new FileWriter(a));
    bw1.write("aaaaaaaa");
    bw1.flush();
    a.delete();
    bw1.write("aaaaaaaaa");
    bw1.flush();
	
    File b = new File("test.txt");
    BufferedWriter bw2 = new BufferedWriter(
			new FileWriter(b));
    bw2.write("bbbbbbbbb");
    bw2.flush();
	
    bw1.write("aaaaaaaaa");
    bw1.flush();
	
}

上面這段代碼不會有任何異常,最終生成的文件內容是bbbbbbbbb。jvm

這個問題只是線程泄漏帶來的問題之一,還有與之對應的內存泄漏等其它問題。接下來就來分析一下線程泄漏等緣由與如何避免此類問題。ide


應用服務器如何清理線程?函數


對於應用中本身起動的一些後臺線程,應用服務器通常不會給你停掉。不瞭解weblogic怎麼清理這些線程的,看了下tomcat的,tomcat默認並不會強制關閉這些線程。

先看tomcat中的一段警告日誌:

七月 27, 2016 7:02:10 下午 org.apache.catalina.loader.WebappClassLoaderBase clearReferencesThreads

警告: The web application [firefly] appears to have started a thread named [memkv-gc-thread-0] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:

 sun.misc.Unsafe.park(Native Method)

 java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)

 java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)

 java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1090)

 java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:807)

 java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)

 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)

 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)

 java.lang.Thread.run(Thread.java:745)


在tomcat中有關中止這些線程有一個配置默認是關的,若是開了,它是用stop方法,也是有風險的。

    /**
     * Should Tomcat attempt to terminate threads that have been started by the
     * web application? Stopping threads is performed via the deprecated (for
     * good reason) <code>Thread.stop()</code> method and is likely to result in
     * instability. As such, enabling this should be viewed as an option of last
     * resort in a development environment and is not recommended in a
     * production environment. If not specified, the default value of
     * <code>false</code> will be used.
     */
    private boolean clearReferencesStopThreads = false;

我猜weblogic也是相似的策略,因此不能期望應用服務器給你清理線程。


應該在什麼地方清理線程?


正確的中止應用線程的方法是本身去中止,而不要依賴於應用服務器!

例如,使用spring的,能夠利用bean的destroy方法,或者沒有spring的,註冊一個listener。

public class ContextDestroyListener 
		implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
	// TODO Auto-generated method stub		
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
	// TODO Auto-generated method stub
	//在這個地方清理線程
    }
}

咱們知道在什麼地方去清理這些線程了,接下來就是如何去清理他們了。清理線程並非加個shutdown方法或者調用一下interrupt那麼簡單的事情。


如何正確中止線程?


要本身中止線程,首先你得拿到線程的句柄,也就是thread對象或者線程池,若是你寫了下面的代碼,啓動完後你就找不到這個線程了,因此線程必定要在清理線程的時候能拿獲得句柄。

public static void main(String[] args) {
    new Thread(new Runnable() {
	@Override
	public void run() {
	    while(true) {
		try {
		    Thread.sleep(1000);    
		System.out.println("wake up");
		} catch (InterruptedException e) {
		e.printStackTrace();
		}
	    }
	}
	
    }).start();	
}

正確的方法是把Thread放到一個變量裏,例如t,而後經過t去中止這個線程。中止線程的方法通常有stop,destroy,interrupt等,可是stop,destroy都已經被廢棄了,由於可能形成死鎖,因此一般等作法是使用interrupt。使用interrupt其實相似於信號,就比如你在Linux進程中把SIGTERM信號忽略了,那就無法經過kill殺死進程了,interrupt也是如此。 下面等線程只會報一個異常,中斷信號被忽略。

public static void main(String[] args) {
    Thread t = new Thread(new Runnable() {
	@Override
	public void run() {
	    while(true) {
		try {
		    Thread.sleep(1000);
		    System.out.println("wake up");
	        } catch(InterruptedException e) {
		    e.printStackTrace();
	        }
	    }
	}
    });
    t.start();
    t.interrupt();//並不能中止線程
}

這種阻塞的線程,通常調用的函數都會強制檢查並拋出interruption異常,相似的還有wait(),阻塞隊列的take等,爲了程序能正常關閉,InterruptedException最好很差忽略。

public static void main(String[] args) {
	Thread t = new Thread(new Runnable() {
		@Override
		public void run() {
			while (true) {
				try {
					Thread.sleep(1000);
					System.out.println("wake up");
				} catch (InterruptedException e) {
					e.printStackTrace();
					System.out.println("stop...");
					break;
				}
			}
		}
	});
	t.start();
	t.interrupt();
}

若是run方法裏沒有拋出InterruptedException怎麼辦?例以下面這個

public static void main(String[] args) {
	Thread t = new Thread(new Runnable() {
		@Override
		public void run() {
			int i = 0;
			while (true) {
				i++;
			}
		}
	});
	t.start();
	t.interrupt();
}

這種狀況就須要run方法裏不斷檢查是否被中斷了,不然永遠停不下來。

public static void main(String[] args) {
	Thread t = new Thread(new Runnable() {
		@Override
		public void run() {
			int i = 0;
			while (true) {
				i++;
				if(Thread.interrupted()) {
					System.out.println("stop..");
					break;
				}
			}
		}
	});
	t.start();
	t.interrupt();
}


上面就是正確中止單個線程的方法,對於線程池,通常有兩個方法,shutdown和shutdownNow,這兩個方法差異是很大的,shutdown只是線程池再也不接受新的任務,可是不會打斷正在運行的線程,而shutdownNow會對逐個線程調用interrupt方法,若是你的線程是肯定能夠在一段時間跑完的,能夠用shutdown,可是若是是一個死循環,或者在sleep須要很長時間才從新喚醒,那就用shutdownNow,而後對於Runnable的實現也須要遵循上面單個線程的原則。

相關文章
相關標籤/搜索