Java多线程
进程和线程
进程:即为OS中运行的程序,它是OS管理的基本运行单元。
线程:进程中独立运行的子任务。例如在QQ运行时,有很多子任务在同时运行,比如好友视频线程,下载文件线程,传输数据线程等等。
多线程的优点
在使用多任务OS中,我们可以最大限度地利用CPU的空闲时间处理其他任务,即CPU在不同任务之间不停切换。而使用多线程技术后,可以在同一时间内运行更多不同种类的任务。
按照传统的理解,程序中的代码应该是一行一行的执行,上一行代码若没有执行完毕,下一行代码则不会执行,而多线程是若上一行代码没有执行完毕,下面的代码就可以执行了,两行代码可以交替的执行。
使用多线程
实现多线程编程有两种:继承Thread类 和 实现Runnable接口
但实际上,Thread类实现了Runnable接口,它们之间具有多态关系;其次,为了支持多继承,可以以实现Runnable接口的方式。
实际上,这两种方式创建的线程在工作时性质是一样的。
(1)继承Thread类
public static class myThread extends Thread {
private int index;
public myThread(int index) {
this.index = index;
}
@Override
public void run() {
System.out.println("myThreead-" + index);
}
}
public static void main(String[] args) {
myThread mythead1 = new myThread(1);
myThread mythead2 = new myThread(2);
myThread mythead3 = new myThread(3);
mythead1.start();
mythead2.start();
mythead3.start();
}
执行start()方法的顺序不代表线程启动的顺序
(2)实现Runnable接口
若希望创建的线程类已有一个父类,此时不能继承Thread类,因此可以实现Runnable接口来应对这种情况。
public static class myRunnable implements Runnable {
@Override
public void run() {
System.out.println("");
}
}
在Thread类的构造函数中,有两个构造函数可以传递Runnable接口
Thread(Runnable target);
Thread(Runnable target, String name);
Runnable myRunnable = new myRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println("结束");
实例变量和线程安全
自定义线程类中的实例变量,对于其他线程可以有共享和不共享之分。
(1)不共享数据index的情况
public static class myThread extends Thread {
private int index = 5;
public myThread(String name) {
this.setName(name);
}
@Override
public void run() {
index--;
System.out.println(this.currentThread().getName() + "计算 " + index);
}
}
public static void main(String[] args) {
new myThread("A").start();
new myThread("B").start();
new myThread("C").start();
}
创建3个线程,每个线程有各自的index变量
(2)共享数据index的情况
public static class myThread extends Thread {
private int index = 5;
@Override
public void run() {
index--;
System.out.println(this.currentThread().getName() + "计算 " + index);
}
}
public static void main(String[] args) {
myThread mT = new myThread();
new Thread(mT, "A").start();
new Thread(mT, "B").start();
new Thread(mT, "C").start();
}
在某些JVM中,index--的操作分成3步:
- 取得原有index值
- 计算index-1
- 对index进行赋值
在这3个步骤,若有多个线程同时访问,则一定会出现非线程安全问题。
非线程安全问题:多个线程对同一对象中同一个实例变量进行操作时,会出现值更改,值不同步问题
可以用 synchronized 关键字来修饰方法来解决此问题
@Override
public synchronized void run() {
index--;
System.out.println(this.currentThread().getName() + "计算 " + index);
}
index-- 在println()方法里使用出现的问题
在前面 非线程安全的例子中,index-- 是作为单独的一行。下面我们将这个语句放在输出语句println()中
@Override
public void run() {
System.out.println(this.currentThread().getName() + "计算 " + (--index));
}
这样执行的结果还是会出现非线程安全的问题。
虽然println()方法在内部是同步的,但index--操作是在进入输出方法前发生的,因此仍有出现问题的概率
isAlive()方法
isAlive()方法是测试线程是否处于活动状态。
活动状态:线程已启动且尚未终止,处于正在运行或者准备开始运行的状态。
public static class myThread extends Thread {
@Override
public void run() {
System.out.println("状态:" + this.isAlive());
}
}
public static void main(String[] args) {
myThread mT = new myThread();
System.out.println("开始:" + mT.isAlive());
mT.start();
System.out.println("结束:" + mT.isAlive());
}
运行结果:
开始:false
结束:true
状态:true
需要说明的是
System.out.println("结束:" + mT.isAlive());
虽然结果是true,但此值是不确定的,打印true是因为myThread线程尚未执行完毕。若将main函数改为
public static void main(String[] args) throws InterruptedException{
myThread mT = new myThread();
System.out.println("开始:" + mT.isAlive());
mT.start();
Thread.sleep(1000); // 添加此句
System.out.println("结束:" + mT.isAlive());
}
则结束那一段输出为 false。因为myThread对象已在1s内执行完毕。
另外在使用isAlive()方法时,若是将线程对象以构造参数的方式传递该Thread对象进行start()启动时,运行的结果和前面示例是有差异的。造成差异的原因是Thread.currentThread()和this的差异。下面测试这个实验:
public static class myThread extends Thread {
public myThread() {
System.out.println("构造函数开始----");
System.out.println("Thread.currentThread().getName()线程:" + Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive()线程是否存活:" + Thread.currentThread().isAlive());
System.out.println("this.getName()线程:" + this.getName());
System.out.println("this.isAlive()线程是否存活:" + this.isAlive());
System.out.println("构造函数结束----");
}
@Override
public void run() {
System.out.println("run函数开始----");
System.out.println("Thread.currentThread().getName()线程:" + Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive()线程是否存活:" + Thread.currentThread().isAlive());
System.out.println("this.getName()线程:" + this.getName());
System.out.println("this.isAlive()线程是否存活:" + this.isAlive());
System.out.println("run函数结束----");
;
}
}
public static void main(String[] args) throws InterruptedException{
myThread myThread = new myThread();
Thread thread = new Thread(myThread);
System.out.println("main函数开始,线程thread: " + thread.isAlive());
thread.setName("A");
thread.start();
System.out.println("main函数结束,线程thread:" + thread.isAlive());
}
输出语句为:
构造函数开始----
Thread.currentThread().getName()线程:main
Thread.currentThread().isAlive()线程是否存活:true
this.getName()线程:Thread-0
this.isAlive()线程是否存活:false
构造函数结束----
main函数开始,线程thread: false
main函数结束,线程thread:true
run函数开始----
Thread.currentThread().getName()线程:A
Thread.currentThread().isAlive()线程是否存活:true
this.getName()线程:Thread-0
this.isAlive()线程是否存活:false
run函数结束----
currentThread()方法
currentThread()方法返回代码段正在被哪个线程调用的信息。
下面我们测试 this 和 Thread.currentThread 之间的差异:
public static class myThread extends Thread {
private int index = 3;
public myThread() {
System.out.println("构造函数开始------------------------");
System.out.println("Thread.currentThread().getName()线程:" + Thread.currentThread().getName());
System.out.println("this.getName()线程:" + this.getName());
System.out.println("Thread.currentThread() == this):" + (Thread.currentThread() == this));
System.out.println("构造函数结束------------------------");
}
@Override
public void run() {
System.out.println("run函数开始------------------------");
System.out.println("Thread.currentThread().getName()线程:" + Thread.currentThread().getName());
System.out.println("this.getName()线程:" + this.getName());
System.out.println("Thread.currentThread() == this):" + (Thread.currentThread() == this));
System.out.println("run函数结束------------------------");
;
}
}
public static void main(String[] args) throws InterruptedException{
myThread myThread = new myThread();
Thread thread = new Thread(myThread);
thread.setName("A");
thread.start();
}
输出结果:
构造函数开始------------------------
Thread.currentThread().getName()线程:main
this.getName()线程:Thread-0
Thread.currentThread() == this):false
构造函数结束------------------------
run函数开始------------------------
Thread.currentThread().getName()线程:A
this.getName()线程:Thread-0
Thread.currentThread() == this):false
run函数结束------------------------
倘若我们是将线程对象myThread以构造参数的方式传递给Thread对象后进行start()方法,则在Thread的run方法中调用的是target.run()方法。即myThread.run()。
public void run() {
if (target != null) {
target.run();
}
}
此时Thread.currentThread()是Thread的引用thread,而this是myThread的引用。
而如果是直接调用线程对象myThread的start()方法,则 Thread.currentThread() 和 this 都是 myThread的引用
public static void main(String[] args) throws InterruptedException{
myThread myThread = new myThread();
myThread.setName("A");
myThread.start();
}
sleep()方法
该方法能在指定的毫秒数内让 当前正在执行的线程 休眠。
此处”当前正在执行的线程“ 是指 this.currentThread()返回的线程
interruop()方法
该方法是将当前线程标记上停止的标志(但不会直接停止)。下面测试是否会停止线程:
public static class MYThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 500000; i++)
System.out.println("i= " + (i + 1));
}
}
public static void main(String[] args) {
try {
MYThread tmp = new myThread.MYThread();
tmp.start();
Thread.sleep(1000);
tmp.interrupt();
}catch (InterruptedException e) {
System.out.println("抓住");
e.printStackTrace();
}
}
输出为打印1~500000。因此调用 interrupt()方法并没有停止线程。
判断线程是否是停止状态
Thread类提供了两种方法来判断:
this.interrupted():测试当前线程是否已中断,该方法是静态方法。(当前线程是指运行this.interrupted()方法的线程)
this.isinterrupted():测试线程是否已中断
我们来看第一种方法 interrupted()
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
System.out.println("i= " + i);
}
}
}
public class Thread_Test {
public static void main(String[] args) {
try {
MyThread tmp = new MyThread();
tmp.start();
tmp.interrupt();
Thread.sleep(10);
// Thread.currentThread().interrupt();
System.out.println("是否已停止:" + tmp.interrupted());
System.out.println("是否已停止:" + tmp.interrupted());
}catch (InterruptedException e) {
System.out.println("抓住");
e.printStackTrace();
}
}
}
输出:
i= 216
i= 217
是否已停止:false
是否已停止:false
i= 218
i= 219
i= 220
由此可知,这个”当前线程“是main,他从未中断过,因此为false。
为了使main线程产生中断效果,修改以下测试函数
public class Thread_Test {
public static void main(String[] args) {
try {
MyThread tmp = new MyThread();
tmp.start();
Thread.sleep(10);
Thread.currentThread().interrupt(); // 修改
System.out.println("是否已停止:" + tmp.interrupted());
System.out.println("是否已停止:" + tmp.interrupted());
}catch (InterruptedException e) {
System.out.println("抓住");
e.printStackTrace();
}
}
}
输出片段为:
i= 338
是否已停止:true
是否已停止:false
i= 339
至于为什么是第二个布尔值是false。参考一下官方文档对interrupted()方法的解释:
该方法测试当前线程是否已经中断,并清除该线程中断状态。
也就是说,如果连续两次调用该方法,由于第一次调用会清除其中断状态,第二次则会返回false(排除这两次之间线程再次中断的情况)
(2)isInterrupted()方法
isInterrupted()方法 不会清除状态标志,因此会打印两个true。
public class Thread_Test {
public static void main(String[] args) {
try {
MyThread tmp = new MyThread();
tmp.start();
Thread.sleep(10);
tmp.interrupt();
System.out.println("是否已停止:" + tmp.isInterrupted());
System.out.println("是否已停止:" + tmp.isInterrupted());
}catch (InterruptedException e) {
System.out.println("抓住");
e.printStackTrace();
}
}
}
输出为:
i= 303
是否已停止:true
是否已停止:true
i= 304
能停止的线程——异常法
我们可以在线程中用for语句判断线程是否是停止状态,若是,则抛出异常,后面代码不再执行。
public class MyThread extends Thread {
@Override
public void run() {
try {
for(int i = 0; i < 10000; i++) {
if(this.interrupted()) {
System.out.println("已是停止状态");
throw new InterruptedException();
}
System.out.println("i= " + i);
}
System.out.println("for语句下方");
}catch (InterruptedException e ) {
System.out.println("进入MyThread类中的catch");
e.printStackTrace();
}
}
}
public class Thread_Test {
public static void main(String[] args) {
try {
MyThread tmp = new MyThread();
tmp.start();
Thread.sleep(10);
tmp.interrupt();
}catch (InterruptedException e) {
System.out.println("抓住");
e.printStackTrace();
}
System.out.println("结束----------");
}
}
输出:
i= 283
i= 284
i= 285
结束----------
已是停止状态
进入MyThread类中的catch
java.lang.InterruptedException
at Thread.MyThread.run(MyThread.java:10)
sleep()中被停止
倘若线程在sleep()状态下被停止,会是什么效果:
public class MyThread extends Thread {
// 线程负责睡眠200000毫秒
@Override
public void run() {
try {
System.out.println("run 开始");
Thread.sleep(200000);
System.out.println("run 结束");
}catch (InterruptedException e ) {
System.out.println("sllep 中被停止,进入catch:" + this.isInterrupted());
e.printStackTrace();
}
}
}
public class Thread_Test {
public static void main(String[] args) {
try {
MyThread tmp = new MyThread();
tmp.start();
Thread.sleep(200);
tmp.interrupt();
}catch (InterruptedException e) {
System.out.println("抓住");
e.printStackTrace();
}
System.out.println("结束----------");
}
}
输出为:
run 开始
sllep 中被停止,进入catch:false
结束----------
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Thread.MyThread.run(MyThread.java:15)
在sleep状态下停止某一线程,会抛出InterruptedException,并清除停止状态值(即变为false)。
如果是先interrupt()后再sleep(),则同样会进入catch语句。
return停止线程
将interrupt()方法与return结合使用可以达到停止线程的效果:
public class MyThread extends Thread {
@Override
public void run() {
while (true) {
if(this.isInterrupted()) {
System.out.println("停止");
return;
}
System.out.println("time:" + System.currentTimeMillis());
}
}
}
public class Thread_Test {
public static void main(String[] args) throws InterruptedException{
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
}
}
但还是建议使用 ”抛异常“的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止的事件得以传播。
yield()方法
yield()方法是放弃当前的CPU资源,将它让给其他任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
我们来通过运行时间来测试yield()方法的使用效果
public class MyThread extends Thread {
@Override
public void run() {
long begin = System.currentTimeMillis();
int count = 0;
for(int i = 0; i < 50000; i++) {
// Thread.yield();
count++;
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - begin));
}
}
public class Thread_Test {
public static void main(String[] args) throws InterruptedException{
MyThread thread = new MyThread();
thread.start();
}
}
将取消注释的程序结果和未取消注释的程序结果来对比,发现将CPU让给其他资源导致速度变慢(取消注释)
线程优先级
线程剋划分优先级,优先级高的线程得到的CPU资源较多,也就是说CPU优先执行优先级较高的线程对象中的任务。
我们通过 setPriority()方法设置线程优先级。来测试下优先级带来的效果:
public class MyThread extends Thread {
@Override
public void run() {
long begin = System.currentTimeMillis();
long res = 0;
for(int j = 0; j < 10; j++) {
for(int i = 0; i < 50000; i++) {
Random random = new Random();
random.nextInt();
res++;
}
}
long end = System.currentTimeMillis();
System.out.println("-------------------线程1用时:" + (end - begin));
}
}
public class MyThread2 extends Thread{
@Override
public void run() {
long begin = System.currentTimeMillis();
long res = 0;
for(int j = 0; j < 10; j++) {
for(int i = 0; i < 50000; i++) {
Random random = new Random();
random.nextInt();
res++;
}
}
long end = System.currentTimeMillis();
System.out.println("-------------------线程2用时:" + (end - begin));
}
}
public class Thread_Test {
public static void main(String[] args) throws InterruptedException{
for(int i = 0; i < 5; i++) {
MyThread t1 = new MyThread();
t1.setPriority(10);
t1.start();
MyThread2 t2 = new MyThread2();
t2.setPriority(1);
t2.start();
}
}
}
从输出结果来看,较高优先级MyThread对象总是大部分先执行完,但不代表它全部先执行完。
同时,优先级较高的线程不一定每一次都先执行完run()方法
守护线程
Java线程有两种线程:用户线程和守护线程。当进程中不存在非守护线程,守护线程会自动销毁。
守护线程最典型的应用就是GC(垃圾回收器)
通过setDaemon(true)设置为守护线程,下面我们将 daemonThread 对象设置为守护线程
public class daemonThread extends Thread{
private int index = 0;
@Override
public void run() {
try {
while (true) {
index++;
System.out.println("index= " + index);
Thread.sleep(1000);
}
}catch (InterruptedException e) {e.printStackTrace();}
}
}
public class Thread_Test {
public static void main(String[] args) throws InterruptedException{
try {
daemonThread mt = new daemonThread();
mt.setDaemon(true);
mt.start();
Thread.sleep(5000);
System.out.println("停止");
}catch (InterruptedException e) {e.printStackTrace();}
}
}
线程组
线程组的作用是可以批量管理线程或线程组对象。我们可以把线程归属到某一个线程组中,而线程组里可以有线程对象,也可以有线程组。
main线程所在线程组的父线程组是system
取出线程组里的线程
public class Run {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("A");
System.out.println(group.activeCount());
ThreadA Aa = new ThreadA(group, "Aa");
Aa.start();
Thread[] threads = new Thread[group.activeCount()];
group.enumerate(threads);
for(int i = 0; i < threads.length; i++) {
System.out.println(threads[i].getName());
}
}
}
线程组自动归属特性
在实例化一个线程组group时,若不指定所属的线程组,则它会自动归到当前线程对象所属的线程组,即在这个线程组添加子线程组group。
public class test {
// activeGroupCount() 取得当前线程组对象中的子线程组数量。
// enumerate() 将线程组中的子线程以复制的形式拷贝到groupList中。
public static void main(String[] args) {
System.out.println("当前线程:" + Thread.currentThread().getName() + "所属的线程组名为:" +
Thread.currentThread().getThreadGroup().getName() + ",该组有线程组数量:" +
Thread.currentThread().getThreadGroup().activeGroupCount());
ThreadGroup group = new ThreadGroup("新的组"); //他会自动加到main组
System.out.println("当前线程:" + Thread.currentThread().getName() + "所属的线程组名为:" +
Thread.currentThread().getThreadGroup().getName() + ",该组有线程组数量:" +
Thread.currentThread().getThreadGroup().activeGroupCount());
ThreadGroup[] groupList = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
Thread.currentThread().getThreadGroup().enumerate(groupList);
for(int i = 0; i < groupList.length; i++) {
System.out.println("线程组名称:" + groupList[i].getName());
}
}
}
输出:
线程组内的线程批量停止
当调用线程组group的Interrupt()方法时,可以将该组中所有正在运行的线程批量停止。
1级关联
1级关联是指:父对象有子对象,但不创建子孙对象。比如:在创建一些线程时,为了有效管理这些线程,通常我们创建一个线程组,将这些线程归属组内。
public class ThreadA extends Thread {
@Override
public void run() {
try {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程名:" + Thread.currentThread().getName());
Thread.sleep(3000);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadB extends Thread {
@Override
public void run() {
try {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程名:" + Thread.currentThread().getName());
Thread.sleep(3000);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) {
ThreadA ta = new ThreadA();
ThreadB tb = new ThreadB();
ThreadGroup group = new ThreadGroup("线程组");
Thread threadA = new Thread(group, ta);
Thread threadB = new Thread(group, tb);
threadA.setName("threadA"); threadB.setName("threadB");
threadA.start();
threadB.start();
System.out.println("活动的线程数:" + group.activeCount());
System.out.println("线程组名称:" + group.getName());
}
}
输出:
Thread(ThreadGroup group, Runnable target)
以target的run方法作为线程执行体创建新线程,属于group组
多级关联
父对象有子对象,子对象创建创建其子对象。
测试:在main线程组中添加一个线程组A。然后在A组添加线程对象Z。
public class Run {
public static void main(String[] args) {
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
ThreadGroup group = new ThreadGroup(mainGroup, "A");
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("执行方法");
Thread.sleep(10000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread newThread = new Thread(group, runnable);
newThread.setName("Z");
newThread.start(); //线程必须启动后才会归到组A中
ThreadGroup[] groupList = new ThreadGroup[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(groupList);
System.out.println("main线程有多少个子线程组:" + groupList.length + " 名字为:" + groupList[0].getName());
Thread[] threadList = new Thread[groupList[0].activeCount()];
groupList[0].enumerate(threadList);
System.out.println(threadList[0].getName());
}
}
输出:
SimpleDateFormat 非线程安全
SimpleDateFormat 负责日期的转换与格式化,它不是线程安全的。因此在多线程环境下,容易造成数据转换及处理的不准确。
public class MyThread extends Thread {
private SimpleDateFormat sdf;
private String dateStr;
public MyThread(SimpleDateFormat sdf, String dateStr) {
this.sdf = sdf;
this.dateStr = dateStr;
}
@Override
public void run() {
try {
Date date = sdf.parse(dateStr);
String newDateStr = sdf.format(date).toString();
if(!newDateStr.equals(dateStr)) {
System.out.println(this.getName() + "报错:日期字符串 " + dateStr +
"转换成新的日期字符串 " + newDateStr);
}
}catch (ParseException e) {
System.out.println("报错");
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] dateStrArray = new String[] {"2000-01-01", "2000-01-01", "2100-11-01", "2021-01-23",
"2123-12-22"};
MyThread[] threads = new MyThread[5];
for(int i = 0; i < 5; i++) {
threads[i] = new MyThread(sdf, dateStrArray[i]);
threads[i].start();
}
}
}
输出:
Exception in thread "Thread-2" Exception in thread "Thread-4" Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at formatError.MyThread.run(MyThread.java:19)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at formatError.MyThread.run(MyThread.java:19)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at formatError.MyThread.run(MyThread.java:19)
Thread-3报错:日期字符串 2021-01-23转换成新的日期字符串 2000-01-01