并发和并行
- 并发
并发指的是在同一个时间段发生,即交替执行。例如单核CPU一次只能处理一个程序,但是为什么我们可以在使用IntelliJ IDEA写代码的同时又可以使用QQ音乐播放音乐呢?因为这两个程序微观上在执行时是交替执行,只不过交替的时间非常短(毫秒级),我们感觉是同时在执行,实际上是并发。单核CPU不能并行处理多个任务,只能是多个任务在单个CPU上并发运行。 - 并行
并行指的两个或者多个事件在同一时刻发生,即同时执行 。在多核CPU系统中,并发的程序可以分配到不同的CPU上,实现多任务同时执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前的计算机绝大多数都是多核CPU,核心越多,并行处理的程序越多,能大大提高电脑的运行效率。
CPU信息
进程和线程
- 进程
进程是程序的一次执行过程,是系统运行程序的基本单元,系统运行一个程序既是一个进程从创建、运行到消亡的过程。每个进行都拥有一个独立的内存空间,一个应用程序可以运行多个进程(例如QQ就可以登录多个账号)
Windows任务管理器中可以查看到当前系统运行的进程 - 线程
线程是进程的执行单元,负责当前进程中的任务执行,一个进程中至少会有一个线程。但是通常都是一个进程有多个线程,这样的应用程序称为多线程应用程序。每个线程都有独立的栈空间
一个Java程序就是一个进程,而一个进程一次只能执行一个线程,因此Java程序只有高并发,没有高并行。即多个线程交替执行,没有同时执行。
Java程序运行时,内部同时有多条线程,而此时需要一个线程调度机制,Java线程的调度机制是抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个线程执行。
创建线程的两种方式
在Java中java.lang.Thread类表示线程。如果想要创建线程就需要创建Thread类的对象
- 继承Thread类,重写run()方法
PrintThread继承了java.lang.Thread,并且重写任务的方法
@Log4j2注解主要用于日志记录,通过由于log42.xml文件中配置了控制台输出的日志会打印线程信息,因此在程序运行时可以看到当前运行的线程名称。
/** 自定义线程类PrintThread */@Log4j2class PrintThread extends Thread { /** 重写父类的run()方法 run()方法就是线程执行的任务 */ @Override public void run() { for (int i = 0; i < 10; i++) { //PrintThread的run方法打印i的值 log.info("Print Thread i = "+i); } }}
然后在单元测试方法中创建线程对象,调用start()方法启动线程执行线程的任务。
而且主线程中也有和PrintThread一样的任务在执行。
/** * 线程创建的方式1 继承Thread类重写run()方法 */ @Test public void testCreateThreadInherit() { //创建线程对象 PrintThread printThread=new PrintThread(); //启动线程执行任务 printThread.start(); //主线程中打印0-9 for (int i = 0; i < 10; i++) { //主线程中打印i的值 log.info("Main Thread i = "+i); } }
程序运行结果
从控制台打印输出的结果看出两条线程是相互交替执行的。不过交替的时间非常短暂,给人的感觉几乎同时执行。但是其本质还是串行的。
程序运行时会同时启动至少两条线程 main方法所在的线程是main线程,而PrintThread由于在创建的时没有传递线程名参数,因此默认的线程名是Thread-0
当主线程执行到printThread.start();时Thread-0线程启动,并等待线程调度抢占CPU资源,一旦抢占到资源就会由JVM调用run方法执行任务,线程的run()方法执行完毕后表示线程的任务执行完毕,线程就会被回收。由于Java线程采用的调度机制是抢占机制,因此main线程和Thread-0线程两条线程会同时抢占CPU资源,同一时刻只能有一条线程执行,而且每条线程有自己的单独栈空间。当线程开启后会在栈区开辟空间来执行任务,任务执行完毕线程对象就会被销毁,而主线程会等待所有子线程任务执行完毕,才会结束。
- 实现java.lang.Runnable接口,实现run()方法
自定义实现类CustomizationRunnable 实现Runnable接口
@Log4j2class CustomizationRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { //PrintThread的run方法打印i的值 log.info("CustomizationRunnable Thread i = "+i); } }}
然后在测试方法中创建Thread对象,并在Thread构造器中传入CustomizationRunnable的匿名对象
/** * 线程创建的方式2:实现Runnable接口,重写run方法 */ @Test public void testCreateThreadImplRunnable(){ Thread thread=new Thread(new CustomizationRunnable()); thread.start(); for (int i = 0; i < 10; i++) { //主线程中打印i的值 log.info("Main Thread i = "+i); } } }
程序运行结果
和之前继承Thread类重写run()方法一样,程序也是交替运行的。
Thread类提供了接收java.lang.Runnable接口的构造器去构造Thread对象,我们可以使用匿名内部类的方式,方便的实现每个线程执行不同的任务操作。
例如某个网站要统计年度用户访问总人数和统计年度用户平均访问时间,此时可以使用两个Runnable接口的匿名内部类作为参数传入Thread来创建Thread对象
/** * 使用匿名内部类构建Runnable对象创建线程 */ @Test public void testCreateThreadNonInnerClassRunnable(){ //创建Thread对象,直接传入Runnable接口的匿名内部类作为参数传入 Thread sumThread=new Thread(new Runnable() { @Override public void run() { log.info("统计年度用户访问总人数"); } }); sumThread.start(); Thread avgThread=new Thread(new Runnable() { @Override public void run() { log.info("统计年度用户平均访问时间"); } }); avgThread.start(); for (int i = 0; i < 10; i++) { // 主线程中打印i的值 log.info("Main Thread i = " + i); } } }
程序运行结果
继承Thread类重写run()和实现Runnable接口重写run()方法两种方式创建线程的区别
- Java的类只能支持单继承,但是可以实现多个接口。通过实现接口扩展性比较强,因此推荐使用实现Runnable接口,实现run()方法的方式创建线程
- 继承Thread类重写run()方法的线程和任务绑定在一块,而实现Runnable接口重写run()方法的任务和线程是独立的,增强程序健壮性,实现解耦操作。
- 使用实现Runnable接口,实现run()方法创建线程,还可以适合多个相同业务线程共享同一个(Runnable)任务对象。
- 线程池(ThreadPool)只会接收实现Runnable或者Callable接口的线程对象,不能直接放入继承Thread类的对象
Thread类常用方法
Thread类常用的构造方法
- public Thread(): 分配一个新的线程对象,线程名由系统给出
- public Thread(String name):分配一个新的线程对象,线程名由参数传入
- public Thread(Runnable target):分配一个带有指定目标的新的线程对象,Runnable是任务接口,线程名由系统给出。
- public Thread(Runnable target,String name):分配一个带有指定目标的新的线程对象并指定名字
Thread类的常用方法
- public void start() :启动线程,JVM调用该线程的run方法
- public void run():线程执行的任务
- public String getName(): 获取当前线程名称
- public static void sleep(long millis):使当前执行的线程以指定的毫秒数暂停,暂停任务执行
- public static Thread currentThread() :返回当前正在执行线程对象的引用
我们可以使用Thread提供的API把之前写的程序改造下
相比之前的PrintThread
- 增加了两个构造器,其中一个有参构造器可以在创建Thread对象时可以传递线程的名称
- run方法中使用super.getName()获取当前的线程名称
- run方法中使用Thread.sleep(1000)让PrintThread在每次打印i的值后暂停1秒,该方法有个编译异常,异常类型是InterruptedException,这里使用try/catch捕获异常,并在catch处理异常,主要是打印异常堆栈信息
/** 自定义线程类PrintThread */@Log4j2class PrintThread extends Thread { PrintThread(){} PrintThread(String threadName){ super(threadName); } /** 重写父类的run()方法 run()方法就是线程执行的任务 */ @Override public void run() { for (int i = 0; i < 10; i++) { // PrintThread的run方法打印i的值 //getName()获取当前运行的线程名称 log.info(super.getName()+"i = " + i); try { //当前线程暂停1秒 System.out.println(Thread.currentThread().getName()+"暂停1秒"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }}
相比之前的testCreateThreadInherit()方法
- 在创建线程对象时指定了线程的名称
- 在main线程中打印i的值后也使用Thread.sleep(1000)暂停1秒
- 通过Thread.currentThread()获取当前运行线程的对象
/** 线程创建的方式1 继承Thread类重写run()方法 */ @Test public void testCreateThreadInherit() { // 创建线程对象 PrintThread printThread = new PrintThread("PrintThread"); // 启动线程执行任务 printThread.start(); // 主线程中打印0-9 for (int i = 0; i < 10; i++) { // 主线程中打印i的值 log.info(Thread.currentThread().getName()+" i = " + i); try { System.out.println(Thread.currentThread().getName()+"暂停1秒"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
程序运行结果
线程的状态
线程的六种状态
线程的状态就是线程由生到死的过程,在java.lang.Thread.State这个枚举中一共给出线程的六种状态
- New(新建) :线程刚被创建,但是并未启动。即还没有调用start()方法,只有线程对象,没有线程特征,当线程对象创建时就是NEW状态
- Runnable(可运行) 线程可以在JVM中运行的状态,可能线程正在运行,也有可能没有运行,这主要取决于CPU,当调用了线程对象的start()方法时就是Runnable状态
- Blocked(锁阻塞):当一个线程试图获取一个对象锁,而该对象锁被其线程所持有,则该线程进入Blocked状态,当该线程持有锁时,该线程变成Runnable状态。等待锁对象时就是Blocked状态
- Waiting(无限等待):一个线程在等待另外一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后不能自动唤醒,必须等待另外一个线程调用notify()方法或者notifyAll()方法才能唤醒,使用锁对象调用wait()方法时就是Waiting状态
- Timed Waiting(计时等待):同waiting状态,有几个方法有超时参数,调用它们将进入Timed Waiting状态,这状态将一直保持到超时期满或者接受到唤醒通知。带有超时的常用方法有Thread.sleep(参数),Obejct.wait(参数),调用sleep()方法时进入Timed Waiting状态
- Terminated(被终止):因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡,run方法执行结束时或者执行过程遇到了异常但是没有捕获处理异常就是Terminated状态
线程状态的切换
- 创建线程对象时线程处于New(新建状态)
- 线程对象调用start()方法后进入Runnable(可运行)状态
- 线程对象在可运行状态(Runnable)执行时 ,在有锁的情况下(例如遇到了同步代码块时)需要获取锁,如果没有获取到锁就进入锁阻塞(Blocked)状态,如果获取到了锁究进入可运行(Runnable)状态
- 线程对象在 可运行状态(Runnable)执行时,获得锁对象并调用了wait()方法则进入无限等待(Waiting)状态,如果其他线程调用notify()方法,获取到锁对象,此时进入可运行(Runnable)状态,如果其他线程调用notify()方法但是没有获取锁对象,此时进入锁阻塞(Blocked)状态
- 线程对象在可运行状态(Runnable)执行时,如果调用了Thread.sleep(参数)方法或者是Object.wait(参数)方法,此时线程进入计时等待(Timed Waiting)状态,如果sleep时间或者是wait时间到了,或者是wait时间未到但是其他线程调用了notify()方法并获取到锁对象,此时线程状态变成可运行(Runnable)状态,如果是wait时间到了但没有获取到锁,或者wait时间未到,其他线程调用notify()但没有获取到锁,此时线程进入Blocked(锁阻塞)状态。
- 线程对象在可运行状态(Runnable)执行结束,线程状态是被终止(Terminated)状态
调用wait()方法和notify()方法需要使用同一个锁对象
锁对象调用wait()方法进入无限等待,那么该线程不会霸占CPU和锁对象
锁对象调用notify()方法唤醒无限等待的线程
调用sleep()方法进入计时等待,那么该线程就不会霸占CPU