Java-多线程知识点
进程,线程
并发
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。即一个处理器同时处理多个任务,只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。(交替执行)
并行
并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)。 并行即多个处理器或者是多核的处理器同时处理多个不同的任务,两个线程互不抢占CPU资源,可以同时进行。(同时执行)
什么是进程
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
什么是线程
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
好处 :
(1)易于调度。
(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
(3)开销少。创建线程比创建进程要快,所需开销很少。
(4)充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
进程与线程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
什么是线程调度
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权。
有两种调度模型:分时调度模型和抢占式调度模型。
- 分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。
- Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
Java的线程调度是不分时的,同时启动多个线程后,不能保证各个线程轮流获得均等的CPU时间片。
如果希望明确地让一个线程给另外一个线程运行的机会,可以采取以下办法:
- 调整各个线程的优先级
- 让处于运行状态的线程调用Thread.sleep()方法
- 让处于运行状态的线程调用Thread.yield()方法
- 让处于运行状态的线程调用另一个线程的join()方法
- 线程切换:不是所有的线程切换都需要进入内核模式
需要注意的是,线程的调度不是跨平台的,它不仅仅取决于Java虚拟机,还依赖于操作系统。在某些操作系统中,只要运行中的线程没有遇到阻塞,就不会放弃CPU;在某些操作系统中,即使线程没有遇到阻塞,也会运行一段时间后放弃CPU,给其它线程运行的机会。
小结
- 并发和并行都可以处理“多任务”,二者的主要区别在于是否是“同时进行”多个的任务。
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
- 线程的调度不是跨平台的,既取决于Java虚拟机,又依赖于操作系统。
线程操作
实现线程的两种方式
创建新执行线程有两种方法:
- 一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。
- 另一种方法是声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。
继承Thread类实现线程
Java虚拟机允许应用程序同时执行多个执行线程。每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。Thread类是Runnable接口的实现类,可以是实现多线程。
Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是
完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
Thread类构造方法:
- public Thread() :分配一个新的 Thread对象。
- public Thread(Runnable target) :分配一个新的 Thread对象。
- public Thread(Runnable target, String name) :分配一个新的 Thread对象。
- public Thread(String name) :分配一个新的 Thread对象。
Thread类常用方法:
- public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
- public String getName() :返回此线程的名称。
- public int getPriority() :返回此线程的优先级。
- public void join() :等待这个线程死亡。
- public void join(long millis) :等待这个线程死亡最多 millis毫秒。
- public void join(long millis, int nanos) :等待最多 millis毫秒加上 nanos纳秒这个线程死亡。
- public void run() :如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。
- public void setName(String name) :将此线程的名称更改为等于参数 name 。
- public void setPriority(int newPriority) :更改此线程的优先级。
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
- public static void sleep(long millis, int nanos) :导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。
- public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
- public static void yield() :对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。
继承Thread类创建线程的步骤:
- 定义子类,继承Thread类;
- 子类中重写Thread类中的run方法;
- 创建Thread子类对象,也就是创建线程对象;
- 调用线程对象的start方法启动线程。
【案例1】 继承Thread类实现多线程
代码实现:
package day18;
//继承Thread类
public class Myth01 extends Thread {
//设置线程名称的属性
private String name;
//有参构造
public Myth01(String name) {
this.name = name;
}
@Override
public void run() {
//for循环
for (int i = 1; i <= 20 ; i++) {
//输出
System.out.println(this.name + i );
}
}
}
package day18;
//创建测试类
public class Test01 {
//程序入口
public static void main(String[] args) {
//创建自定义线程对象
Myth01 m1=new Myth01("自定义线程");
//调用start方法来启动线程
m1.start();
//for循环
for (int i = 1; i <= 20 ; i++) {
//输出
System.out.println("主线程" + i );
}
}
}
运行结果:
(每次运行的结果都是不一样的 )解释说明:
程序启动运行main时候,Java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的 start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
那么为什么可以完成并发执行呢?我们再来讲一讲原理:
多线程执行时,到底在内存中是如何运行的呢?
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
主线程:主线程是当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行。即main就是程序运行中的主线程。
主线程的重要性体现在两方面:
1.是产生其他子线程的线程;
2.通常它必须最后完成执行比如执行各种关闭动作。
子线程:就是我们自己手动创建的,并且在启动的时候只能调用start方法,不能调用run方法,run方法就是主线程了。
实现Runnable接口实现线程
java.lang .Runnable接口用来指定每个线程要执行的任务。包含了一个run 的无参数抽象方法,需要由接口实现类重写该方法。
- public void run():当实现接口的对象 Runnable被用来创建一个线程,启动线程使对象的 run在独立执行的线程中调用的方法
实现Runnable接口创建线程的步骤:
-
定义子类,实现Runnable接口;
-
子类中重写Runnable接口中的run方法;
-
创建Runnable接口的子类对象;
-
通过Thread类的构造器创建线程对象;
-
调用Thread类的start方法启动线程。
【案例2】 实现Runnable接口实现多线程
代码实现:
package day18;
//创建Runnable接口实现类
public class Myth02 implements Runnable {
@Override
public void run() {
//for循环
for (int i = 1; i <= 20 ; i++) {
//输出
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
package day18;
//创建测试类
public class Test02 {
//程序入口
public static void main(String[] args) {
//创建线程对象
Myth02 m2=new Myth02();
//创建Thread类
Thread t1=new Thread(m2,"线程一");
Thread t2=new Thread(m2,"线程二");
//调用start方法开启线程
t1.start();
t2.start();
}
}
运行结果:
通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程 代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread 对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现 Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
两种创建线程方法的区别
- 一个是继承Thread 类,一个是实现Runnable 接口。
- Java是只能继承一个父类,可以实现多个接口的一个特性,所以说采用Runnable方式可以避免Thread方式由于Java单继承带来的缺陷。
- Runnable的代码可以被多个线程共享(Thread实例),适合于多个多个线程处理统一资源的情况。
线程的状态
- 新建(NEW):刚刚创建出来但是没有运行的线程处于此状态。
- 运行(RUNNABLE):调用start方法启动后的线程处于运行状态。
- 受阻塞(BLOCKED):等待获取锁的线程处于此状态。
- 无限等待(WAITING):当线程调用wait()方法时,线程会处于无限等待状态【没有时间的等待】
- 计时等待(TIMED_WAITING):当线程调用wait(毫秒值)方法或sleep(毫秒值)时,线程会处于计时等待状态【有时间的等待】
- 退出(TERMINATED):当线程执行完了自己的run方法或者调用了stop方法,会进入退出状态。
线程状态转换图可参考此博客: 线程状态转换图及各部分介绍
阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入等待池中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒。(线程池的知识点会在后面讲)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。(线程同步的知识点会在下个单元讲解)
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
实现线程调度
方 法 | 说 明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
设置线程优先级:
Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行。
- 线程的优先级没有指定时,所有线程都携带普通优先级。
- 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
- 记住优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。
- 与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
- 由调度程序决定哪一个线程被执行。
- 线程名.setPriority()用来设定线程的优先级。
- 记住在线程开始方法被调用之前,线程的优先级应该被设定。
- 你可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级。
线程优先级可参考此博客: Java 多线程:线程优先级
【案例3】 设置线程的优先级
代码实现:
package day18;
//创建Runnable接口实现类
public class Myth03 implements Runnable {
//重写run方法
@Override
public void run() {
//for循环
for (int i = 1; i <= 10 ; i++) {
//输出
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
package day18;
//创建测试类
public class Test03 {
//程序入口
public static void main(String[] args) {
// //方法一:
// //创建线程类对象
// Myth03 m3 = new Myth03();
// //创建Thread类对象
// Thread t1 = new Thread(m3,"线程一");
// Thread t2 = new Thread(m3,"线程二");
// Thread t3 = new Thread(m3,"线程三");
// //设置最高优先级
// t1.setPriority(Thread.MAX_PRIORITY);
// //设置正常优先级,默认情况下即为5
// t2.setPriority(5);
// //设置最低优先级
// t3.setPriority(Thread.MIN_PRIORITY);
//
// //调用start方法开启线程
// t1.start();
// t2.start();
// t3.start();
//方法二:
//创建Thread类对象 Thread类对象中包含线程类对象
Thread t1=new Thread(new Myth03(),"线程一");
Thread t2=new Thread(new Myth03(),"线程二");
Thread t3=new Thread(new Myth03(),"线程三");
//设置最高优先级
t1.setPriority(Thread.MAX_PRIORITY);
//设置正常优先级,默认情况下即为5
t2.setPriority(5);
//设置最低优先级
t3.setPriority(Thread.MIN_PRIORITY);
//调用start方法开启线程
t1.start();
t2.start();
t3.start();
}
}
运行结果:
由此可知,设置优先级,只是优先执行的概率变大,但不是绝对的。
线程的强制执行
通过join()方法,使当前线程暂停执行,等待其他线程结束后再继续执行本线程。
【案例4】 线程的强制执行
代码实现:
package day18;
//创建Runnable接口实现类
public class Myth04 implements Runnable {
//重写run方法
@Override
public void run() {
//for循环
for (int i = 0; i < 20; i++) {
//输出
System.out.println(Thread.currentThread().getName()+"运行:"+i);
}
}
}
package day18;
//创建测试类
public class Test04 {
//程序入口
public static void main(String[] args) {
//创建Thread类对象 Thread类对象中包含线程类对象
Thread t1 = new Thread(new Myth04(),"自定义线程");
//调用start方法开启线程
t1.start();
//for循环
for( int i = 0; i < 20 ; i++ ){
//使用if判断当i=6的时候 让自定义线程开始执行 待自定义线程执行完之后再执行主线程
if(i==6){
try {
//使用join方法让t1加入进来
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//输出
System.out.println("主线程"+"运行:"+i);
}
}
}
运行结果:
线程休眠
运用sleep()方法,使正在执行的线程暂停一段时间,将CPU让给别的线程,即让线程进入休眠等待状态。
线程休眠只是让该线程停一段时间,一段时间之后就可以接着进行,不需要别的线程进行完,例子中因为线程一总执行时间较短,在主线程休眠的时间内就运行完了。
【案例5】 线程的休眠
代码实现:
package day18;
//继承Thread类
public class Myth05 extends Thread {
//设置线程名称的属性
private String name;
//有参构造
public Myth05(String name) {
this.name = name;
}
//重写run方法
@Override
public void run() {
//for循环
for (int i = 0; i < 20 ; i++) {
//输出
System.out.println(this.name + i );
}
}
}
package day18;
//创建测试类
public class Test05 {
//程序入口
public static void main(String[] args) throws InterruptedException {
//创建线程类对象
Myth05 m1 = new Myth05("熊大");
//调用start方法开启线程
m1.start();
//for循环
for (int i = 0; i < 10; i++) {
//使用if判断当i=6的时候 让主线程休眠
if (i == 6) {
//进行休眠
Thread.sleep(500);
}
//输出
System.out.println("熊二" + i );
}
}
}
运行结果:
由结果可知,主线程休眠,子线程先执行
线程礼让
通过yield()方法,将当前进程停下,换成就绪状态,让系统的调度器重新调度一次。
与sleep()方法相似,但yield()方法不会阻塞该线程,之后该线程与其他线程是相对公平的。调度谁看系统,有可能还是调度它自己。
【案例6】线程的礼让
代码实现:
package day18;
//创建Runnable接口实现类
public class Myth06 implements Runnable {
//重写run方法
@Override
public void run() {
//for循环
for (int i = 0; i < 20 ; i++) {
//使用if判断当i=5的时候 进行礼让
if (i==5){
//输出执行礼让
System.out.println(Thread.currentThread().getName()+"执行礼让");
//执行礼让
Thread.yield();//执行礼让
}
//输出
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
package day18;
//创建测试类
public class Test06 {
//程序入口
public static void main(String[] args) {
//创建线程对象
Myth06 my=new Myth06();
//创建Thread类对象
Thread t1=new Thread(my,"线程A");
Thread t2=new Thread(my,"线程B");
//调用start方法开启线程
t1.start();
t2.start();
}
}
运行结果:
执行代码可知,线程礼让的动作不一定成功
【综合案例1】使用多线程模拟多人徒步爬山
分析:
1.每个线程代表一个人
2.设置每人爬山速度
3.每爬完100米显示信息
4.爬完了显示信息
实现步骤:
- 创建线程类
- 构造方法完成属性初始化,实现run()方法
- 线程休眠模拟爬山中的延时
- 实现测试类
代码实现:
package day18;
//继承Thread类
public class Myth07 extends Thread{
// 定义爬100米的时间
private int time;
// 定义爬多少个100米的数量
public int num = 0;
//有参构造
public Myth07(String name, int time, int kilometer) {
super(name);
this.time = time;
this.num = kilometer * 1000 / 100;
}
//重写run方法
@Override
public void run() {
//进行while循环
while (num > 0) {
try {
//进行休眠
Thread.sleep(this.time);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出爬完100米
System.out.println(Thread.currentThread().getName() + "爬完100米!");
//数量减减
num--;
}
//输出爬完了
System.out.println(Thread.currentThread().getName()+"爬完了");
}
}
package day18;
//创建测试类
public class Test07 {
//程序入口
public static void main(String[] args) {
//创建线程类对象
Myth07 young = new Myth07("年轻人",500,1);
Myth07 old = new Myth07("老年人",1000,1);
//调用start方法开启线程
young.start();
old.start();
}
}
运行结果:
【综合案例2】某科室一天需看普通号50个,特需号10个,特需号看病时间是普通号的2倍,开始时普通号和特需号并行叫号,叫到特需号的概率比普通号高,当普通号叫完第10号时,要求先看完全部特需号,再看普通号,使用多线程模拟这一过程
实现步骤:
- 创建线程类
- 构造方法完成属性初始化,实现run()方法
- 线程休眠模拟特需号和普通号的时间
- 实现测试类
代码实现:
package day18;
//创建Runnable接口实现类
public class Myth08 implements Runnable {
//重写run方法
@Override
public void run() {
//for循环
for (int i = 0; i < 10; i++) {
//输出特需号信息
System.out.println("特需号:" + (i+1) );
try {
//进行休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package day18;
//创建测试类
public class Test08 {
//程序入口
public static void main(String[] args) {
//创建Thread类对象 Thread类对象中包含线程类对象
Thread t1 = new Thread(new Myth08());
//设置最高优先级
t1.setPriority(Thread.MAX_PRIORITY);
//调用start开启线程
t1.start();
//for循环
for(int i=0;i<20;i++){
//输出普通号信息
System.out.println("普通号:" + (i+1) );
try {
//进行休眠
Thread.sleep(500);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
//使用if判断当i=9的时候 让特需号线程开始执行 待特需号线程执行完之后再执行普通号线程
if(i==9){
try {
//使用join方法让t1加入进来
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
小结
- Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。 实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
- 实际开发中建议使用Runnable实现多线程。
- 在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
- 线程的礼让和设置线程的优先级,都是让线程获得CPU资源的概率大,但是并不绝对。
- 线程的休眠会使其他线程先被CPU分配资源。
以上描述的代码全为我本人亲自敲打,可能会有些错误的地方,如有更好的建议,欢迎您在评论区友善发言。