首页 > 编程语言 >java线程的基本操作

java线程的基本操作

时间:2024-01-19 16:01:54浏览次数:30  
标签:状态 java Thread static 线程 基本操作 执行 public

1. 线程名称的设置和获取

  • 在Thread类中可以通过构造器Thread(...)初始化设置线程名称,

  • 也可以通过setName(...)实例方法去设置线程名称,取得线程名称可以通过getName()方法完成。

    关于线程名称有以下几个要点:

  1. 线程名称一般在启动线程前设置,但也允许为运行的线程设置名称

  2. 允许两个Thread对象有相同的名称,但是应该避免。

  3. 如果程序没有为线程指定名称,系统会自动为线程设置名称。

    public class ThreadNameDemo {
        private static final int MAX_TURN = 3;
        
        //异步执行目标类
        static class RunTarget implements Runnable {    // 实现Runnable接口
            public void run() {    
                for (int turn = 0; turn < MAX_TURN; turn++) {
                    // TODO
                }
            }
        }
        
        public static void main(String args[]) {
            RunTarget target = new RunTarget();    // 实例化Runnable异步执行目标类
            new Thread(target).start();        // 系统自动设置线程名称
            new Thread(target).start();        // 系统自动命令线程名称
            new Thread(target).start();        // 系统自动命令线程名称
            new Thread(target, "手动命名线程-A").start();        // 手动设置线程名称
            new Thread(target, "手动命名线程-B").start();        // 手动设置线程名称
        }
    };
    

2. 线程的 sleep 操作

  • sleep的作用是让目前正在执行的线程休眠,让CPU去执行其他的任务。从线程状态来说,就是从执行状态变成限时阻塞状态。sleep()方法定义在Thread类中,是一组静态方法,有两个重载版本:
public static void sleep(long millis) throws InterruptException //使目前正在执行的线程休眠millis笔秒
    
public static void sleep(long millis, int nanos) throws InterruptException://使目前正在执行的线程休眠millis毫秒,nanos纳秒

具体使用

public class SleepDemo {
    public static final int SLEEP_GAP = 5000; //睡眠时长
    public static final int MAX_TURN = 50; //睡眠次数

    static class SleepThread extends Thread {
        static int threadSeqNumber = 1;

        public SleepThread() {
            super("sleepThread-" + threadSeqNumber);
            threadSeqNumber++;
        }

        public void run() {
            try {
                for (int i = 1; i < MAX_TURN; i++) {
                    // TODO
                    // 线程睡眠一会
                    Thread.sleep(SLEEP_GAP);
                }
            } catch (InterruptedException e) {
         			// TODO      
            }
        }
    }
    public static void main(String args[]) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread thread = new SleepThread();
            thread.start();
        }
    }

image

  • 通过Jstack指令输出,可以看到在进行线程DUMP的时间点,所创建的sleepThread线程都处于TIMED_WAITIN状态。
  • 当线程睡眠时间满后,线程不一定会立即得到执行,因为此时CPU可能正在执行其他的任务线程首先是进入就绪状态,等待分配CPU时间片以便有机会执行。

3. 线程的 interrupt 操作

  • Java语言提供了stop()方法终止正在运行的线程,但是Java将Thread的stop()方法设置为过时,不建议大家使用。

    • 为什么呢? 因为使用stop()方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机。在程序中,我们是不能随便中断一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能被释放的问题、
    • 线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正是由于使用stop()方法来终止线程可能会产生不可预料的结果,因此并不推荐调用stop()方法。
  • 一个线程什么时候可以退出呢?当然只有线程自己才能知道。Thread的interrupt()方法本质不是用来中断一个线程,而是将线程设置为中断状态。当我们调用线程的interrupt()方法时,它有两个作用:

    1. 如果此线程处于阻塞状态(如调用了Object.wait()方法),就会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。更确切地说,如果线程被Object.wait()Thread.join()Thread.sleep()三种方法之一阻塞,此时调用该线程的interrupt()方法,该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而过早终结被阻塞状态

    2. 如果此线程正处于运行中,线程就不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。

如果线程的interrupt()方法先被调用,然后线程开始调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。

如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。

public class InterruptDemo {
    
    public static final int SLEEP_GAP = 5000;//睡眠时长
    public static final int MAX_TURN = 50;//睡眠次数
    
    static class SleepThread extends Thread {
        static int threadSeqNumber = 1;
        
        public SleepThread() {
            super("sleepThread-" + threadSeqNumber);
            threadSeqNumber++;
        }
        
