目录
♫什么是线程
♫Java的线程和操作系统线程的关系
♫第一个多线程代码
♫Thread类常见的构造方法
♫创建线程的方式
♪继承Thread类
♪使用匿名内部类继承Thread类
♪实现Runnable接口
♪使用匿名内部类实现Runnable接口
♪使用Lambda表达式
♫Thread类的常见属性
♫线程终止
♪使用自定义的标志位
♪使用Thread自带的标志位
♫等待一个线程
♫线程的状态
♫多线程的意义
我们已经知道进程能充分利用CPU,实行并发编程,但因为进程消耗资源多且执行速度慢,所以就有了比进程更轻量的线程(线程创建,销毁和调度所需开销都比进程低),因此线程也被称为“轻量级进程”。
线程可以看作是进程中的一个实体,一个进程可以包含一个或多个线程,这些线程共享进程的同一份资源(主要指内存(一个线程new的对象其他线程也能用)和文件描述符表(一个线程打开的文件其他线程也能用)),但是每个线程都拥有自己的栈空间和局部变量,都可以执行各自的上下文,且不会相互影响。
注:线程是系统调度的最小单位,进程是系统资源分配的最小单位
线程是操作系统中的概念, 操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库)。Java虚拟机JVM对很多操作系统提供的API进行封装,因此我们只需了解JVM提供的API就能进行Java 线程的相关操作。标准库中的 Thread 类就是对操作系统提供的 API 进行了进一步的抽象和封装的类。
Thread类在Java.lang包(java的内置包,无需显式导入)下,创建线程主要用到Thread类的两个方法:
♩.start():start()的作用是调用系统的API,通过操作系统内核创建新的线程,并把要执行的指令交给新线程,当新线程调度到CPU上执行是,就执行到run()方法。
♩.run():run()是线程要执行的任务
下面就是通过继承Thread,重写run()的方式在主线程下创建了一个新线程thread,主线程和thread线程并发打印五次自己的线程名:
class MyThread extends Thread { @Override public void run() { int a = 5; while (a-- > 0) { //打印该线程的线程名 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class ThreadDemol { public static void main(String[] args) { Thread thread = new MyThread(); //创建线程 thread.start(); int a = 5; while (a-- > 0) { //打印所在线程的线程名 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
运行结果如下:
注:
①.由于线程之间是抢占式执行的,所以在应用程序这一层无法判断main线程和thread线程谁先执行,谁后执行。
②.public static Thread currentThread()方法用于返回当前线程对象的引用。(类比获取所在对象的this)
③.public static void sleep(long millis) throws InterruptedException方法用于休眠当前线程millis毫秒。
使用jdk自带的jconsole工具可以查看java进程里的所有线程:
点进thread.ThreadDemol就可以查看进程里的线程:
方法 描述 Thread() 创建线程对象 Thread(Runnable target) 使用Runnable对象创建线程对象 Thread(String name) 创建线程对象,并命名 Thread(Runnable target, String namre) 使用Runnable对象创建线程对象,并命名 Thread(ThreadGroup group, Runnable target)
线程可以被用来分组管理,分好的组即为线程组
注:t3和t4是代码里的线程名,张三和李四是系统里创建的线程名Thread t1 = new Thread(); Thread t2 = new Thread(new MyRunnable()); Thread t3 = new Thread("张三"); Thread t4 = new Thread(new MyRunnable(), "李四");
♪继承Thread类
创建一个继承Thread类的子类,并重写run方法,通过调佣start创建线程:
//继承Thread类 class MyThread extends Thread { @Override public void run() { //线程执行内容 } } public class ThreadTest { public static void main(String[] args) { Thread thread = new MyThread(); thread.start(); } }
♪使用匿名内部类继承Thread类
还可以通过使用匿名内部类的方式继承Thread类:
//使用匿名内部类继承Thread类 public class ThreadTest { public static void main(String[] args) { Thread thread = new Thread() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }; thread.start(); } }
♪实现Runnable接口
创建一个实现Runnable接口的类,并重写run方法,通过调用start创建线程:
//实现Runnable接口 class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } public class ThreadTest { public static void main(String[] args) { Runnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } }
♪使用匿名内部类实现Runnable接口
还可以通过匿名内部类实现Runnable接口:
//匿名内部类 public class ThreadTest { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }; Thread thread = new Thread(runnable); thread.start(); } }
♪使用Lambda表达式
还可以把任务用Lambda表达式描述,直接将表达式传递给Thread的构造方法:
//使用lambda表达式创建Runnable子类对象 public class ThreadTest { public static void main(String[] args) { Thread thread = new Thread(()->{ System.out.println(Thread.currentThread().getName()); }); } }
属性 获取属性的方法 ID getId() 名称 getName() 状态 getState() 优先级 getPriority() 是否为守护线程 isDaemon() 是否存活 isAlive() 是否被中断 isInterrupted() ♩.ID 是线程的唯一标识,不同线程不会重复。
♩.名称 是各种调试工具用到。(指系统里的线程名)
♩.状态 表示线程当前所处的一个情况,下面我们会进一步说明。
♩.优先级 高的线程理论上来说更容易被调度到。(优先级可以获取也可以设置,只是设置了没啥用)
♩.守护线程 需要记住一点:JVM会在一个进程的所有非守护线程结束后,才会结束运行。(代码里主线程和自己创建的线程默认都是守护线程,其他JVM自带的线程则是守护线程)
♩.是否存活 即简单的理解为 run 方法是否运行结束了。(run()正在跑,isAlive()就是true;run()还没跑或run()跑完了,isAlive()就是false)
♩.线程是否中断 取决于线程本身的代码的实现。
终止线程并不是让线程直接终止掉,而是通知线程应该终止,线程究竟终不终止取决于线程本身的代码实现(就好比老师布置的作业,你可以马上写,你也可以过会在写,你甚至还可以不写)。我们可以通过标志位来实现线程终止操作:
♪使用自定义的标志位
设置公共标志位flag,通过设置flag的值控制线程终止:
public class Test { public static boolean flag = true; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ while (flag) { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); Thread.sleep(1000); //主线程里可以通过修改flag的值控制thread线程的终止 flag = false; } }
运行结果:
注:使用自定义标志位这种方式不能及时响应,尤其在sleep休眠时间较长的情况下
♪使用Thread自带的标志位
通过Thread.currentThread.isInterrupted()方法判断标志位,通过thread.interrupt()设置标志位来实现线程终止:
public class Test { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ while (!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
运行结果:
可以观察到:使用interrupt()会令sleep捕获到InterruptedException异常,这样就能使sleep提前终止。此外,虽然interrupt修改了线程内部的标志位,但sleep在唤醒的时候会将标志位重新修改回去,因此在sleep被唤醒后仍会执行线程里的代码(不写作业)。当然我们也可以在sleep里加个break让线程立即终止(马上去写作业):
public class Test { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ while (!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); break;- } } }); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
运行结果:
当然,break也可以换成其它代码(过会再写作业),这取决于thread的代码实现。
线程是随机调度的,等待一个线程就是让一个线程等待另一个线程执行后完毕再执行,这样可以控制两个线程执行的先后顺序。
等待一个线程可以通过join关键字来实现,下面就是一个main线程等待thread线程的例子:
public class Test { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName()); }); thread.start(); System.out.println("join前"); thread.join(); System.out.println("join后"); } }
运行结果:
上面这种写法相当于main线程死等thread线程,我们还可以通过传递参数,让main线程阻塞等待thread一定时间,thread线程执行完毕或超过指定的时间main线程都会开始执行:
public class Test { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName()); }); thread.start(); System.out.println("join前"); thread.join(100); System.out.println("100毫秒后"); } }
运行结果:
线程状态是针对当前线程调度的情况来描述的,线程的状态是一个枚举类型 Thread.State。通过for-earch可以遍历线程状态:
public class Test { public static void main(String[] args) { for (Thread.State state : Thread.State.values()) { System.out.println(state); } } }
运行结果:
♩NEW(新建状态):当线程对象被创建时,它处于新建状态。
♩RUNNABLE(就绪状态):当线程对象被创建后,其他线程调用了该对象的 start() 方法时,该线程进入就绪状态。
♩BLOCKED(阻塞状态):线程在等待某个监视器锁时,进入阻塞状态。
♩WAITING(等待状态):线程在等待其他线程通知调度器已经满足了某个条件时,进入等待状态。
♩TIMED_WAITING(等待状态):线程等待某个特定时间段的状态。
♩TERMINATED(终止状态):run方法执行完毕,但Thread对象还在。
下面是各种状态的大致流程:
多线程可以利用计算机的多个处理器核心或多个CPU实现并发执行多个任务,提高计算机的执行效率和性能。
对于CPU密集型的任务,多线程的执行速率远高于单线程,如:自增20亿次的操作,在单线程中花费的时间:
public class Test { public static void main(String[] args) { long beginTime = System.currentTimeMillis(); long a = 0; for (long i = 0; i < 20_0000_0000L; i++) { a++; } long endTime = System.currentTimeMillis(); System.out.println("执行时间:" + (endTime-beginTime) + "ms"); } }
运行结果:
在多线程中花费的时间:
public class Test { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ long a = 0; for (long i = 0; i < 10_0000_0000L; i++) { a++; } }); Thread t2 = new Thread(()->{ long a = 0; for (long i = 0; i < 10_0000_0000L; i++) { a++; } }); //记录开始执行的时间 long beginTime = System.currentTimeMillis(); t1.start(); t2.start(); //主线程等待t1,t2线程都执行完 t1.join(); t2.join(); //记录执行完毕的时间 long endTime = System.currentTimeMillis(); System.out.println("执行时间:" + (endTime-beginTime) + "ms"); } }
运行结果:
注:多线程提供运行速率的前提是CPU是多核CPU,且核心处于空闲状态