多线程
关于
关于线程池可以前往 =====》https://www.cnblogs.com/nicechen/p/15264499.html
环境说明: idea、java8、maven
第一章 并发简介
01 并发编程的挑战之频繁的上下文切换
cpu为线程分配时间片,时间片非常短(毫秒级别),cpu不停的切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行的。
线程越多,上下文切换越频繁,带来一定的性能开销。多线程竞争锁时,会引起上下文切换。
如何减少上下文切换的开销?
1、无锁并发编程
多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
2、CAS
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
3、使用最少线程
避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
4、协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。--GO语言用的多,java用的少(技术不成熟)
02 并发编程的挑战之死锁
示例:
package com.xdclass.synopsis; /** * 死锁Demo */ public class DeadLockDemo { private static final Object HAIR_A = new Object(); private static final Object HAIR_B = new Object(); public static void main(String[] args) { new Thread(()->{ synchronized (HAIR_A) { try { Thread.sleep(50L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (HAIR_B) { System.out.println("A成功的抓住B的头发"); } } }).start(); new Thread(()->{ synchronized (HAIR_B) { synchronized (HAIR_A) { System.out.println("B成功抓到A的头发"); } } }).start(); } }
03 并发编程的挑战之线程安全
一开始某个线程读取 num = 0 ,准备调用 num++ 时(还未调用),cpu 时间片切换到下一个线程,这个线程读取 num 也是为 0 ,所以这两个线程都调用 num++ 完毕后, num = 1。
自己的理解:主要是看这两个线程开始时,所读取的 num 的值,如果这两者读取的 num 值都为 0 那么调用 num++ 后 num 还是为 1 。如果在 cpu 切换时间片之前,线程一已经调用完毕 num++ 了,此时 num = 1,cpu 切换到线程二所读取 num 的值就为 1 ,之后再调用 num++,最后 num 才会等于 2 。
package com.xdclass.synopsis; import java.util.concurrent.CountDownLatch; /** * 线程不安全操作代码实例 */ public class UnSafeThread { private static int num = 0; private static CountDownLatch countDownLatch = new CountDownLatch(10);//后面讲 /** * 每次调用对num进行++操作 */ public static void inCreate() { num++; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ for (int j = 0; j < 100; j++) { inCreate(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } //每个线程执行完成之后,调用countdownLatch countDownLatch.countDown(); }).start(); } while (true) { if (countDownLatch.getCount() == 0) { System.out.println(num); break; } } } }
第二章----线程基础
01 进程与线程的区别
进程:是系统进行分配和管理资源的基本单位
线程:进程的一个执行单元,是进程内调度的实体、是CPU调度和分派的基本单位,是比进程更小的独立运行的基本单位。线程也被称为轻量级进程,线程是程序执行的最小单位。
一个程序至少一个进程,一个进程至少一个线程。
02 线程的状态及其相互转换
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE):处于可运行状态的线程正在JVM中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
阻塞(BLOCKED):线程阻塞于synchronized锁,等待获取synchronized锁的状态。
等待(WAITING):Object.wait()、join()、 LockSupport.park(),进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIME_WAITING):Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil,该状态不同于WAITING,
它可以在指定的时间内自行返回。
终止(TERMINATED):表示该线程已经执行完毕。
03 创建线程的方式
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池
使用Callable:
public class CallableDemo implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "nice"; } public static void main(String[] args) throws ExecutionException, InterruptedException { CallableDemo callableDemo = new CallableDemo(); FutureTask<String> futureTask = new FutureTask<>(callableDemo); new Thread(futureTask).start(); System.out.println(futureTask.get()); } }
05 线程的挂起跟恢复
1、什么是挂起线程?
线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。在线程挂起后,可以通过重新唤醒线程来使之恢复运行。
2、为什么要挂起线程?
cpu分配的时间片非常短、同时也非常珍贵。避免资源的浪费。
3、如何挂起线程?
被废弃的方法:
thread.suspend() 该方法不会释放线程所占用的资源。如果使用该方法将某个线程挂起,则可能会使其他等待资源的线程死锁。
thread.resume() 方法本身并无问题,但是不能独立于suspend()方法存在。
演示:(已被废弃,不再使用)
public class Test implements Runnable { private Object object = new Object(); @Override public void run() { synchronized (object){ System.out.println(Thread.currentThread().getId()+" :挂起"); Thread.currentThread().suspend(); } System.out.println("释放资源"); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new Test(), "Thread1"); thread1.start(); Thread.sleep(1000);//因为主线程运行比线程运行速度快,当线程还未被挂起时,就运行了 resume() 是没用的。 thread1.resume(); Thread thread2 = new Thread(new Test(), "Thread2"); thread2.start(); thread2.resume(); Thread.sleep(1000); thread1.resume(); } }
4、可以使用的方法:
wait() 暂停执行、放弃已经获得的锁、进入等待状态;
notify() 随机唤醒一个在等待锁的线程;
notifyAll() 唤醒所有在等待锁的线程,自行抢占cpu资源;
........
示例1: currentThread()方法返回正在被执行的线程的信息。注意事项:main线程是主线程,和main方法没有关系。
public class Test implements Runnable { private static Object object = new Object(); @Override public void run() { synchronized (object){ System.out.println(Thread.currentThread().getId()+" :挂起"); try { //注意:需要使用 synchronized 中的锁调用 wait() 方法,调用 wait() 的线程会释放所占用的锁 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("释放资源"); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new Test(), "Thread1"); thread1.start(); Thread.sleep(5000); //注意:唤醒线程方法需要使用在 synchronized 中,并且他的锁必须与要唤醒线程的锁一致,还需要该锁来调用 notify() 方法来唤醒 synchronized (object){ object.notify(); } } }
示例2:
public class Test implements Runnable { private static Object object = new Object(); @Override public void run() { synchronized (object){ System.out.println(Thread.currentThread().getId()+" :挂起"); try { //注意:需要使用 synchronized 中的锁调用 wait() 方法,调用 wait() 的线程会释放所占用的锁 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("释放资源"); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new Test(), "Thread1"); thread1.start(); Thread thread2 = new Thread(new Test(), "Thread2"); thread2.start(); Thread.sleep(5000); synchronized (object){ /* object.notify(); //只会随机唤醒一个线程 object.notify(); */ object.notifyAll(); //唤醒该锁下的所有线程 } } }
06 线程的中断操作
- stop() 废弃方法,开发中不要使用。因为一调用,线程就立刻停止,此时有可能引发相应的线程安全性问题;
- Thread.interrupt方法;
- 自行定义一个标志,用来判断是否继续执行。
1、interrupt()方法:
1)其作用是中断此线程,它可以中断使用 lockInterruptibly() 获取锁,并且正在运行的线程,也可以中断使用 lockInterruptibly() 等待获取锁的线程。但是 interrupt()不能中断使用 lock() 等待获取锁的线程,也不能中断使用 lock() 获取锁,并正在运行的线程,除非,这个线程运行到了 Thread.sleep(),他会抛出异常,而执行 finally 中的 unlock() 释放锁。
2)上代码:
示例一(lock()):
public class LockDemo1 { public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); lock.lockInterruptibly(); System.out.println(Thread.currentThread().getName()+"我已获取到锁"); Thread thread = new Thread(() -> { try { lock.lock(); System.out.println(Thread.currentThread().getName() + ":我已获取到锁"); System.out.println("执行了一定的逻辑1"); //执行 Thread.sleep(3000); 会抛出异常 Thread.sleep(3000); System.out.println("执行了一定的逻辑2"); } catch (Exception e) { System.out.println(lock.tryLock()); System.out.println(Thread.currentThread().getName() + ":我已放弃获取锁"); } finally { if (lock.tryLock()) lock.unlock(); } }); thread.start(); Thread.sleep(4000); thread.interrupt(); System.out.println("主线程执行了 thread.interrupt()"); lock.unlock(); } }
运行结果:
示例二(lockInterruptibly()):
public class LockDemo2 { public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); Thread thread = new Thread(() -> { try{ lock.lockInterruptibly(); while (true){ System.out.println(Thread.currentThread().getName()); Thread.sleep(200); } }catch (Exception e){ System.out.println(lock.tryLock()); }finally { lock.unlock(); } }); thread.start(); Thread.sleep(3000); thread.interrupt(); System.out.println("主线程中断了 thread"); } }
运行结果:
2、interrupted()方法:
作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。
3、isInterrupted()方法:
作用是测试此线程是否被中断 ,不清除中断状态。
07 线程的优先级
- 线程的优先级告诉程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,程序会尽可能地先运行优先级的那个线程。 但是,这并不表示优先级较低的线程不会运行。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。
- 线程的优先级设置可以为1-10的任一数值,Thread类中定义了三个线程优先级,分别是:
- MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),一般情况下推荐使用这几个常量,不要自行设置数值。
- 不同平台,对线程的优先级的支持不同。 编程的时候,不要过度依赖线程优先级,如果你的程序运行是否正确取决于你设置的优先级是否按所设置的优先级运行,那这样的程序不正确
任务:
- 快速处理:设置高的优先级
- 慢慢处理:设置低的优先级
示例:
public class Test extends Thread{ public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName()); } }, "线程一"); Thread thread1 = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName()); } },"线程二"); thread.start(); thread1.start(); //设置优先级 thread.setPriority(Thread.MAX_PRIORITY); thread1.setPriority(Thread.NORM_PRIORITY); Thread.sleep(300); thread.interrupt(); thread1.interrupt(); } }
08 守护线程
线程分类:
用户线程、守护线程
用户线程:调用 start() 方法启动,并且 setDaemon(false) 方法参数设置为 false(默认为 false)的线程。
守护线程:任何一个守护线程都是整个程序中所有用户线程的守护者,只要有活着的用户线程,守护线程就活着。当JVM实例中最后一个非守护线程结束时,也随JVM一起退出。也就是说,当用户线程和主线程(main)终止后,守护线程才终止。如果没有用户线程,则主线程(main)终止后,守护线程才终止。
守护线程的用处:jvm垃圾清理线程
建议:
- 尽量少使用守护线程,因其不可控
- 不要在守护线程里去进行读写操作、执行计算逻辑
示例:
public class Test extends Thread{ public static void main(String[] args) throws InterruptedException { //守护线程 Thread thread = new Thread(() -> { while (true) { System.out.println(Thread.currentThread().getName()); } }, "线程一"); Thread thread1 = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName()); } },"线程二"); //必须在 start() 方法前设置为守护线程 thread.setDaemon(true); thread.start(); thread1.start(); Thread.sleep(1000); thread1.interrupt(); } }
第三章----线程安全性问题
01 什么是线程安全性?
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。
----《并发编程实战》
什么是线程不安全?
多线程并发访问时,得不到正确的结果。
02 原子性操作
什么是原子性操作:
一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。
那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。
此时A的帐户仍然有3000块,B的帐户仍然有2000块。
通俗点讲:操作要成功一起成功、要失败大家一起失败
如何把非原子性操作变成原子性?
volatile关键字仅仅保证可见性,并不保证原子性
synchronize关机字,使得操作具有原子性
03 深入理解synchronized
内置锁
每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
互斥锁
内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
修饰实例方法:锁住对象的实例;
修饰静态方法:锁住整个类;
修饰代码块: 锁住一个对象的实例, synchronized (lock) 即synchronized后面括号里的内容。
-----↓↓↓↓↓↓↓↓------
为了易懂,直接上代码。
-----↓↓↓↓↓↓↓↓------
synchronize 修饰实例方法:
public class Test implements Runnable{ public synchronized void exampleMain(){ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(2000); }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { exampleMain(); } public static void main(String[] args) { /** Test test1 = new Test(); Test test2 = new Test(); new Thread(test1,"线程一").start(); new Thread(test2,"线程二").start(); 输出: 同时输出线程一和线程二 */ Test test = new Test(); new Thread(test,"线程一").start(); new Thread(test,"线程二").start(); /** 输出: 线程一 等待两秒后 线程二 结论: 1、创建多个同类的对象,多线程分别调用他们被 synchronized 修饰的实例方法,都不需要等待获取锁。 2、多线程调用同一个对象被 synchronized 修饰的实例方法,需要等待获取锁。 */ } }
synchronize 修饰静态方法:
public class Test implements Runnable{ public synchronized static void staticMain(){ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(2000); }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { staticMain(); } public static void main(String[] args) { Test test1 = new Test(); Test test2 = new Test(); new Thread(test1,"线程一").start(); new Thread(test2,"线程二").start(); /**以上四行代码输出结果等价于: Test test = new Test(); new Thread(test,"线程一").start(); new Thread(test,"线程二").start(); 输出:打印线程一 两秒后打印线程二 结论:1、无论创建多少个同类的对象,多线程分别调用他们被 synchronized 修饰的静态方法,都要等待获取锁
2、多线程调用同一对象被 synchronized 修饰的静态方法,也需要等待获取锁 */ } }
synchronize 修饰代码块:
public class Test implements Runnable{ private static final Object obj = new Object(); @Override public void run() { synchronized(obj){ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(2000); }catch (Exception e){ e.printStackTrace(); } } } public static void main(String[] args) { Test test1 = new Test(); Test test2 = new Test(); new Thread(test1,"线程一").start(); new Thread(test2,"线程二").start(); /**以上四行代码等价于: Test test = new Test(); new Thread(test,"线程一").start(); new Thread(test,"线程二").start(); 输出:打印线程一 两秒后打印线程二 结论:1、无论创建多少个同类的对象,多线程分别启动他们的 run()方法,运行到 synchronized 代码块时,都需要等待获取锁 2、多线程启动同一对象 run()方法,运行到 synchronized 代码块时,也需要等待获取锁 */ } }
05 volatile关键字及其使用场景:
volatile关键字:
1)能且仅能修饰变量,一个线程对该变量进行更改后,会通知其他线程;
2)保证该变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性;
3)禁止指令重排序。
A、B两个线程同时读取volatile关键字修饰的对象,A读取之后,修改了变量的值,修改后的值,对B线程来说,是可见。
使用场景:
1:作为线程开关
2:单例,修饰对象实例,禁止指令重排序
第四章--锁
01 锁的分类(这里只做介绍,详细介绍请前往 ========>>>> https://www.cnblogs.com/nicechen/p/15284873.html )
自旋锁: 线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起
阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态
重入锁:支持线程再次进入的锁,就跟我们有房间钥匙,可以多次进入房间类似
读写锁: 两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享
互斥锁: 上厕所,进门之后就把门关了,不让其他人进来
悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
公平锁:大家都老老实实排队,对大家而言都很公平
非公平锁:一部分人排着队,但是新来的可能插队
偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
独占锁:独占锁模式下,每次只能有一个线程能持有锁
共享锁:允许多个线程同时获取锁,并发访问共享资源
02 深入理解Lock接口
Lock的使用
注意:调用释放锁 unlock()方法时候需要放在 finally代码块中(这里没有放入),避免程序出现异常后,线程无法释放锁,一直占用着锁。
public class Test{ private static int num = 0; private static CountDownLatch countDownLatch = new CountDownLatch(10); private static Lock lock = new ReentrantLock(); /** * 每次调用对num进行++操作 */ public static void inCreate() { lock.lock(); num++; lock.unlock(); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ for (int j = 0; j < 100; j++) { inCreate(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } //每个线程执行完成之后,调用countdownLatch countDownLatch.countDown(); }).start(); } while (true) { if (countDownLatch.getCount() == 0) { System.out.println(num); break; } } } }
lock与synchronized的区别
lock:
- 获取锁与释放锁的过程,都需要程序员手动的控制;
- Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就 是CAS操作。
synchronized:
- synchronized托管给jvm执行;
- 原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
第五章--线程间的通信
1 wait、notify、notifyAll
何时使用:
在多线程环境下,有时候一个线程的执行,依赖于另外一个线程的某种状态的改变,这个时候,我们就可以使用wait与notify或者notifyAll。
wait跟sleep的区别:
wait会释放持有的锁,而sleep不会,sleep只是让线程在指定的时间内,不去抢占cpu的资源。
注意点:
- wait notify必须放在同步代码块中, 且必须拥有当前对象的锁,即不能取得A对象的锁,而调用B对象的wait;
- 哪个对象wait,就得调哪个对象的notify;
- 线程一调用 wait() 后,释放锁,此时线程二获取到该锁,但是线程二还没有运行完毕时,调用了 notify 唤醒线程一,此时,因为线程一的锁被线程二获取了,需要等待线程二运行完毕释放锁后,线程一才能继续执行。
notify跟notifyAll的区别:
- nofity随机唤醒一个等待的线程;
- notifyAll唤醒所有在该对象上等待的线程。
3 使用管道流进行通信
以内存为媒介,用于线程之间的数据传输。
主要有面向字节:【PipedOutputStream、PipedInputStream】、面向字符【PipedReader、PipedWriter】
示例:
public class Reader implements Runnable{ private PipedInputStream pipedInputStream; public Reader(PipedInputStream pipedInputStream) { this.pipedInputStream = pipedInputStream; } @Override public void run() { if(pipedInputStream != null){ String collect = new BufferedReader(new InputStreamReader(pipedInputStream)).lines().collect(Collectors.joining("\n")); System.out.println(collect); } try{ pipedInputStream.close(); }catch (Exception e){ e.printStackTrace(); } } }
public class Main { public static void main(String[] args) { //管道输入流 PipedInputStream pipedInputStream = new PipedInputStream(); //管道输出流 PipedOutputStream pipedOutputStream = new PipedOutputStream(); BufferedReader reader = null; try { //连接两个管道 pipedOutputStream.connect(pipedInputStream); new Thread(new Reader(pipedInputStream)).start(); reader = new BufferedReader(new InputStreamReader(System.in)); pipedOutputStream.write(reader.readLine().getBytes()); } catch (IOException e) { e.printStackTrace(); }finally { try { pipedOutputStream.close(); reader.close(); } catch (IOException e) { e.printStackTrace(); } } }
4 Thread.join()通信及其源码浅析
使用场景:线程A执行到一半,需要一个数据,这个数据需要线程B去执行修改,只有B修改完成之后,A才能继续操作
线程A的run方法里面,调用线程B的join方法,这个时候,线程A会等待线程B运行完成之后,再接着运行
示例:
public class Main { public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "开始运行"); System.out.println(Thread.currentThread().getName() + "运行结束"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }, "线程一"); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "开始运行"); thread.start(); //线程加入 try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束"); },"线程二").start(); } }