        public void run() {
            try {
                // 线程睡眠一会
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                // 被打断
                e.printStackTrace();
                return;
            }
        }
        
    }
    
    public static void main(String args[]) throws InterruptedException {
        Thread thread1 = new SleepThread();
        thread1.start();
        Thread thread2 = new SleepThread();
        thread2.start();
        sleepSeconds(2);//等待2秒
        thread1.interrupt(); //打断线程1
        sleepSeconds(5);//等待5秒
        thread2.interrupt();  //打断线程2,此时线程2已经终止
        sleepSeconds(1);//等待1秒
    }
  1. sleepThread-1线程在大致睡眠了2秒后,被主线程打断(或者中断)。被打断的sleepThread-1线程停止睡眠,并捕获到InterruptedException受检异常。程序在异常处理时直接返回了,其后面的执行逻辑被跳过。

  2. sleepThread-2线程在睡眠了7秒后,被主线程中断,但是在sleepThread-2线程被中断的时候,其执行已经结束了,所以thread2.interrupt()中断操作没有发生实质性的效果。

  3. Thread.interrupt()方法并不像Thread.stop()方法那样中止一个正在运行的线程,其作用是设置线程的中断状态位为true,至于线程是死亡、等待新的任务还是继续运行至下一步,就取决于这个程序本身。线程可以不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。

  4. Thread.interrupt()方法只是改变中断状态,不会中断一个正在运行的线程,线程是否停止执行,需要用户程序去监视线程的isInterrupted()状态,并进行相应的处理。

  • 下面示例程序演示如何使用isInterrupted()实例方法监视线程的中断状态
public void testInterrupted2() {
        Thread thread = new Thread() {
            public void run() {
                //一直循环
                while (true) {
                    System.out.println(isInterrupted());
                    sleepMilliSeconds(SLEEP_GAP);
                    
                    //如果调用 interrupt 为true,退出死循环
                    if (isInterrupted()) {
                       	// TODO
                        return;
                    }
                }
            }
        };
        thread.start();
        sleepSeconds(2);//等待2秒
        thread.interrupt(); //打断线程1
        sleepSeconds(2);//等待2秒
        thread.interrupt();
}

4. 线程的 join 操作

  • 什么是线程的合并呢?

    • 假设有两个线程A和B,现在线程A在执行过程中对另一个线程B的执行有依赖,具体的依赖为:线程A需要线程B的执行流程合并到自己的执行流程中,这就是线程合并,被动方线程B可以叫作被合并线程。

      class ThreadA extends Thread {
          void run() {
            Thread threadb = new Thread("thread-b");
            threadb.join();  
          }
      }
      
      

4.1 线程的 join 操作的三个版本

// 重载版本1:此方法会把当前线程变为WAITING,直到被合并线程执行结束
public final void join() throws InterruptedException:
// 重载版本2:此方法会把当前线程变为TIMED_WAITING,直到被合并线程结束,或者等待被合并线程执行millis的时间
public final synchronized void join(long millis)throws InterruptedException:
// 重载版本3:此方法会把当前线程变为TIMED_WAITING,直到被合并线程结束,或者等待被合并线程执行millis+nanos的时间
public final synchronized void ioin(long millis,int nanos)throws InterruptedException:
  • join()方法是实例方法,需要使用被合并线程的句柄(或者指针、变量)去调用,如threadb.join()。执行threadb.join()这行代码的当前线程为合并线程(甲方),进入TIMED_WAITING等待状态,让出CPU。
  • 如果设置了被合并线程的执行时间millis(或者milis+nanos),并不能保证当前线程一定会在millis时间后变为RUNNABLE。
  • 如果主动方合并线程等待时被中断,就会抛出InterruptedException受检异常。

image

如果乙方线程无限制长时间地执行,甲方线程可以进行限时等待。

甲方线程等待乙方线程执行一定的时间后,如果乙方还没有完成,甲方线程再继续执行。

使用join()方法的优势是比较简单的,劣势是join()方法没有办法直接取得乙方线程的执行结果。

4.2 join运行结果

public class JoinDemo {
    public static final int SLEEP_GAP = 5000;//睡眠时长
    public static final int MAX_TURN = 50;//睡眠次数
    
    static class SleepThread extends Thread {
        static int threadSeqNumber = 1;
        
        public SleepThread() {
            super("sleepThread-" + threadSeqNumber);
            threadSeqNumber++;
        }
        
        public void run() {
            try {
                // 线程睡眠一会
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
        }
        
    }
    
