多线程
多线程
一、概念
1、什么是进程?
正在进行的程序,当点击程序.exe
图片,程序运行起来,这时候就叫进程。正在运行的程序(进程),是系统资源分配的基本单位。
目前操作系统都是支持多进程的,可以同时执行多个进程,通过进程ID区分。
在详情信息
这里可以看到进程ID
单核CPU在同一个时刻,只能运行一个进程;但是也可以实现多线程,因为CPU的执行速度是非常快的,CPU在多个进程之间快速地切换运行,这样宏观上,似乎就让人感觉是多个线程同时执行。(宏观并行,微观串行)
2、什么是线程?
线程,又称轻量级进程(Light Weight Process),是进程中的一条执行路径,也是CPU的基本调度单位;一个进程是由一个或多个线程组成的,彼此间完成不同的工作,同时执行,称为多线程。
我们可以在任务管理器
->性能
-打开资源管理器
中查看到线程。
迅雷是一个进程,当中的多个下载任务即是多个线程。
我们可以观察到,每个进程至少包含一个线程。
Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。
3、进程与线程的区别
1、进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
2、一个程序运行后至少有一个线程。
3、一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的。
4、进程间不能共享数据段地址,但同进程的线程之间可以。
4、线程的组成
线程的基本组成:
- CPU时间片:操作系统会为每个线程分配执行时间。
- 运行数据:
- 堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象。(在java中,new关键字实例化的对象存储在堆中)
- 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈
- 线程的逻辑代码。
5、线程的特点
1、线程抢占式执行
- 效率高
- 可防止单一线程长时间独占CPU
2、在单核CPU中,宏观上同时执行,微观上顺序执行。
二、多线程的使用
2.1 创建线程
创建线程的三种方式:
- 继承
Thread
类,重写run
方法 - 实现
Runnalbe
接口 - 实现
Callable
接口
2.2 方式1:继承Thread类
步骤:
- 自定义一个类继承
Thread
- 重写自定义类中的
run
方法 - 实例化自定义类对象
- 调用实例化对象的
start
方法,启动线程
MyThread.java
1 | public class MyThread extends Thread{// 1.继承Thread类 |
Test1.java
1 | public class Test1 { |
运行结果:
注意:调用线程时,不能调用
run
方法,而是需要调用start
方法,如果是调用run
方法,那就是调用普通的方法,并不是启动线程。
案例1:线程抢占式执行
MyThread.java
1 | public class MyThread extends Thread{// 1.继承Thread类 |
Test1.java
1 | public class Test1 { |
运行结果:
主线程首先抢到了CPU的执行权,执行三次后CPU被子线程抢占运行一次,然后主线程又抢占运行一次,接着子线程抢占运行九次后,主线程又抢占运行六次结束程序。由于线程时抢占式执行,所以每次运行的结果时不一定相同的。
第二次运行:
第三次运行:
案例2:通过构造方法修改线程名称
MyThread.java
1 | public class MyThread extends Thread{ |
Test1.java
通过调用有参构造方法创建对象,同时给线程设置名称。
1 | public class Test1 { |
运行结果:
案例3:实现继承Thread类4个窗口各卖100张票。
TicketsWindow.java
1 | public class TicketsWindow extends Thread{ |
Test.java
1 | public class Test { |
运行结果:
2.3 线程名称、ID的获取
线程名称与ID的获取
方式1,通过
this.getId
与this.getName
方法获取,缺点是必须是Thread
的子类才能使用1
2
3
4
5
6
7
8
9public class MyThread extends Thread{
public void run() {
for (int i = 0; i < 10; i++) {
//方式1
System.out.println("线程ID:"+getId()+",线程名称:"+getName()+",子线程..."+i);
}
}
}方式2,通过
Thread
类的静态方法currentThread()
获取到当前线程后使用getId()
与getName()
,好处是无论是否继承Thread
类都可以使用。1
2
3
4
5
6
7
8
9public class MyThread extends Thread{
public void run() {
for (int i = 0; i < 10; i++) {
//方式2
System.out.println("线程ID:"+Thread.currentThread().getId()+",线程名称:"+Thread.currentThread().getName()+",子线程..."+i);
}
}
}
线程名称的修改
通过使用Thread
类的setName方法
,注意,要在线程启动之前修改(经测试在start方法之后修改线程名称依然是生效的)
1 | public class Test2 { |
2.4 方式2:实现Runnable接口
步骤:
- 自定义一个类实现
Runnable
接口 - 实现
run
方法 - 实例化自定义类
- 实例化
Thread
类,使用构造方法将实例化的自定义类传入 - 调用实例化
Thread
的对象的start
方法,开始线程。
MyRunnable.java
1 | public class MyRunnable implements Runnable{// 1.自定义类实现Runnable接口 |
Test1.java
1 | public class Test1 { |
运行结果:
- 对于方式二定义线程又两种简化写法,即使用匿名内部类或lambda表达式。
匿名内部类实现
1 | public class Test1 { |
Lambda表达式(对匿名内部类的简化)
1 | public class Test2 { |
案例1:四个窗口共卖100张票
Ticket.java
1 | public class Ticket implements Runnable{ |
Test.java
1 | public class Test { |
运行结果:
从运行结果中可以看到出来,例如第一行与第二行输出都是卖出100张牌,存在问题,这就是线程安全问题。后面通过同步锁来解决。
案例2:你和你女朋友共用一张卡,你向卡里面存钱,你女朋友从卡中取钱,用线程模拟此过程。
Card.java
1 | public class Card { |
AddMoney.java
1 | public class AddMoney implements Runnable{ |
SubMoney.java
1 | public class SubMoney implements Runnable{ |
Test.java
1 | public class Test { |
运行结果:
结果中可以发现有点问题,第一行存入1000元,余额还是0元,这也是线程安全的问题,后面通过同步锁解决。
2.5 线程的常见方法
线程的休眠:
public static native void sleep(long millis)
线程的放弃:
public static native void yield()
线程的加入:
public final void join()
线程的优先级:
public final void setPriority(int newPriority)
设置线程为守护线程:
public final void setDaemon(boolean on)