多线程基础
多线程
背景知识:
cpu:做运算功能,一开始是单核的
多进程:(单核cpu)多个进程,采用交替执行的方式执行。但是进程之间的交替付出了很大代价
多cpu和多核cpu:对于操作系统和软件来说都一样。
多线程:随着科技的发展,多个进程可以分别并行在多个cpu上,进程之间变得更加独立。但是过多的进程,导致的交替执行,任然付出了很大代价,所以进步将原本进程中单线程(满足通知执行的需求),改为多线程。多个线程来并发执行,进程主要负责资源分配,线程承当进程的调度功能。
多线程的Java程序
- 其实java命令,它启动了一个jvm进程
- jvm进程会创建一个main线程
- 在main线程中,运行主类中的main方法
- 同时有一个后台进程, 也就是垃圾回收线程。
概念(全文背诵)
进程和线程:
操作系统中所有运行中的任务通常对应一个进程,当程序进入内存运行时,即变成进程。进程时处于运行过程中的程序,是系统进行资源分配和调度的一个独立单位。即使在多核处理器中,宏观上并行的进程微观上也是并发,由于进程之间交替执行需要很大的开销,故进程中扩展多了多线程来帮助进程实现调度,在进程初始化后,主线程就被创建了,线程拥有的是父进程的全部资源,且多个进程共享资源,但相互保持独立性。
多线程有啥用,优势在哪儿?
不是提高执行速度,而是提高CPU的使用率,提高程序的运行效率.
并发,并行,串行?
-
串行:一个任务接一个任务
-
并发:一个时间短内,多个程序快速的交替执行
-
并行: 同一个时间点,多个程序同时运行(其实没有同一个时间点,只是理想的状态,微观上还是并发)
run()方法有啥作用?
run方法就是线程执行体,如果直接调用run就会当作普通方法执行。就作为线程去调用了run,就不受系统的调度去控制而简洁执行。
start()方法的作用?
让线程进入就绪态
如何创建线程和启动?
继承Thread类创建线程类:
public class FirstThread extends Thread{
private int i;
// 线程执行体
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName());
FirstThread thread1 = new FirstThread();
thread1.setName("线程1");
thread1.start();
FirstThread thread2 = new FirstThread();
thread2.setName("线程2");
thread2.start();
}
}
}
-
为什么i 不是static 的时候,线程1,2都执行了100次,而static的时候就线程1,2,两个线程加起来的次数约等于100次,为什么是约等于?
当i非静态变量时候,i没有在两个线程之间共享,主线程的main方法让每个线程都执行了100*100次,当为静态变量的时候,两个线程共享i,所以i的变化对于两个线程是可见的,但注意!由于线程它们使用资源是抢占型的,在都是默认优先级的情况下,会出现一个线程没有执行完,而另一个线程抢占了,导致某个i被重复输出的情况。所以导致输出的数字个数是约等于100。
-
主线程main不会调用run线程体吗??
是的,因为主线程的线程体不是由run()方法确定的,而是由main()方法确定的--main() 方法的方法体代表主线程的执行体。
实现Runnable接口创建线程类(也可通过匿名内部类创建)
public class SecondThread implements Runnable{
private int i;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "--" + i);
if (i == 20){
// 创建类
SecondThread st = new SecondThread();
// 通过new Thread(target, name)方法创建新线程
new Thread(st, "新线程1").start();
new Thread(st, "新线程2").start();
}
}
}
}
两种创建方式有啥不一样?
继承Thread类创建线程类
创建步骤:
写一个类继承Thread类
子类并不是线程类,只是对线程的描述。用子类创建的对象倒是线程。
重写run方法
- 创建子类让子类start,运行子线程
如果不start直接就run,就相当于对象普通调用了。
实现Runnable接口创建线程类(好用)
创建步骤:
实现Runnable接口
重写run方法
主线程中创建实现类对象(这个对象只是作为target)
新建Thread对象,并打上标记。
start执行Thread对象
共享变量的问题:
- 继承Thread类创建的线程类,相互线程对象之间独立,不共享非静态变量,静态变量共享
- 实现Runnable接口创建的线程类,线程对象之间由于是同一个实现类的标记,变量共享
优先级问题:
优先级包括:动态优先级和静态优先级
-
优先级高得线程,随着运行得时间会降低优先级。
-
就绪态得线程随着等待时间得提高优先级。
-
优先级高并不代表一定先run,看操作系统心情。
-
通常不设置具体的数值,只需要设置位normal or MAX_PRIORITY or MIN_PRIORITY 这样设置程序有最好的可移植性
因为不同操作系统的优先级不一样,有的有7个。
-
子线程与父线程拥有相同的优先级
如何控制线程?
-
线程睡眠 sleep
- sleep方法会让当前线程进入阻塞态,完全给别人机会,不管其他线程的优先级。
- 声明抛出了InterruptedException异常,要么捕捉,要么抛出。
-
线程加塞 join
当前执行的线程同学先等等,让调用join方法的先执行完。
通俗一点说:谁他妈让我(某个线程)调用了join方法,谁就等我(某个线程)执行完。比如你让我调用了join的方法,你就得等我执行完。你再执行,但是我在执行的时候,”他“(除了咱俩以外的其他线程)不受影响。
补充理解:
join方法:线程老弟说 ”谁他妈让爷调用了自己的join方法,谁就要等爷执行完。“
通俗点:你修了一个单人间厕所,请我进去方便一下(join()),这肯定得我先弄完。然后你再进去嘛。
-
线程礼让 yield(尽量不要使用这个玩意)
- 没有声明抛出任何异常
- 当前执行的线程1同学先暂停,但是不是进入阻塞态,而是直接进入就绪态。也就是小小的暂停一下。
- 下次系统的线程调度,也很有可能还是线程1又抢到了时间片,开始运行。
-
后台线程(守护线程)setDaemon
线程start之前就要调用守护方法
守护线程到底是被保护的,还是保护别的线程的?
我认为:就垃圾回收线程来说,就是来保护jvm中堆空间不要溢出。保护程序正常执行的。
-
中断线程stop, 直接中断不抛出异常(不建议用) 建议用interrupt(),抛出中断异常,但线程体里面的语句依旧会执行。
线程的五种状态:
新建态,就绪态,运行态,阻塞态,死亡
五种状态出现的标志,在什么情况出现什么状态?
-
新建:新建一个线程
-
就绪:start调用线程就绪,运行到就绪可以通过yield方法
丢失执行权
-
运行:就绪到运行态的转换由系统线程调度所决定
-
阻塞:
除了缺少执行权,还缺失资源
线程阻塞的时候,操作系统会将线程的执行情况中的信息保存在内存中。等待被唤醒的时候再读出来恢复至线程的运行态。
- 运行的进程调用sleep()
- join()会进入阻塞,
- IO中read方法等堵塞方法
- 等待阻塞:wait()也会进入阻塞
- 同步阻塞:试图取访问一个同步监视器,但是人家正在被其他线程使用,并还没解锁呢
- 线程正在等待某个通知 notify
- suspend方法将线程挂起。(避免使用这玩意,容易死锁)
解除阻塞:
- sleep()时间已到
- IO的阻塞式方法已经返回
- 成功获得了试图取得的同步监视器
- 等到了通知
- 线程被调用了resume()恢复方法
-
死亡:
- run() 或者call方法执行完 线程正常结束
- 线程抛出了一个异常
- stop()方法结束(不推荐常导致死锁)
- 死了就是死了,不可再start()复活。再诈尸就出异常
流程图简单总结:
![2021-1-21 17-40-15](C:\Users\Public\Documents\极域课堂管理系统软件V6.0 2016 豪华版\Snapshots\2021-1-21 17-40-15.JPG)
注意:
- 主线程结束时候,子线程该跑的继续跑,只要子线程开启动,就不受主线程的影响了。
- isAlive方法测试线程是否死亡,就绪,运行,阻塞都算还活着,新建和死亡就是死了。false
- sleep()方法该方法有三种重载形式。
后台(守护)进程:
两个相关方法:
- setDaemon(true)方法指定线程设置成后台线程
- isDaemon() 方法 判断是否是后台进程
特点:
- 前台的进程都死了,爷还守护个屁,也死了算了。
疑问:为什么出现多个线程共享的变量的值出现重复输出的问题?(比如多线程买票的demo中出现的重票问题)
出现安全问题的环境:
-
程序实现了多线程
-
多线程共享了数据
-
线程体中有非原子操作
以上三个条件同时满足时:应该警惕安全问题。
线程的安全性问题来源?为什么有安全问题?(核心)
同步延迟性:
不同的线程在交替访问一个内存中的一个变量的时候,先访问的线程在自己所处的cpu缓存中修改了该变量的值,但由于内存中变量偶尔跟不上cpu缓存修改的速度,无法立即同步,导致第二个线程再访问内存中的那个变量时,还是之前的值。 所以出现了安全问题。同步随机性:
可能突然就内存中的变量和各个cpu缓存区中的变量同步了,导致出问题
如何解决呢?
预备知识:
原子性操作的含义?
一个目标操作要么一次执行完,要么不执行。
同步与异步:
- 同步:若资源被占用,哪个这个进程就会等待释放,按顺序执行。同步是一种阻塞模式
- 异步:就是大家各干各个的,某个线程或者进程,因为其他原因暂停,其他的照旧执行,暂停的任务等待条件重新具备以后再继续跑。多线程就是异步的
为什么要有同步的一些操作?
因为操作系统同步机制具有随机性和延迟性,还是自己解决靠谱。
线程同步机制:
synchronized:
同步代码块
如何实现逻辑上的同步?
同步方法或者同步块中的区域,里面的变量的改变,同步的操作都交给线程,一个线程执行完锁释放,其他线程才可进来。而不是操作系统区随机性同步了。
锁对象:锁对象时java的任意对象
同步方法:
普通同步方法:
锁对象时 this
静态同步方法:
锁对象:当前类的.class对象
监视器
何为监视器? 什么应该作为监视器(锁)
- 可以是任意对象,一般声明在继承了Thread的子类,或者实现了Runnable接口的子类 的成员变量位置。
- 共享变量在哪个对象里面。那个对象就作为锁(最好这么做),
- 注意: 多个线程使用同一把锁才能达到同步,达到前者没有用完解锁,后者不能用的效果,如果有每个线程都有一把锁,那么先调用的那个线程不用解锁,后面的线程照样执行。
Lock同步锁
使用步骤:
- 新建一个锁对象 new ReentrantLock();
- lock()方法 上锁
- unlook 解锁
同步synchronized vs Lock
- Lock锁具有比synchronized 更加广泛的锁定操作,允许实现更灵活的结构。
- Lock显式上锁,解锁。且锁对象明确。
- synchronized 简单 同步方法不用创建对象。隐式锁
死锁:
两个线程相互等待都不愿意释放自己的锁。
死锁解决方法:
-
更改锁的顺序
-
在加上一把锁,把操作变成了原子操作
补充 :每个类都有一个字节码文件对象·。
线程之间如何通信?
- 通过Object对象的synchronized + wait 和 notify notifyAll方法
- 通过Loke + Condition 的await 和 signal() 和 signalAll()方法
有哪几种通信方式:
- 传统方法 隐式调用wait 和signal,signalAll方法
- 使用Lock+Conditionn控制进程同学,原理和上面一样。只是Condition作为同步监视器,显式得去控制
- 使用阻塞队列BlockingQueue控制,主要是用put和take方法
传统通信手段:
- wait():此方法一旦执行,当前线程就进入阻塞状态,并释放同步监视器(锁资源)。
- notify():此方法一旦执行,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级最高的那个。
- notifyAll():此方法一旦执行,就会唤醒所有被 wait 的线程。
- 3、wait()、notify()、notifyAll()三个方法必须使用在同步代码块或者同步方法中。
- 4、wait()、notify()、notifyAll()三个方法是定义在java.lang.Object类中
wait()方法
阻塞功能:
- 在某个线程中:对象.wait() 在哪个线程中被调用,哪个线程就进入阻塞态
- 它的阻塞态,得被别人通知才醒来。
- 和sleep最大的区别,sleep睡醒以后,锁没有释放,线程一wait 会释放锁。
生产者消费者模型:
生成者线程干啥: 生产产品
消费者线程干啥:消费产品
生产者消费者模型实例:
开一个水果店,有完备的供应链,有水果就卖出,生产者线程和消费者线程,通过水果店通信。
ReadMe:
生产者和消费者模型
生产者和消费者都是线程,中间的商店作为共享变量
1.新建生产者和消费者线程,以那个商店作为共享变量
2.以商店商品的消耗情况,是否为null作为两个线程切换的点
3.两个线程的线程题run方法应该while不停生产和消费。
4.能够接收两个线程的生产和消费的功能交给,商店去做判断。
Producer类:实现Runnable
构造方法:
以商店对象作为有参构造
成员变量:
Shop shop 变量 // 要给他供货
成员方法:
production // 生产商品
重写run方法
Consumer类:实现Runnable
以商店对象作为有参构造
成员变量
Shop shop 变量 // 要从商店拿货
成员方法
buyFruit
重写run方法
Shop类 实现Runnable接口
成员变量:
Fruits fruits 商品
成员方法,(线程的通信交给它来做判断)
要放置商品, placeFruitsthis.fruits = fruits;
要卖货,sellFruits 将成员变量设置为空
Fruits 类
成员变量:
String name
int price
构造方法
无参构造
有参构造
FruitsShop类:
package Day22.producerandconsumer;
import java.util.EmptyStackException;
/**
* @author :XiongZhikang
* @date :Created in 2021/1/22 21:40
* @description:想吃水果了
* @modified By:
*/
public class FruitsShop {
// 我的水果专供一种水果
Fruits fruits;
// 接收生产者 生产出来的的水果, 在实现的过程中,由于没有加锁,出现了不合法的监视器异常,为什么呢?
public synchronized void setFruits(Fruits fruits) {
//
// 如果过水果店里还有水果,生产者生产的水果,商店就先不上架
if (this.fruits != null){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果水果店里面没有水果了,就上架,并且通知其他线程,比如消费者线程来消费拉
else{
System.out.println("从工厂进货:" + fruits);
this.fruits = fruits;
this.notifyAll();
}
}
// 将水果卖出的方法
public synchronized void sellFruits(){
// synchronized
// 如果商店没了水果,就先别卖了,消费者进程先wait
if (fruits == null){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 如果还有水果, 就卖出,并通知生产者线程等其他线程干活了,生产者快来上货!
else {
System.out.println("水果点卖出:" + fruits);
this.fruits = null;
this.notifyAll();
}
}
}
class Fruits{
String name;
int price;
public Fruits(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Fruits{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
Producer类:
package Day22.producerandconsumer;
import java.util.Random;
/**
* @author :XiongZhikang
* @date :Created in 2021/1/22 21:37
* @description:生产者
* @modified By:
*/
public class Producer implements Runnable{
// 生产者要为商户提供水果,这里我们让水果品种随机
FruitsShop fruitsShop;
Random random = new Random();
// 这是生产者能够提供的水果类型
Fruits[] fruitsList = {new Fruits("西瓜", 30), new Fruits("榴莲", 50)
,new Fruits("香蕉", 5)};
public Producer(FruitsShop fruitsShop) {
this.fruitsShop = fruitsShop;
}
// 生产水果:
public void production(){
fruitsShop.setFruits(fruitsList[random.nextInt(3)]);
}
@Override
public void run() {
// 24小时无休止生产
while (true){
production();
}
}
}
Consumer类:
package Day22.producerandconsumer;
/**
* @author :XiongZhikang
* @date :Created in 2021/1/22 21:38
* @description:消费者
* @modified By:
*/
public class Consumer implements Runnable{
// 消费者 要去那个商店里面买水果
FruitsShop fruitsShop;
public Consumer(FruitsShop fruitsShop) {
this.fruitsShop = fruitsShop;
}
// 要调用
public void buyFruit(){
fruitsShop.sellFruits();
}
@Override
public void run() {
// 一直买入
while(true){
buyFruit();
}
}
}
RunPACPattern类:
package Day22.producerandconsumer;
/**
* @author :XiongZhikang
* @date :Created in 2021/1/22 22:35
* @description:运行生产者消费者模式
* @modified By:
*/
public class RunPACPattern {
public static void main(String[] args) {
// 小店,工厂,买家全部实例化。 准备干大事
FruitsShop fruitsShop = new FruitsShop();
// 小店作为中间最重要的一环,参与其他两个线程的构造。通过这个小店联系工厂和买家
Producer producer = new Producer(fruitsShop);
Consumer consumer = new Consumer(fruitsShop);
// 然后联系厂家 和 卖家 建立线程。准备开工
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
System.out.println("熊氏集团开业大吉!!!");
// 开始动工
producerThread.start();
consumerThread.start();
}
}
这种设计的优点:
我们将功能,具体的操作放入到中间容器,一个类中处理。
而让生产者,和消费者只执行简单的生产和买入的操作。
实现过程中有个疑问:
为什么wait外面一定要有同步锁?
参考说法:
https://stackoverflow.com/questions/2779484/why-must-wait-always-be-in-synchronized-block
https://www.jianshu.com/p/4dc88dff8f86
分析:
一开始没有看明白,原因在于低估了cpu调度两个线程的速度,对多线程出现安全问题的根本原因不敏感。从我的代码上看,两个线程调用的商店的不同方法 ,表面上是很独立,各自运行各自的,商店有水果就不生产者就wait,消费者就买+notify,商店没有水果,生产者就生产,消费者买不到就wait。但是重点在于他们有一个共享的变量,Fruits fruits,如果不加锁,可能出现生产者在给商店添加商品,执行到this.fruits = fruits,并this.notifyAll();通知消费者线程来买,重点来了:一般都是多核cpu,进程中的多线程并行执行.各自把内存中的fruits拿到自己所在的cpu缓存,由于cpu缓存的对fruits修改很快,内存中的fruits还是null,或者另一个线程所在的核心中的缓存区还没来得及同步,导致消费者线程fruits == null,线程wait, 生产者线程继续生产,发现自己线程运行在的cpu缓存中的fruits不是null,也wait。导致两个线程都进入了阻塞,所以wait必须上锁防止操作系统的同步问题。
根本原因:
- 多核cpu 多个cpu,cpu缓存与内存的同步延迟性。
- 两个线程并不独立,共享了变量
- 对变量的操作也非原子性操作。受线程调度机制影响,导致变量来不及同步。
多线程会出现安全问题的信号
程序实现了多线程
多线程共享了数据
线程体中有非原子操作
(原子操作:不会被线程调度机制打断的操作)
一个线程进入阻塞态后,再次醒来,是否会从阻塞之前还没执行的地方运行?
是的
面试题:Thread.sleep vs Object.wait()
1.所属不同:
sleep定义在Thread类,静态方法
wait定义在 Object类中,非静态方法
2.唤醒条件不同
a. sleep:休眠时间到
b. wait:在其他线程中,在同一个锁对象上,调用了notify或notifyAll方法
3.使用条件不同:
a.sleep没有任何前提条件
b. wait(),必须当前线程,持有锁对象,锁对象上调用wait()
4.休眠时,对锁对象的持有,不同:(最最核心的区别)
a.线程因为sleep方法而处于阻塞状态的时候,在阻塞的时候不会放弃对锁的持有
b.但是wait()方法,会在阻塞的时候,放弃锁对象持有
线程池:
真正开发中用的都是线程池,线程池类似于公交车,用完了还可以继续用。
优点:
- 提高响应速度,减少线程的创建时间
- 降低资源消耗,重复利用线程池中线程,不需要每次都创建
- 便于线程管理。
两种线程池的创建方法以及特点:
- ExecutorService newCashedThreadPool()
特点:
-
线程数量不固定
-
60s没有被使用,立即销毁
- ExecutorService newFixedThreadPool(线程池的大小)
特点:
-
线程数量固定
-
维护一个无界队列,暂存来不及处理的任务
-
按照任务的提交顺序,将任务执行。
一般线程池的创建和执行任务的步骤:
- 创建提供指定线程数量的线程池,
- 需要提供实现Runnable接口,创建实现类对象,执行指定线程的操作
- execute 提交,或者submit提交任务和线程池。
- shutdown关闭
public class ThreadPool {
public static void main(String[] args) {
// 建立线程池 指定数量
ExecutorService service = Executors.newFixedThreadPool(10);
// 创建Runnable的实现类对象,提供实现Runnable 接口实现类的对象
NumberThread target = new NumberThread();
service.execute(target); // 适用于runnable
// service.submit()
// 关闭链接
service.shutdown();
}
}
例子:利用线程池创建线程买100张票
步骤:
- 写一个类来实现Runnable接口
- 重写run方法
- main方法中,Executors.newFixedThreadPool(10);创建一个线程池,可以用接口ExecutorService引用指向,或者用实现类ThreadPoolExecutor引用指向。子类可以改池的参数。
- 创建Runnable接口的实现类对象 让其作为一个target
- 批量用线程池创建线程。
- 任务完成,线程池shutdown()
package Day21;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
class NumberThread implements Runnable{
int i;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "在执行,已经买了: " + i + "票");
try {
Thread.currentThread().join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
// 建立线程池 指定数量 多态方式 Executors是工厂类 ExecutorService代表尽快执行线程的线程池,是一个接口
ExecutorService service = Executors.newFixedThreadPool(10);
// 看看这个线程池是哪个类创建的
System.out.println(service.getClass());
// ThreadPoolExecutor转化为实现子类
ThreadPoolExecutor service1 = ((ThreadPoolExecutor) service);
// 实现类对象可以修改线程池的参宿
service1.setCorePoolSize(11);
// 创建Runnable的实现类对象,提供实现Runnable 接口实现类的对象
NumberThread target = new NumberThread();
// 以for循环,线程池的形式 批量创建线程
for (int i = 0; i < 8; i++) {
service1.execute(target);
}
// service.submit()
// 关闭链接
service.shutdown();
}
}
实现Callable接口创建线程任务并交给线程池做处理:
- 创建Callable接口实现类
- 重写call方法
- 创建线程池
- 创建Callable接口实现类对象
- 以Callable接口实现类对象作为任务,提交给线程池
有啥用啊:
可能就是 返回值,作为线程结束的标志吧,或许可以实现多线程分解大问题,计算完各个小问题的结果,通过返回值拼凑得到大问题的结果吧。
定时任务:
Timer
数据结构:
其中维护了一个任务集合(小顶堆)有一个后台进程负责:每次从小顶堆中取出堆任务。执行完的进程,会到堆中重新排序。
怎么写一个定时任务:
- 写个子类继承TimerTask 作用:定时任务
- 重写run方法 具体任务内容
- main 方法新建要给定时器 new Timer()
- 用定时器给定时任务安排执行计划 schedule(任务,第一次执行时间,然后间隔多久执行一次)
- 不用了就cancel
例子: 将一个字符定时写入一个名为日志文件中去。
package Day23;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* @author :XiongZhikang
* @date :Created in 2021/1/23 14:11
* @description:重写日志文件定时器
* @modified By:
*/
public class RewriteLogFileTimer extends TimerTask {
public static void main(String[] args) {
// 创建一个定时器
Timer timer = new Timer();
// 启动定时器,一次执行+周期执行
timer.schedule(new RewriteLogFileTimer(), 3000, 5000);
// 不用了 就cancel 有用的时候 还是让它继续跑
// timer.cancel();
}
@Override
public void run() {
// 线程体内要重写日志文件
// 每次执行都新建一个时间戳 时间戳转换为指定 格式时间
String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime());
// 就输入一个字符串。“xxx日期 日志写入成功”
try(
FileWriter fileWriter = new FileWriter("E:\\JavaProjectStation\\JavaBaseHomeworkFile\\LogFile.txt", true);
)
{
fileWriter.write(currentTime + "\t日志文件备份成功\n");
} catch (IOException e) {
e.printStackTrace();
}
}
}