    public static void main(String args[]) {
        Thread thread1 = new SleepThread();
        sleepSeconds(10);
        thread1.start();
        try {
            thread1.join();//合并线程1,不限时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        sleepSeconds(10);
        //启动第二条线程,并且进行限时合并,等待时间为1秒
        Thread thread2 = new SleepThread();
        thread2.start();
        try {
            thread2.join(2000);//限时合并,限时1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

image

4.3 join 线程的 WAITING 状态

  • 线程的WAITING(等待)状态表示线程在等待被唤醒。处于WAITING状态的线程不会被分配CPU时间片。执行以下两个操作,当前线程将处于WAITING状态:
    1. 执行没有时限(timeout)参数的thread.join()调用:在线程合并场景中,若线程A调用B.join()去合入B线程,则在B执行期间线程A处于WAITING状态,一直等线程B执行完成。
    2. 执行没有时限参数的Object.wait()调用: 指一个拥有object对象锁的线程,进入到相应的代码临界区后,调用相应的objectwait()方法去等待其对象锁(Obiect Monitor)上的信号,若对象锁上没有信号,则当前线程处于WAITING状态。

image

4.4 join 线程的 TIMED_WAITING 状态

  • 线程的TIMED_WAITING状态表示在等待唤醒。处于TIMED_WAITING状态的线程不会被分配CPU时间片,它们要等待被唤醒,或者直到待的时限到期。
  • 在线程合入场景中,若线程A在调用B.join()操作时加入了时限参数,则在B执行期间线程A处于TIMED_WAITING状态。若B在等待时限内没有返回,则线程A结束等待TIMED_WAITING状态,恢复成RUNNABLE状态。

5. 线程的 yield 操作

  • 线程的yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行让出CPU的执行权限,使得CPU去执行其他的线程。
  • 处于让步状态的JVM层面的线程状态仍然是RUNNABLE状态,但是该线程所对应的操作系统层面的线程从状态上来说会从执行状态变成就绪状态
  • 线程在yield时,线程放弃和重占CPU的时间是不确定的,可能是刚刚放弃CPU,马上又获得CPU执行权限,重
    新开始执行。
  • yield()方法是Thread类提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次。

public class YieldDemo {
    public static final int MAX_TURN = 100;//执行次数
    public static AtomicInteger index = new AtomicInteger(0);//执行编号
    
    // 记录线程的执行次数
    private static Map<String, AtomicInteger> metric = new HashMap<>();
    
    //输出线程的执行次数
    private static void printMetric() {
        System.out.println("metric = " + metric);
    }
    
    static class YieldThread extends Thread {
        static int threadSeqNumber = 1;
        
        public YieldThread() {
            super("YieldThread-" + threadSeqNumber);
            threadSeqNumber++;
            metric.put(this.getName(), new AtomicInteger(0));
        }
        
        public void run() {
            
            for (int i = 1; i < MAX_TURN && index.get() < MAX_TURN; i++) {
                System.out.println("线程优先级:" + getPriority());
                index.incrementAndGet();
                metric.get(this.getName()).incrementAndGet();
                if (i % 2 == 0) {
                    //让步:出让执行的权限
                    Thread.yield();
                }
            }
            //输出线程的执行次数
            printMetric();
            System.out.println(getName() + " 运行结束.");
        }
    }
 
     public static void main(String[] args) {
        Thread thread1 = new YieldThread();
        thread1.setPriority(Thread.MAX_PRIORITY);
        Thread thread2 = new YieldThread();
        thread2.setPriority(Thread.MIN_PRIORITY);
        System.out.println("启动线程.");
        thread1.start();
        thread2.start();
        sleepSeconds(100);
    }
}

image

  • 启动两个让步线程,两个线程每执行两次操作就让出CPU。

  • 两个线程的优先级有区别,YieldThread-1的优先级为Thread.MAX_PRIORITY(值为10),YieldThread-2的优先级为Thread.MIN_PRIORITY(值为1),优先级高的YieldThread-1执行的次数比优先级低的YieldThread-2在执行的次数多很多。

    线程调用yield之后,操作系统在重新进行线程调度时偏向于将执行机会让给优先级较高的线程。

    1. yield仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。
    2. yield不能保证使得当前正在运行的线程迅速转换到就绪状态。
    3. 即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不被选中,其调度的过程受到其他因素(如优先级)的影响。

6. 线程的 daemon 操作

  • Java中的线程分为两类:守护线程与用户线程。
    • 守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行着一系列的GC(垃圾回收)线程,这些GC线程就是守护线程,提供幕后的垃圾回收服务。

image

只要JVM实例中尚存在任何一个用户线程没有结束,守护线程就能执行自己工作;

只有当最后一个用户线程结束,守护线程随着JVM一同结束工作。

6.1 守护线程的基本操作

  • 在Thread类中,有一个实例属性两个实例方法,专门用于进行守护线程相关的操作。

    1. 实例属性daemon:保存一个Thread线程实例的守护状态,默认为false,表示线程默认为用户线程

      private boolean daemon = false;
      
    2. 实例方法setDaemon(...):此方法将线程标记为守护线程或者用户线程。

      • setDaemon(true)将线程设置为守护线程
      • setDaemon(false)将线程设置为用户线程。
      public final void setDaemon(boolean on);
      
    3. 实例方法isDaemon():获取线程的守护状态,用于判断该线程是不是守护线程。

      public final boolean isDaemon();
      

6.2 守护线程与用户线程的关系

  • 从是否为守护线程的角度,对Java线程进行分类,分为用户线程和守护线程。
    • 守护线程和用户线程的本质区别: 二者与JVM虚拟机进程终止的方向不同。
    • 用户线程和JVM进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止
    • 守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止。

image

  • 守护线程提供服务,是守护者,用户线程享受服务,是被守护者。只有全部的用户线程终止了,相当于没有了被守护者,守护线程也就没有工作可做了,也就可以全部终止了。
  • 用户线程全部终止,JVM进程也就没有继续的必要了。反过来说,只要有一个用户线程没有终止,JVM进程也不会退出。但是在终止维度上,守护线程和JVM进程没有主动关系。也就是说,哪怕是守护线程全部被终止,JVM虚拟机也不一定终止。

6.3 守护线程的要点

  1. 守护线程必须在启动前将其守护状态设置为true,启动之后不能再将用户线程设置为守护线程,否则JVM会抛出一个InterruptedException异常。如果线程为守护线程,就必须在线程实例的start()方法调用之前调用线程实例的setDaemon(true),设置其daemon实例属性值为true。
  2. 守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。
  3. 守护线程创建的线程也是守护线程。在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显式地设置为用户线程,新的线程可以调整成用户线程。

7. 线程状态小结

7.1NEW 状态

  • 通过new Thread(...)已经创建线程,但尚未调用start()启动线程,该线程处于NEW(新建)状态。

7.2 RUNNABLE 状态

  • Java把Ready(就绪)和Running(执行)两种状态合并为一种状态:RUNNABLE(可执行)状态(或者可运行状态)。

  • 调用了线程的start()实例方法后,线程就处于就绪状态。此线程获取到CPU时间片后,开始执行run()方法中的业务代码,线程处于执行状态。

    1. 就绪状态, 就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序选中,线程就永远是就绪状态,当前线程进入就绪状态的条件大致包括以下几种:

      • 调用线程的start()方法,此线程进入就绪状态
      • 当前线程的执行时间片用完。
      • 线程睡眠(sleep)操作结束。
      • 对其他线程合入(join)操作结束。
      • 等待用户输入结束。
      • 线程争抢到对象锁(Obiect Monitor)。
      • 当前线程调用了yield()方法出让CPU执行权限。
    2. 执行状态, 线程调度程序从就绪状态的线程中选择一个线程,被选中的线程状态将变成执行状态。这也是线程进入执行状态的唯一方式。

    3. BLOCKED 状态: 处于BLOCKED(阻塞)状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:

      • 线程等待获取锁,等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
      • IO阻塞,线程发起了一个阻塞式IO操作后,如果不具备IO操作的条件,线程就会进入阻塞状态。IO包括磁盘IO网络IO等。IO阻塞的一个简单例子: 线程等待用户输入内容后继续执行。
    4. WAITING 状态,处于WAITING(无限期等待)状态的线程不会被分配CPU时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下3种方法让自己进入无限等待状态:

    • Object.wait()方法,对应的唤醒方式为: Object.notify()/Object.notifyAll()
    • Thread.join()方法,对应的唤醒方式为: 被合入的线程执行完毕
    • LockSupport.park()方法,对应的唤醒方式为: LockSupport.unpark(Thread)
    1. TIMED_WAITING 状态,处于TIMED_WAITING(限时等待)状态的线程不会被分配CPU时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下3种方法会让线程进入限时等待状态:
    • Thread.sleep(time)方法,对应的唤醒方式为: sleep睡眠时间结束
    • Object.wait(time)方法,对应的唤醒方式为: 调用 Object.notify()/Obiect.notifyAll()去主动唤醒,或者限时结束。
    • LockSupport.parkNanos(time)/parkUntil(time)方法,对应的唤醒方式为: 线程调用配套的 LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。

    进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权,另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。

    1. TERMINATED 状态

      • 线程结束任务之后,将会正常进入TERMINATED(死亡)状态;
      • 线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

标签:状态,java,Thread,static,线程,基本操作,执行,public
From: https://www.cnblogs.com/ccblblog/p/17974844

相关文章

  • Java开发之Java8 新特性--流式数据处理学习
    一.流式处理简介在我接触到java8流式处理的时候,我的第一感觉是流式处理让集合操作变得简洁了许多,通常我们需要多行代码才能完成的操作,借助于流式处理可以在一行中实现。比如我们希望对一个包含整数的集合中筛选出所有的偶数,并将其封装成为一个新的List返回,那么在java8之前,我们需......
  • 线程和进程
    进程和线程是操作系统中的两个基本概念,他们都是用来完成执行任务的,但是有所区别。进程是资源分配的最小单位,它代表CPU所能处理的单个任务。每个进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。而线程是进程中执......
  • Java实现基于GDAL将单波段影像转为三波段影像-唯一值渲染
    在处理遥感影像的渲染时,经常需要处理单波段影像。单波段影像没有任何颜色,只有一个波段的值。渲染时只能采用色带拉伸、离散颜色、唯一值渲染这几种方式。直接将单波段影像转成三波段的影像,并将三个波段转为颜色对应的rgb值,这样可以加速渲染、切片的过程。这里我有一张单波段影像,需......
  • qt和java的socket连接
    事先说明qt为客户端(发出请求)java为服务端(处理请求)关于qt的客户端来说我们大体上要完成三个需求,即请求连接,发送,接收请求连接如果想使用qt写socket程序,首先需要在.pro文件中添加QT+=network;(非常非常重要)接收然后我们就可以在代码中使用QT的网络库了,socket涉及到的函数库......
  • java线程核心原理
    1.线程的调度与时间片1.1java线程与操作系统现代操作系统(如Windows、Linux、Solaris)提供了强大的线程管理能力,Java不需要再进行自己独立的线程管理和调度,而是将线程调度工作委托给操作系统的调度进程去完成。在某些系统(比如Solaris操作系统)上,JVM甚至将每个Java线程一对一......
  • 深入理解JavaScript堆栈、事件循环、执行上下文、作用域以及闭包
    合集-JavaScript进阶系列(5) 1.JavaScriptthis绑定详解01-092.JavaScriptapply、call、bind函数详解01-093.JavaScriptforEach方法跳出循环01-024.深入理解JavaScript堆栈、事件循环、执行上下文和作用域以及闭包01-105.JavaScript到底应不应该加分号?JavaScript自......
  • java多态
    有两个类,一个Animal类,一个Cat类,其中Cat是Animal的子类,此时我在主函数中这样声明一个对象"Animalanimal=newCat();",此时animal实际上是Cat类此时,Animal类中没有catMouse()这个方法,Cat类中有这个方法,我在主函数声明了"Animalanimal=newCat();"后,无法调用animal.catchMouse();......
  • java相似度算法计算
     publicclassCompareStrSimUtil{privatestaticintcompare(Stringstr,Stringtarget,booleanisIgnore){intd[][];//矩阵intn=str.length();intm=target.length();inti;//遍历str的intj;//遍历......
  • HanLP — 汉字转拼音 -- JAVA
    目录语料库训练加载语料库训练模型保存模型加载模型计算调用HanLP在汉字转拼音时,可以解决多音字问题,显示输出声调,声母、韵母,通过训练语料库,本文代码为《自然语言处理入门》配套版本HanLP-1.7.5对重载不是重任进行转拼音,效果如下:原文:重载不是重任拼音(数字音调):chong2,zai3,bu......
  • docker构建java镜像,运行镜像出现 no main manifest attribute, in /xxx.jar
    背景本文主要是一个随笔,记录一下出现"nomainmanifestattribute"的解决办法问题原因主要是近期在构建一个镜像,在镜像构建成功后,运行一直提示"nomainmanifestattribute",但是还在想,是不是Dockerfile写错了,后来仔细检查了一下,发现是在pom文件下build节点下配置问题,修改配置......