簡(jiǎn)介
創(chuàng)新新互聯(lián),憑借十多年的網(wǎng)站建設(shè)、成都網(wǎng)站建設(shè)經(jīng)驗(yàn),本著真心·誠(chéng)心服務(wù)的企業(yè)理念服務(wù)于成都中小企業(yè)設(shè)計(jì)網(wǎng)站有近千家案例。做網(wǎng)站建設(shè),選創(chuàng)新互聯(lián)。
在實(shí)現(xiàn)定時(shí)調(diào)度功能的時(shí)候,我們往往會(huì)借助于第三方類庫(kù)來完成,比如: quartz 、 Spring Schedule 等等。JDK從1.3版本開始,就提供了基于 Timer 的定時(shí)調(diào)度功能。在 Timer 中,任務(wù)的執(zhí)行是串行的。這種特性在保證了線程安全的情況下,往往帶來了一些嚴(yán)重的副作用,比如任務(wù)間相互影響、任務(wù)執(zhí)行效率低下等問題。為了解決 Timer 的這些問題,JDK從1.5版本開始,提供了基于 ScheduledExecutorService 的定時(shí)調(diào)度功能。
本節(jié)我們主要分析 Timer 的功能。對(duì)于 ScheduledExecutorService 的功能,我們將新開一篇文章來講解。
如何使用
Timer 需要和 TimerTask 配合使用,才能完成調(diào)度功能。 Timer 表示調(diào)度器, TimerTask 表示調(diào)度器執(zhí)行的任務(wù)。任務(wù)的調(diào)度分為兩種:一次性調(diào)度和循環(huán)調(diào)度。下面,我們通過一些例子來了解他們是如何使用的。
1. 一次性調(diào)度
public static void main(String[] args) { Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); System.out.println(format.format(scheduledExecutionTime()) + ", called"); } }; // 延遲一秒,打印一次 // 打印結(jié)果如下:10:58:24, called timer.schedule(task, 1000); }
2. 循環(huán)調(diào)度 - schedule()
public static void main(String[] args) { Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); System.out.println(format.format(scheduledExecutionTime()) + ", called"); } }; // 固定時(shí)間的調(diào)度方式,延遲一秒,之后每隔一秒打印一次 // 打印結(jié)果如下: // 11:03:55, called // 11:03:56, called // 11:03:57, called // 11:03:58, called // 11:03:59, called // ... timer.schedule(task, 1000, 1000); }
3. 循環(huán)調(diào)度 - scheduleAtFixedRate()
public static void main(String[] args) { Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); System.out.println(format.format(scheduledExecutionTime()) + ", called"); } }; // 固定速率的調(diào)度方式,延遲一秒,之后每隔一秒打印一次 // 打印結(jié)果如下: // 11:08:43, called // 11:08:44, called // 11:08:45, called // 11:08:46, called // 11:08:47, called // ... timer.scheduleAtFixedRate(task, 1000, 1000); }
4. schedule()和scheduleAtFixedRate()的區(qū)別
從2和3的結(jié)果來看,他們達(dá)到的效果似乎是一樣的。既然效果一樣,JDK為啥要實(shí)現(xiàn)為兩個(gè)方法呢?他們應(yīng)該有不一樣的地方!
在正常的情況下,他們的效果是一模一樣的。而在異常的情況下 - 任務(wù)執(zhí)行的時(shí)間比間隔的時(shí)間更長(zhǎng),他們是效果是不一樣的。
我們先來看看 schedule() 的異常效果:
public static void main(String[] args) { Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(format.format(scheduledExecutionTime()) + ", called"); } }; timer.schedule(task, 1000, 2000); // 執(zhí)行結(jié)果如下: // 11:18:56, called // 11:18:59, called // 11:19:02, called // 11:19:05, called // 11:19:08, called // 11:19:11, called }
接下來我們看看 scheduleAtFixedRate() 的異常效果:
public static void main(String[] args) { Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(format.format(scheduledExecutionTime()) + ", called"); } }; timer.scheduleAtFixedRate(task, 1000, 2000); // 執(zhí)行結(jié)果如下: // 11:20:45, called // 11:20:47, called // 11:20:49, called // 11:20:51, called // 11:20:53, called // 11:20:55, called }
樓主一直相信,實(shí)踐是檢驗(yàn)真理比較好的方式,上面的例子從側(cè)面驗(yàn)證了我們最初的猜想。
但是,這兒引出了另外一個(gè)問題。既然 Timer 內(nèi)部是單線程實(shí)現(xiàn)的,在執(zhí)行間隔為2秒、任務(wù)實(shí)際執(zhí)行為3秒的情況下, scheduleAtFixedRate 是如何做到2秒輸出一次的呢?
【特別注意】
這兒其實(shí)是一個(gè)障眼法。需要重點(diǎn)關(guān)注的是,打印方法輸出的值是通過調(diào)用 scheduledExecutionTime() 來生成的,而這個(gè)方法并不一定是任務(wù)真實(shí)執(zhí)行的時(shí)間,而是當(dāng)前任務(wù)應(yīng)該執(zhí)行的時(shí)間。
源碼閱讀
樓主對(duì)于知識(shí)的理解是,除了知其然,還需要知其所以然。而閱讀源碼是打開 知其所以然 大門的一把強(qiáng)有力的鑰匙。在JDK中, Timer 主要由 TimerTask 、 TaskQueue 和 TimerThread 組成。
1. TimerTask
TimerTask 表示任務(wù)調(diào)度器執(zhí)行的任務(wù),繼承自 Runnable ,其內(nèi)部維護(hù)著任務(wù)的狀態(tài),一共有4種狀態(tài)
TimerTask 還有下面的成員變量
分析完大致的功能之后,我們來看看其代碼。
/** * The state of this task, chosen from the constants below. */ int state = VIRGIN; /** * This task has not yet been scheduled. */ static final int VIRGIN = 0; /** * This task is scheduled for execution. If it is a non-repeating task, * it has not yet been executed. */ static final int SCHEDULED = 1; /** * This non-repeating task has already executed (or is currently * executing) and has not been cancelled. */ static final int EXECUTED = 2; /** * This task has been cancelled (with a call to TimerTask.cancel). */ static final int CANCELLED = 3;
TimerTask 有兩個(gè)操作方法
cancel() 比較簡(jiǎn)單,主要對(duì)當(dāng)前任務(wù)加鎖,然后變更狀態(tài)為已取消。
public boolean cancel() { synchronized(lock) { boolean result = (state == SCHEDULED); state = CANCELLED; return result; } }
而在 scheduledExecutionTime() 中,任務(wù)執(zhí)行時(shí)間是通過下一次執(zhí)行時(shí)間減去間隔時(shí)間的方式計(jì)算出來的。
public long scheduledExecutionTime() { synchronized(lock) { return (period < 0 ? nextExecutionTime + period : nextExecutionTime - period); } }
2. TaskQueue
TaskQueue 是一個(gè)隊(duì)列,在 Timer 中用于存放任務(wù)。其內(nèi)部是使用【最小堆算法】來實(shí)現(xiàn)的,堆頂?shù)娜蝿?wù)將最先被執(zhí)行。由于使用了【最小堆】, TaskQueue 判斷執(zhí)行時(shí)間是否已到的效率極高。我們來看看其內(nèi)部是怎么實(shí)現(xiàn)的。
class TaskQueue { /** * Priority queue represented as a balanced binary heap: the two children * of queue[n] are queue[2*n] and queue[2*n+1]. The priority queue is * ordered on the nextExecutionTime field: The TimerTask with the lowest * nextExecutionTime is in queue[1] (assuming the queue is nonempty). For * each node n in the heap, and each descendant of n, d, * n.nextExecutionTime <= d.nextExecutionTime. * * 使用數(shù)組來存放任務(wù) */ private TimerTask[] queue = new TimerTask[128]; /** * The number of tasks in the priority queue. (The tasks are stored in * queue[1] up to queue[size]). * * 用于表示隊(duì)列中任務(wù)的個(gè)數(shù),需要注意的是,任務(wù)數(shù)并不等于數(shù)組長(zhǎng)度 */ private int size = 0; /** * Returns the number of tasks currently on the queue. */ int size() { return size; } /** * Adds a new task to the priority queue. * * 往隊(duì)列添加一個(gè)任務(wù) */ void add(TimerTask task) { // Grow backing store if necessary // 在任務(wù)數(shù)超過數(shù)組長(zhǎng)度,則通過數(shù)組拷貝的方式進(jìn)行動(dòng)態(tài)擴(kuò)容 if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); // 將當(dāng)前任務(wù)項(xiàng)放入隊(duì)列 queue[++size] = task; // 向上調(diào)整,重新形成一個(gè)最小堆 fixUp(size); } /** * Return the "head task" of the priority queue. (The head task is an * task with the lowest nextExecutionTime.) * * 隊(duì)列的第一個(gè)元素就是最先執(zhí)行的任務(wù) */ TimerTask getMin() { return queue[1]; } /** * Return the ith task in the priority queue, where i ranges from 1 (the * head task, which is returned by getMin) to the number of tasks on the * queue, inclusive. * * 獲取隊(duì)列指定下標(biāo)的元素 */ TimerTask get(int i) { return queue[i]; } /** * Remove the head task from the priority queue. * * 移除堆頂元素,移除之后需要向下調(diào)整,使之重新形成最小堆 */ void removeMin() { queue[1] = queue[size]; queue[size--] = null; // Drop extra reference to prevent memory leak fixDown(1); } /** * Removes the ith element from queue without regard for maintaining * the heap invariant. Recall that queue is one-based, so * 1 <= i <= size. * * 快速移除指定位置元素,不會(huì)重新調(diào)整堆 */ void quickRemove(int i) { assert i <= size; queue[i] = queue[size]; queue[size--] = null; // Drop extra ref to prevent memory leak } /** * Sets the nextExecutionTime associated with the head task to the * specified value, and adjusts priority queue accordingly. * * 重新調(diào)度,向下調(diào)整使之重新形成最小堆 */ void rescheduleMin(long newTime) { queue[1].nextExecutionTime = newTime; fixDown(1); } /** * Returns true if the priority queue contains no elements. * * 隊(duì)列是否為空 */ boolean isEmpty() { return size==0; } /** * Removes all elements from the priority queue. * * 清除隊(duì)列中的所有元素 */ void clear() { // Null out task references to prevent memory leak for (int i=1; i<=size; i++) queue[i] = null; size = 0; } /** * Establishes the heap invariant (described above) assuming the heap * satisfies the invariant except possibly for the leaf-node indexed by k * (which may have a nextExecutionTime less than its parent's). * * This method functions by "promoting" queue[k] up the hierarchy * (by swapping it with its parent) repeatedly until queue[k]'s * nextExecutionTime is greater than or equal to that of its parent. * * 向上調(diào)整,使之重新形成最小堆 */ private void fixUp(int k) { while (k > 1) { int j = k >> 1; if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } } /** * Establishes the heap invariant (described above) in the subtree * rooted at k, which is assumed to satisfy the heap invariant except * possibly for node k itself (which may have a nextExecutionTime greater * than its children's). * * This method functions by "demoting" queue[k] down the hierarchy * (by swapping it with its smaller child) repeatedly until queue[k]'s * nextExecutionTime is less than or equal to those of its children. * * 向下調(diào)整,使之重新形成最小堆 */ private void fixDown(int k) { int j; while ((j = k << 1) <= size && j > 0) { if (j < size && queue[j].nextExecutionTime > queue[j+1].nextExecutionTime) j++; // j indexes smallest kid if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } } /** * Establishes the heap invariant (described above) in the entire tree, * assuming nothing about the order of the elements prior to the call. */ void heapify() { for (int i = size/2; i >= 1; i--) fixDown(i); } }
3. TimerThread
TimerThread 作為 Timer 的成員變量,扮演著調(diào)度器的校色。我們先來看看它的構(gòu)造方法,作用主要就是持有任務(wù)隊(duì)列。
TimerThread(TaskQueue queue) { this.queue = queue; }
接下來看看 run() 方法,也就是線程執(zhí)行的入口。
public void run() { try { mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } }
主邏輯全在 mainLoop() 方法。在 mainLoop 方法執(zhí)行完之后,會(huì)進(jìn)行資源的清理操作。我們來看看 mainLoop() 方法。
private void mainLoop() { // while死循環(huán) while (true) { try { TimerTask task; boolean taskFired; // 對(duì)queue進(jìn)行加鎖,保證一個(gè)隊(duì)列里所有的任務(wù)都是串行執(zhí)行的 synchronized(queue) { // Wait for queue to become non-empty // 操作1,隊(duì)列為空,需要等待新任務(wù)被調(diào)度,這時(shí)進(jìn)行wait操作 while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); // 這兒再次判斷隊(duì)列是否為空,是因?yàn)椤静僮?】有任務(wù)進(jìn)來了,同時(shí)任務(wù)又被取消了(進(jìn)行了`cancel`操作), // 這時(shí)如果隊(duì)列再次為空,那么需要退出線程,避免循環(huán)被卡死 if (queue.isEmpty()) break; // Queue is empty and will forever remain; die // Queue nonempty; look at first evt and do the right thing long currentTime, executionTime; // 取出隊(duì)列中的堆頂元素(下次執(zhí)行時(shí)間最小的那個(gè)任務(wù)) task = queue.getMin(); // 這兒對(duì)堆元素進(jìn)行加鎖,是為了保證任務(wù)的可見性和原子性 synchronized(task.lock) { // 取消的任務(wù)將不再被執(zhí)行,需要從隊(duì)列中移除 if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } // 獲取系統(tǒng)當(dāng)前時(shí)間和任務(wù)下次執(zhí)行的時(shí)間 currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; // 任務(wù)下次執(zhí)行的時(shí)間 <= 系統(tǒng)當(dāng)前時(shí)間,則執(zhí)行此任務(wù)(設(shè)置狀態(tài)標(biāo)記`taskFired`為true) if (taskFired = (executionTime<=currentTime)) { // `peroid`為0,表示此任務(wù)只需執(zhí)行一次 if (task.period == 0) { // Non-repeating, remove queue.removeMin(); task.state = TimerTask.EXECUTED; } // period不為0,表示此任務(wù)需要重復(fù)執(zhí)行 // 在這兒就體現(xiàn)出了`schedule()`方法和`scheduleAtFixedRate()`的區(qū)別 else { // Repeating task, reschedule queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } } // 任務(wù)沒有被觸發(fā),隊(duì)列掛起(帶超時(shí)時(shí)間) if (!taskFired) // Task hasn't yet fired; wait queue.wait(executionTime - currentTime); } // 任務(wù)被觸發(fā),執(zhí)行任務(wù)。執(zhí)行完后進(jìn)入下一輪循環(huán) if (taskFired) // Task fired; run it, holding no locks task.run(); } catch(InterruptedException e) { } } }
4. Timer
Timer 通過構(gòu)造方法做了下面的事情:
/** * The timer thread. */ private final TimerThread thread = new TimerThread(queue); public Timer(String name, boolean isDaemon) { thread.setName(name); thread.setDaemon(isDaemon); thread.start(); }
在 Timer 中,真正的暴露給用戶使用的調(diào)度方法只有兩個(gè), schedule() 和 scheduleAtFixedRate() ,我們來看看。
public void schedule(TimerTask task, long delay) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); sched(task, System.currentTimeMillis()+delay, 0); } public void schedule(TimerTask task, Date time) { sched(task, time.getTime(), 0); } public void schedule(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, -period); } public void schedule(TimerTask task, Date firstTime, long period) { if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), -period); } public void scheduleAtFixedRate(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, period); } public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), period); }
從上面的代碼我們看出下面幾點(diǎn)。
接下來我們看看 sched() 方法。
private void sched(TimerTask task, long time, long period) { // 1. `time`不能為負(fù)數(shù)的校驗(yàn) if (time < 0) throw new IllegalArgumentException("Illegal execution time."); // Constrain value of period sufficiently to prevent numeric // overflow while still being effectively infinitely large. // 2. `period`不能超過`Long.MAX_VALUE >> 1` if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) { // 3. Timer被取消時(shí),不能被調(diào)度 if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); // 4. 對(duì)任務(wù)加鎖,然后設(shè)置任務(wù)的下次執(zhí)行時(shí)間、執(zhí)行周期和任務(wù)狀態(tài),保證任務(wù)調(diào)度和任務(wù)取消是線程安全的 synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } // 5. 將任務(wù)添加進(jìn)隊(duì)列 queue.add(task); // 6. 隊(duì)列中如果堆頂元素是當(dāng)前任務(wù),則喚醒隊(duì)列,讓`TimerThread`可以進(jìn)行任務(wù)調(diào)度 if (queue.getMin() == task) queue.notify(); } }
sched() 方法經(jīng)過了下述步驟:
【說明】:我們需要特別關(guān)注一下第6點(diǎn)。為什么堆頂元素必須是當(dāng)前任務(wù)時(shí)才喚醒隊(duì)列呢?原因在于堆頂元素所代表的意義,即:堆頂元素表示離當(dāng)前時(shí)間最近的待執(zhí)行任務(wù)!
【例子1】:假如當(dāng)前時(shí)間為1秒,隊(duì)列里有一個(gè)任務(wù)A需要在3秒執(zhí)行,我們新加入的任務(wù)B需要在5秒執(zhí)行。這時(shí),因?yàn)?TimerThread 有 wait(timeout) 操作,時(shí)間到了會(huì)自己?jiǎn)拘?。所以為了性能考慮,不需要在 sched() 操作的時(shí)候進(jìn)行喚醒。
【例子2】:假如當(dāng)前時(shí)間為1秒,隊(duì)列里有一個(gè)任務(wù)A需要在3秒執(zhí)行,我們新加入的任務(wù)B需要在2秒執(zhí)行。這時(shí),如果不在 sched() 中進(jìn)行喚醒操作,那么任務(wù)A將在3秒時(shí)執(zhí)行。而任務(wù)B因?yàn)樾枰?秒執(zhí)行,已經(jīng)過了它應(yīng)該執(zhí)行的時(shí)間,從而出現(xiàn)問題。
任務(wù)調(diào)度方法 sched() 分析完之后,我們繼續(xù)分析其他方法。先來看一下 cancel() ,該方法用于取消 Timer 的執(zhí)行。
public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); queue.notify(); // In case queue was already empty. } }
從上面源碼分析來看,該方法做了下面幾件事情:
有的時(shí)候,在一個(gè) Timer 中可能會(huì)存在多個(gè) TimerTask 。如果我們只是取消其中幾個(gè) TimerTask ,而不是全部,除了對(duì) TimerTask 執(zhí)行 cancel() 方法調(diào)用,還需要對(duì) Timer 進(jìn)行清理操作。這兒的清理方法就是 purge() ,我們來看看其實(shí)現(xiàn)邏輯。
public int purge() { int result = 0; synchronized(queue) { // 1. 遍歷所有任務(wù),如果任務(wù)為取消狀態(tài),則將其從隊(duì)列中移除,移除數(shù)做加一操作 for (int i = queue.size(); i > 0; i--) { if (queue.get(i).state == TimerTask.CANCELLED) { queue.quickRemove(i); result++; } } // 2. 將隊(duì)列重新形成最小堆 if (result != 0) queue.heapify(); } return result; }
5. 喚醒隊(duì)列的方法
通過前面源碼的分析,我們看到隊(duì)列的喚醒存在于下面幾處:
第一點(diǎn)和第二點(diǎn)其實(shí)已經(jīng)分析過了,下面我們來看看第三點(diǎn)。
private final Object threadReaper = new Object() { protected void finalize() throws Throwable { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.notify(); // In case queue is empty. } } };
該方法用于在GC階段對(duì)任務(wù)隊(duì)列進(jìn)行喚醒,此處往往被讀者所遺忘。
那么,我們回過頭來想一下,為什么需要這段代碼呢?
我們?cè)诜治?TimerThread 的時(shí)候看到:如果 Timer 創(chuàng)建之后,沒有被調(diào)度的話,將一直wait,從而陷入 假死狀態(tài) 。為了避免這種情況,并發(fā)大師Doug Lea機(jī)智地想到了在 finalize() 中設(shè)置狀態(tài)標(biāo)記 newTasksMayBeScheduled ,并對(duì)任務(wù)隊(duì)列進(jìn)行喚醒操作(queue.notify()),將 TimerThread 從死循環(huán)中解救出來。
總結(jié)
首先,本文演示了 Timer 是如何使用的,然后分析了調(diào)度方法 schedule() 和 scheduleAtFixedRate() 的區(qū)別和聯(lián)系。
然后,為了加深我們對(duì) Timer 的理解,我們通過閱讀源碼的方式進(jìn)行了深入的分析。可以看得出,其內(nèi)部實(shí)現(xiàn)得非常巧妙,考慮得也很完善。
但是因?yàn)?Timer 串行執(zhí)行的特性,限制了其在高并發(fā)下的運(yùn)用。后面我們將深入分析高并發(fā)、分布式環(huán)境下的任務(wù)調(diào)度是如何實(shí)現(xiàn)的,讓我們拭目以待吧~
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。
分享題目:深入理解Java定時(shí)調(diào)度(Timer)機(jī)制
文章分享:http://muchs.cn/article38/jsopsp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供電子商務(wù)、云服務(wù)器、網(wǎng)站建設(shè)、虛擬主機(jī)、網(wǎng)站排名、網(wǎng)站改版
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)