多线程 --- 创建线程与线程的属性
(一).线程的概念1.概念在Java中对线程进程了统一的封装封装成了Thread类2.run方法在Thread中有一个run方法这个方法是一个抽象方法我们需要重写我们的run方法来进行执行run方法是线程的入口方法一旦新的线程启动就要执行这里的代码run方法不需要我们手动调用新的线程创建好了之后自动的取执行注意run方法相当于 “回调函数”3.start方法start方法表示创建了一个新的线程多了一个执行流就意味着在CPU中多了一个人干活所以这个代码就可以“一心两用”start方法才是真正的在系统中创建线程即JVM调用操作系统的API完成线程创建操作注意每个Thread对象只能start一次即每次想要创建一个新的线程都得需要创建一个新的Thread对象(二).线程的创建1.方法1通过继承Thread 类重写run方法class MyThread extends Thread{ Override public void run() { System.out.println(Hello Thread); } }当我将两个线程都写成死循环后程序的运行结果为可以发现程序中的两个线程在交替运行当我使用一个 “休眠”方法 sleep程序再运行sleep方法是一个静态方法表示 “休眠让当前的线程暂时放弃CPU等指定的时间过了之后再执行”直接使用Thread类进行调用即可里面的参数单位为“毫秒”可以看到两个线程在交替执行这说明线程的调度是随机的抢占式执行2.方法2通过实现Runnable接口重写run方法Runnable 表示一个 “可执行任务”通过这个接口来调用run方法这样写可以更好的 “解耦合”将来修改代码的时候更方便class MyRunnable implements Runnable{ Override public void run() { System.out.println(Hello Runnable); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }依旧是两个线程执行的结果还是两者交替执行当我不使用 thread.start()方法而使用thread.run()方法的时候程序的运行结果为可以看出全是 “Hello Runnable”这是因为我调用的是run方法没有创建线程所以只有main这一个线程所以只会执行 run方法里面的内容注意main方法对应的线程即一个进程中至少要包含的那个线程为主线程3.优化方法1使用匿名内部类public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(){ Override public void run() { while (true){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread.start(); while (true){ System.out.println(hello main); Thread.sleep(1000); } }创建了一个Thread子类子类的名字是匿名的。{ } 里面就可以编写子类的代码即子类中要包含的哪些属性方法以及要重写的父类的方法。创建了这个匿名内部类的实例并将这个实例的引用赋值给了thread当程序运行起来的时候也达到了我们想要的效果注意通过匿名内部类来写一般用于这个代码是 “一次性”的时候4.优化方法2使用匿名内部类public static void main(String[] args) throws InterruptedException { Runnable runnablenew Runnable() { Override public void run() { while (true){ System.out.println(hello runnable); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread threadnew Thread(runnable); thread.start(); while (true){ System.out.println(hello main); Thread.sleep(1000); } }和方法2一样这样做的目的是为了“解耦合”。如果直接继承Thread类执行的任务本身和 Thread(线程)这个概念是耦合在一起的我们为了降低耦合是为了后续改代码方便我们只需要记住使用Runnable任务和线程概念是分离的5.优化方法3和方法4使用lambda表达式public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (true){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); while (true){ System.out.println(hello main); Thread.sleep(1000); } }对于lambda表达式本质上就是一个 “匿名函数”最主要的用途就是作为 “回调函数”在java 中方法必须要依托于 类 来存在就像lambda表达式就类似于 “函数式接口”创建了一个匿名的函数式接口的子类并且创建出对应的实例并且重写了里面的方法(三).查看线程的工具jconsole.exeJconsole.exe可以看到当前的线程1.找到JDK文件的位置2.根据上图中的路径在此电脑中找到jdk的存放位置3.点击bin文件夹找到jconsole.exe可执行文件4.打开jconsole.exe执行程序5.选择 “不安全的连接”6.选择线程7.Thread-0 和 main 就是我们创建的线程8.查看线程所在的行数线程的调用栈获取线程状态的时刻线程里的代码执行到哪里了(四).Thread类的其他属性和方法1.Thread类的构造方法方法说明Thread()创建线程对象,必须重写Thread类里面的run()方法Thread(Runnable target)使用Runnable对象创建线程对象不需要重写Thread类里面的run()方法只需要重写Runnable类里面的run()方法Thread(String name)创建线程并命名Thread(Runnable target ,String name)使用Runnable对象创建线程对象并命名Thread(ThreadGroup group ,Runnable target)线程可以被用来分组管理分号的组即为线程组 [了解即可]对于Thread(String name)这个构造方法我们可以给线程起名字起什么样的名字都无所谓都不会影响线程的运行起名字的主要目的就是为了 描述线程是干啥的方便调试示例public static void main(String[] args) { Thread thread1new Thread(()-{ while (true){ System.out.println(hello thread1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread2new Thread(()-{ while (true){ System.out.println(hello thread2); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread3new Thread(()-{ while (true){ System.out.println(hello thread3); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread1.start(); thread2.start(); thread3.start(); }这是我手动创建了三个线程当程序运行起来的时候三个线程分别抢占式的执行然后通过“jconsole.exe”文件看一下三个线程的运行状态可以看到这就是我们的那三个线程下面我们可以分别对他们进行命名我们再通过 “jconsole.exe”执行文件看一下我们的线程可以发现不管我们是什么类型的名字都可以识别所以说这个命名的主要目的就是为了方便调试注意为什么没有main(主线程)线程在上图中我们并没有发现main线程这是因为我们之前写的代码都是单线程的程序即main方法执行完毕之后程序就结束了但是多线程的程序中当main方法执行完 thread3.start()这一行就直接结束了主线程随即销毁所以在 “jconsole.exe”可执行文件中只能看到3个子线程看不到main线程2.线程的其他属性属性获取方法IDgetId()名称getName()状态getState()优先级getPriority()是否后台线程isDaemon()是否存活isAlive()是否被中断isInterrupted()等待一个线程join()休眠当前线程sleep()(1).getId()类似于PIDJava中给每个运行的线程都分配了id标识线程的身份(2).getName()获取线程的名字(3).isDaemon()想要理解这个方法我们需要先明白什么是后台线程和前台线程如果一个线程的结束不会影响到进程的结束那么这个线程就是 “后台线程”如果一个线程的结束影响到进程的结束此时这个线程就被称为“前台线程”示例public static void main(String[] args) { Thread thread1new Thread(()-{ while (true){ System.out.println(hello thread1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } },1线程); Thread thread2new Thread(()-{ while (true){ System.out.println(hello thread2); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } },bbb); Thread thread3new Thread(()-{ while (true){ System.out.println(hello thread3); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } },123); System.out.println(thread1.getPriority()); thread1.start(); thread2.start(); thread3.start(); }还是通过这个例子来看在线程图中我们可以看到即使main线程结束了但是线程“123”线程“bbb”线程“1线程”还在既然三个线程还在那么进程就依然存在也就是说这三个线程的存在能够影响到进程继续存在而不能结束此时这三个线程就被称为 “前台线程”剩下的其他线程其他线程都是JVM自带的一些线程他们的存在不会影响到进程的结束即使他们继续存在如果进程结束了那么他们也就结束了此时这些线程就是 “后台线程”例如垃圾回收线程垃圾回收线程跟随整个进程持续执行通过一个例子来看public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (true){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); for (int i 0; i 3; i) { System.out.println(hello main); Thread.sleep(1000); } System.out.println(main线程结束); }我们可以发现main线程的结束没有影响到thread线程的结束如果我们想要main线程结束的时候thread线程也结束那么我们就需要将thread线程修改成 “后台线程”我们可以通过setDaemon()方法将线程改成后台线程(4).isAlive()检查系统线程是否存活首先要明确一点Java代码中创建的Thread对象和系统中的线程是一 一对应的关系但是Thread对象的生命周期和系统中的线程的生命周期是不同的可能会存在一种情况就是Thread对象还存活但是系统中的线程已经销毁的情况通俗一点说系统线程 从start()开始到run()执行结束结束之后系统线程就彻底销毁了Thread对象从new开始就没有任何引用指向它直到被垃圾回收之后才算是结束生命周期所以就会出现Thread对象还存活但是对应的系统中的线程已经销毁的情况综上所述isAlive()判断的是对应的系统线程是否还在执行/未终止而不是判断Thread对象本身是否还在内存里。通过一个例子来看public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ for (int i 0; i 3; i) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); System.out.println(thread.isAlive()); // 这个地方一定是false因为这个时候 start 方法还没有执行 thread.start(); while (true){ System.out.println(thread.isAlive()); //isAlive() 判断线程是否存活 Thread.sleep(1000); } }Thread中的代码逻辑3秒之后就结束了所以对应的线程的入口方法里面的逻辑就结束了系统中对应的线程就随之销毁了(在操作系统的角度)但是我们的thread对象依然存在当代码运行起来的时候发现(5).isInterrupted()是否被中断”中断“让一个线程能够结束让线程的入口方法执行完毕线程就随之结束了即 run()方法能够尽快的return()①.示例在理解这个方法之前我们可以先通过一个例子来实现一下 ”中断一个线程“private static boolean isFinishedfalse; public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (!isFinished){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(thread 结束); }); thread.start(); Thread.sleep(3000); isFinishedtrue; }上图中的例子我们是通过修改外部类的成员变量来控制线程然后内部类通过访问外部类的成员来判断是否结束线程问题如果将成员变量isFinished修改成局部变量可以吗通过上面的图片发现报错了这就是我们之前学的变量捕获方面的内容了在lambda里面如果希望使用外面的变量那么就会触发 ”变量捕获“这样的语法首先lambda是“回调函数”执行的时机可能是很久之后了很有可能线程创建好了当前main这里的方法都执行完了那么对应的isFinished就销毁了所以线程thread就无法获取到isFinished了针对于这种情况java就采用了 “变量捕获”的思想把被捕获的变量拷贝一份拷贝给lambda里面外面的变量isFinished是否销毁就不会影响到lambda里面的执行了而拷贝意味着这样的变量就不适合进行修改因为修改一方另一方不会随之改变本质上是两个变量所以最终java这边就不允许进行修改对于引用类型的变量不能修改这个引用指向其他的对象但是引用指向的对象本体是可以进行修改的例如 Test test new Test(); test 就表示引用类型的变量 new Test() 就表示对象的本体上图将局部变量修改成 成员变量就不会涉及到 “变量捕获”的语法了而是转换成“内部类访问外部类的成员” 语法成员变量的生命周期也是让 垃圾回收 来管理的在lambda里面不担心变量生命周期失效的问题也就意味着不需要进行拷贝也不必限制final之类的②.isInterrupted() 和 interrupt()Java的Thread对象中提供了现成的变量来中断线程不需要我们自己进行创建现在将上面的代码进行修改public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (!Thread.currentThread().isInterrupted()){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(thread 线程结束); }); thread.start(); Thread.sleep(3000); System.out.println(main 线程尝试中断 thread线程); thread.interrupt(); }currentThread()方法是一个静态方法所以直接通过类名来进行调用currentThread()方法的作用就是在哪个线程中调用获取到的就是哪个线程的Thread引用当前是在lambda表达式中进行调用的所以返回的结果就是threadisInterrupted()方法是用来判断Thread 里的 boolean 变量的值即判断线程是否被终止注意Thread.currentThread().isInterrupted() 这样写的原因是因为lambda的定义是在Thread实例化之前我们要先重写完run方法之后才能进行对象的实例化所以lambda表达式中是不知道thread的存在的所以我们要先通过Thread.currentThread()方法来获取当前的线程对象interrupt()方法是主动进行终止修改这个 boolean 变量的值从而使调用interrupt()方法的这个线程来结束线程下面就开始运行程序当程序运行起来的时候发现报错了这是为什么这是因为 interrupt()方法除了设置boolean变量之外还会唤醒sleep()这样的阻塞方法在while()循环中大部分的时间都在sleep所以当主线程调用 interrupt()方法的时候极大概率下thread线程正在sleep中此时这个interrupt()方法就会唤醒sleep 从而使sleep()方法抛出异常如何解决这个异常我们可以这样做当抛出的异常的时候不要进行抛出直接break掉这个循环注意这样的效果其实是抛出了异常我们没有进行捕获而是直接中断了程序其实和上面的捕获异常一样同样当抛出异常的时候我们也可以不进行捕获当程序运行起来的时候发现会一直打印 “hello thread”这又是为什么对于上面这个代码是sleep()方法导致的当interrupt()方法将isInterrupted()方法内部修改成true同时将sleep()方法给唤醒了当sleep()方法被唤醒之后将 isInterrupted()方法内部设置回了 false因此在这种情况下如果继续执行循环的条件判定就会发现能够继续执行Java这样实现的好处Java把决定权交给了被终止的线程自己了有三种选择①.直接无视终止指令继续执行。就像catch{}里面什么都没写一样②.直接终止线程直接在{}中加入break或者抛出异常③.当捕获到异常之后可以在处理异常处写上一些代码获取阶段性的结果也就是说如果线程终止了我们可以的到线程终止之后的预期结果而不是随机的结果。(6).join()前面我们介绍过多个线程之间是并发执行随即调度的如果我们不想让多个线程之间随即调度那么我们可以通过join()方法join()方法可以决定多个线程之间的结束的先后顺序如果在main线程中调用thread.join()方法可以让main线程等待thread线程先结束join()等待线程结束join(long millis)等待线程结束最多等 millis 毫秒join(long millis , int nanos)等待线程结束最多等待millis 毫秒 nanos 纳秒 (更精确)示例①.调用不带参数的join()方法public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ for (int i 0; i 3; i) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(thread 线程结束); }); thread.start(); thread.join(); System.out.println(main 线程结束); }当在main线程中调用 thread.join()的时候此时main线程就会等待thread线程先结束当执行到thread.join()的时候main线程就会进入 “阻塞等待”一直等到 thread 线程执行完毕main线程才会继续执行②.调用带一个参数的join()方法public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ for (int i 0; i 3; i) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(thread 线程结束); }); thread.start(); thread.join(1000); System.out.println(main 线程结束); }对于上面的代码可以看出main线程只会等待thread线程1000毫秒如果经过1000毫秒之后thread线程还没有结束那么main线程也就不等了③.调用带两个参数的join()方法public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ for (int i 0; i 3; i) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(thread 线程结束); }); thread.start(); thread.join(1000,500); System.out.println(main 线程结束); }带两个参数的join()方法就会使等待的时间更精确但是我们使用的计算机一般很难进行ns级别的精确时间计算。有一类操作系统为 “实时操作系统”可以做到更精确的时间这样的操作系统一般会用于“工业航天”等方面我们日常的系统都不是 “实时操作系统”(7).sleep()休眠当前线程但是注意一点线程是随即调度的所以这个方法只能保证实际休眠时间大于等于参数设置的休眠时间当我们写了 sleep(1000)实际上会休眠比1000多一点因为代码调用sleep相当当前线程于cpu会让出资源当时间到了的时候需要操作系统内核把这个线程重新调用到cpu上才能继续执行也就是说当时间到了的时候意味着允许被调度了而不是立即执行了所以说会比1000多一点特殊写法sleep(0) 写了sleep(0) 意味着让当前的线程 立即放弃cpu资源把cpu让出来给别人更多的执行机会等待操作系统重新调度(8).getState()获取线程的状态站在操作系统的角度进程状态分为 就绪 和 阻塞Java 线程也是对操作系统线程的封装针对线程的状态Java也进行了重新封装进行表示NEW:表示已经创建出来了Thread对象但是还没有startpublic static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ System.out.println(hello thread); }); //获取线程的状态 System.out.println(thread.getState()); }RUNNABLE线程正在CPU上执行/线程随时可以去CPU上执行public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (true){ //什么都不写 //本身也是一段cpu指令一直循环执行也是需要在cpu上运行的 } }); //获取线程的状态 System.out.println(thread.getState()); thread.start(); Thread.sleep(1000); System.out.println(thread.getState()); }TERMINATED内核中的线程已经结束了但是Thread对象还在public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ System.out.println(hello thread); }); //获取线程的状态 System.out.println(thread.getState()); thread.start(); Thread.sleep(1000); System.out.println(thread.getState()); }TIMED_WAITING指定时间阻塞线程阻塞不参与cpu调度不继续执行但是阻塞的时间也是有上限的public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (true){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); Thread.sleep(1000); System.out.println(thread.getState()); }while(true){}中sleep()是2000毫秒然后在主线程中我sleep() 了 1000毫秒所以当 1000毫秒之后我获取thread线程的状态此时thread线程还在阻塞过程中所以状态为TIMED_WAITING但是当2000毫秒之后又会变成RUNNABLE另外 join(时间) 也会进入到 TIMED_WAITING状态public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (true){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); thread.join(60000); }WAITING死等和TIMED_WAITING 相对public static void main(String[] args) throws InterruptedException { Thread threadnew Thread(()-{ while (true){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); //只需要调用不带参数的join()方法就可以将状态变成WAITING thread.join(); }BLOCKED是一种由 “锁”导致的阻塞比较特殊等下一章线程安全的时候再进行介绍总结介绍的这几种线程的状态主要是用于调试程序找BUG的时候使用当发现代码中出现BUG的时候①.通过 jconsole.exe 或者 其他工具查看当前的进程中的所有线程找到对应逻辑的线程是谁②.看线程的状态是啥看到TIMED_WAITING /WAITING 怀疑是不是代码中某个方法产生阻塞没有及时唤醒看到BLOCKED怀疑是不是代码中出现了死锁看到RUNNABLE线程本身没问题考虑逻辑上某些条件没有预期触发之类的③.看线程的具体调用栈(尤其是 阻塞的状态线程代码阻塞到哪一行了)