1.程序、进程和线程
程序
进程
- 进程(process)是程序的一次执行过程,或是一个正在执行的程序。是一个动态的过程:有它自身的产
生、存在和消亡的过程。
- 如:
- 运行中的QQ
- 运行中的音乐播放器
- 视频播放器等;
- 程序是静态的,进程是动态的;
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域;
- 进程是一个具有一定独立功能的应用程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分
配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
- 进程一般由程序,数据集合和进程控制块三部分组成。
- 程序用于描述进程要完成的功能,是控制进程执行的指令集;
- 数据集合是程序在执行时所需要的数据和工作区;
- 程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志。
- 进程具有的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
- 并发性:任何进程都可以同其他进程一起并发执行;
- 独立性:进程是系统进行资源分配和调度的一个独立单位,不同进程的工作互相不影响;
- 结构性:进程由程序,数据和进程控制块三部分组成;
- 制约性:因访问共享数据/资源或进程间同步而产生制约;
进程与程序联系与区别:
- 两者联系
-
- 进程是操作系统处于执行状态程序的抽象
- 程序 = 文件(静态的可执行文件)
- 进程 = 执行中的程序 = 程序 + 执行状态
- 同一个程序的多次执行过程对应为不同进程
-
- 例如:多次使用命令ls的执行对应多个进程。
-
- 进程执行需要的资源
- 内存:保存代码和数据
- cpu:执行指令
- 两者区别
-
- 进程是动态的,程序是静态的。
- 程序是有序代码的集合
- 进程是程序的执行
- 进程是暂时的,程序是永久的。
- 进程时一个状态变化的过程
- 程序可长久保存
- 进程与程序的组成不同。
- 进程的组成包括程序,数据,和进程控制块。
- 简单来说:我们写好的一个代码,要想让它运行起来,首先需要将这个程序加载到内存中,程序一旦运行
那么就是一个进程,就需要给进程分配资源建立PCB等动作。说白了,程序只是一个存储在我们硬盘上的文件
而已断电不会消失,但是进程是在动态运行的一个实体,进程执行结束相关信息就会被释放了。
线程
- 在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单
位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
- 简而言之:线程是进程的一个执行单元。比进程更小的独立运行的基本单位。
- 注:一个程序至少一个进程,一个进程至少一个线程。比如视频中同时有声音、图像、弹幕等;
- 并行和并发:https://www.cnblogs.com/fulaien/p/16489366.htmlhttps://www.cnblogs.com/fulaien/p/16489366.html
- 并行
- 真正的多线程
- 并发
- 假象的多线程
- 并行
2.线程的创建和启动
方式一:继承Thread类
Thread thread1=new Thread(){ @Override public void run() { suo(); } public void suo() { while (true){//定义单个线程循环 synchronized (lock){//上安全锁,以下判断只能互斥进行 if (!thread1turn){ try { lock.wait();//轮不到就等 } catch (InterruptedException e) { throw new RuntimeException(e); } } if (count==0)break;//到点了就跳 System.out.println("我是线程1 "+count); count--; thread1turn=false;//下一轮让出cpu lock.notify();//给另外的线程开锁 } } }
1、如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
2、run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
3、想要启动多线程,必须调用start方9法。
4、一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异 常“IllegalThreadStateException”。
方式二:实现Runnable接口的方式
- 实现Runnable接口
- Thread(Runnable target, String name):创建新的Thread对象;
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法,把新线程要做的事写在run方法中
- 创建自定义的Runnable的子类对象
- 通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给Thread 类的构造器中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
两种创建方式的对比
1. 继承与实现
- Thread类:是一个类,需要通过
extends
关键字来继承。这意味着,如果一个类继承了Thread类,那么它就不能再继承其他类了,因为Java不支持多重继承。 - Runnable接口:是一个接口,需要通过
implements
关键字来实现。实现Runnable接口的类可以自由地继承其他类,从而避免了Java单继承的限制。
2. 线程创建方式
- 继承Thread类:通过继承Thread类并重写其run方法,然后创建该类的实例并调用其start()方法来创建线程。这种方式简单直接,但限制了类的继承能力。
- 实现Runnable接口:通过实现Runnable接口并重写其run方法,然后创建Thread类的实例,并将Runnable实例作为构造参数传递给Thread类,最后调用Thread实例的start()方法来创建线程。这种方式更加灵活,允许将任务(Runnable实现类的实例)和线程(Thread实例)分离开来,从而更容易地实现线程之间的资源共享和任务分配。
3. 资源共享
- Thread类:由于Thread类本身是类的形式,其内部可以直接定义实例变量等成员,这些成员默认是线程隔离的,即每个Thread对象都有自己的实例变量副本。因此,在多个线程之间共享数据时,需要额外的同步机制。
- Runnable接口:实现Runnable接口的类中的实例变量等成员是共享的,因为它们通常被封装在另一个类中(比如一个服务类)。这使得实现Runnable接口的方式更适合于需要多个线程共同访问和修改同一份数据资源的场景。
4. 灵活性与扩展性
- Thread类:虽然简单直接,但在某些复杂的场景下可能会显得不够灵活。例如,当你需要让你的类继承自其他类(而不是Thread类)时,你就无法使用继承Thread类的方式来创建线程了。
- Runnable接口:由于其基于接口的实现方式,使得它更加灵活和可扩展。你可以自由地让你的类继承自其他类,并通过实现Runnable接口来使其具备多线程能力。此外,你还可以将任务(Runnable实现类的实例)作为参数传递给线程池等高级并发工具来使用,从而进一步提高程序的并发性能。
综上所述,Thread类和Runnable接口在Java多线程编程中各有优劣,具体使用哪种方式取决于你的具体需求和代码结构。在实际开发中,建议优先考虑使用实现Runnable接口的方式来创建线程,因为它更加灵活和可扩展。
3.线程的状态(生命周期)
- JDK中用Thread.State类定义了线程的几种状态。
- 要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线
程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建状态(New):当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
-
- 如:Thread t = new MyThread();
- 就绪状态(Runnable):处于新建状态的线程对象被start()后(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,已经加入了操作系统的任务调度队列,等待被操作系统调度执行,并不是说执行了t.start()此线程立即就会执行;看CPU的执行权
- start
- 运行状态(Running):当就绪状态的线程被操作系统的任务调度机制调度到,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;run()方法定义了线程的操作和功能; 抢到了CPU的执行权,开始运行
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其再次进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;a
- 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞 : 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态(Dead):线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
- 就绪状态转换为运行状态:当此线程得到处理器资源;运行状态转换为就绪状态:当此线程主动调用 yield()方法或在运行过程中失去处理器资源。运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
- 注意
- 此处需要特别注意的是:当调用线程的 yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。
4.线程控制(逻辑控制)
1.线程控制:获取当前线程、名称和设置名称
Mythread m= new Mythread("haha"); System.out.println(m.getName());
2.休眠线程:Thread.sleep(2000);
3.守护线程 t2.setDaemon(true);
4.加入线程 join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续;
5.礼让线程 Thread.yield();
6.设置线程优先级 thread.setPriority(10);
7.线程等待wait
8.线程同步方法 synchronized关键字加到方法声明上
通过声明synchronized保证线程互斥安全执行的条件下利用wait阻塞和notifyAll()来实现多线程互斥执行;
=========================================================================
public class thraed1 { static boolean thread1turn=true;//定义顺序标签 static int count =20;//定义循环次数 static Object lock = new Object(); public static void main(String[] args) { Thread thread1=new Thread(){ @Override public void run() { suo(); } public void suo() { while (true){//定义单个线程循环 synchronized (lock){//上安全锁,以下判断只能互斥进行 if (!thread1turn){ try { lock.wait();//轮不到就等 } catch (InterruptedException e) { throw new RuntimeException(e); } } if (count==0)break;//到点了就跳 System.out.println("我是线程1 "+count); count--; thread1turn=false;//下一轮让出cpu lock.notify();//给另外的线程开锁 } } } }; Thread thread2=new Thread(){ @Override public void run() { suo(); } public void suo() { while (true){ synchronized (lock){ if (thread1turn){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } if (count==0)break; System.out.println("我是线程2 "+count); count--; thread1turn=true; lock.notify(); } } } }; thread1.start(); thread2.start(); } }
=========================================================================