多线程 | 我的日常分享

多线程

多线程

一、概念

1、什么是进程?

正在进行的程序,当点击程序.exe图片,程序运行起来,这时候就叫进程。正在运行的程序(进程),是系统资源分配的基本单位。

目前操作系统都是支持多进程的,可以同时执行多个进程,通过进程ID区分。

image-20220328174728024

详情信息这里可以看到进程ID

image-20220328174847475

单核CPU在同一个时刻,只能运行一个进程;但是也可以实现多线程,因为CPU的执行速度是非常快的,CPU在多个进程之间快速地切换运行,这样宏观上,似乎就让人感觉是多个线程同时执行。(宏观并行,微观串行)

2、什么是线程?

线程,又称轻量级进程(Light Weight Process),是进程中的一条执行路径,也是CPU的基本调度单位;一个进程是由一个或多个线程组成的,彼此间完成不同的工作,同时执行,称为多线程。

我们可以在任务管理器->性能-打开资源管理器中查看到线程。

迅雷是一个进程,当中的多个下载任务即是多个线程。

image-20220328175723704

image-20220328175903426

我们可以观察到,每个进程至少包含一个线程。

Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。

3、进程与线程的区别

1、进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。

2、一个程序运行后至少有一个线程。

3、一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的。

4、进程间不能共享数据段地址,但同进程的线程之间可以。

4、线程的组成

线程的基本组成:

  • CPU时间片:操作系统会为每个线程分配执行时间。
  • 运行数据:
    1. 堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象。(在java中,new关键字实例化的对象存储在堆中)
    2. 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈
  • 线程的逻辑代码。

5、线程的特点

1、线程抢占式执行

  • 效率高
  • 可防止单一线程长时间独占CPU

2、在单核CPU中,宏观上同时执行,微观上顺序执行。

二、多线程的使用

2.1 创建线程

创建线程的三种方式:

  1. 继承Thread类,重写run方法
  2. 实现Runnalbe接口
  3. 实现Callable接口

2.2 方式1:继承Thread类

步骤:

  1. 自定义一个类继承Thread
  2. 重写自定义类中的run方法
  3. 实例化自定义类对象
  4. 调用实例化对象的start方法,启动线程

MyThread.java

1
2
3
4
5
6
7
public class MyThread extends Thread{// 1.继承Thread类
// 2.重写run方法
@Override
public void run() {
System.out.println("子线程...");
}
}

Test1.java

1
2
3
4
5
6
7
8
public class Test1 {
public static void main(String[] args) {
// 3.实例化MyThread对象
MyThread myThread = new MyThread();
// 4.调用start方法
myThread.start();
}
}

运行结果:

image-20220328233857603

注意:调用线程时,不能调用run方法,而是需要调用start方法,如果是调用run方法,那就是调用普通的方法,并不是启动线程。

案例1:线程抢占式执行

MyThread.java

1
2
3
4
5
6
7
8
9
public class MyThread extends Thread{// 1.继承Thread类
// 2.重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程..."+i);
}
}
}

Test1.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Test1 {
public static void main(String[] args) {
// 3.实例化MyThread对象
MyThread myThread = new MyThread();
// 4.调用start方法
myThread.start();
//main主线程
for (int i = 0; i < 10; i++) {
System.out.println("主线程"+i);
}
}
}

运行结果:

主线程首先抢到了CPU的执行权,执行三次后CPU被子线程抢占运行一次,然后主线程又抢占运行一次,接着子线程抢占运行九次后,主线程又抢占运行六次结束程序。由于线程时抢占式执行,所以每次运行的结果时不一定相同的。

image-20220328234246442

第二次运行:

image-20220328234551389

第三次运行:

image-20220328234635260

案例2:通过构造方法修改线程名称

MyThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread extends Thread{
//定义构造方法调用父类构造方法
MyThread(String name){
super(name);
}
MyThread(){

}
@Override
public void run() {
System.out.println("线程名称:"+this.getName());
}
}

Test1.java

通过调用有参构造方法创建对象,同时给线程设置名称。

1
2
3
4
5
6
public class Test1 {
public static void main(String[] args) {
MyThread myThread = new MyThread("子线程1");
myThread.start();
}
}

运行结果:

image-20220329151600661

案例3:实现继承Thread类4个窗口各卖100张票。

TicketsWindow.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TicketsWindow extends Thread{
int tickets = 100;
public TicketsWindow(String name){
super(name);
}
@Override
public void run() {
while (true){
// 票数不够直接结束
if(tickets<=0){
break;
}
tickets--;
System.out.println(this.getName()+"卖出了一张票,还剩"+this.tickets+"张");
}
}
}

Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public static void main(String[] args) {
//定义4个窗口线程
TicketsWindow t1 = new TicketsWindow("窗口1");
TicketsWindow t2 = new TicketsWindow("窗口2");
TicketsWindow t3 = new TicketsWindow("窗口3");
TicketsWindow t4 = new TicketsWindow("窗口4");
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}

运行结果:

image-20220329153809361

2.3 线程名称、ID的获取

线程名称与ID的获取

  1. 方式1,通过this.getIdthis.getName方法获取,缺点是必须是Thread的子类才能使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MyThread extends Thread{
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    //方式1
    System.out.println("线程ID:"+getId()+",线程名称:"+getName()+",子线程..."+i);
    }
    }
    }
  2. 方式2,通过Thread类的静态方法currentThread()获取到当前线程后使用getId()getName(),好处是无论是否继承Thread类都可以使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MyThread extends Thread{
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    //方式2
    System.out.println("线程ID:"+Thread.currentThread().getId()+",线程名称:"+Thread.currentThread().getName()+",子线程..."+i);
    }
    }
    }

image-20220329001044258

线程名称的修改

通过使用Thread类的setName方法,注意,要在线程启动之前修改(经测试在start方法之后修改线程名称依然是生效的)

1
2
3
4
5
6
7
8
9
10
public class Test2 {
public static void main(String[] args) {
// 3.实例化MyThread对象
MyThread myThread = new MyThread();
// 修改线程名称
myThread.setName("我是子线程1");
// 4.调用start方法
myThread.start();
}
}

image-20220329003710698

2.4 方式2:实现Runnable接口

步骤:

  1. 自定义一个类实现Runnable接口
  2. 实现run方法
  3. 实例化自定义类
  4. 实例化Thread类,使用构造方法将实例化的自定义类传入
  5. 调用实例化Thread的对象的start方法,开始线程。

MyRunnable.java

1
2
3
4
5
6
7
8
9
public class MyRunnable implements Runnable{// 1.自定义类实现Runnable接口
// 2.实现run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程Id:"+Thread.currentThread().getId()+",线程名称:"+Thread.currentThread().getName()+","+i);
}
}
}

Test1.java

1
2
3
4
5
6
7
8
9
10
public class Test1 {
public static void main(String[] args) {
// 3.实例化自定义类
MyRunnable myRunnable = new MyRunnable();
// 4.实例化Thread类,使用构造方法传入Runnable的实现类
Thread t = new Thread(myRunnable);
// 5.调用Thread的start方法,开始线程
t.start();
}
}

运行结果:

image-20220329002414611

  • 对于方式二定义线程又两种简化写法,即使用匿名内部类或lambda表达式。

匿名内部类实现

1
2
3
4
5
6
7
8
9
10
11
public class Test1 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现的线程");
}
},"匿名内部类线程");
t1.start();
}
}

image-20220329154404759

Lambda表达式(对匿名内部类的简化)

1
2
3
4
5
6
7
public class Test2 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> System.out.println("Lambda实现的线程"),
"Lambda线程");
t1.start();
}
}

image-20220329154552117

案例1:四个窗口共卖100张票

Ticket.java

1
2
3
4
5
6
7
8
9
10
11
public class Ticket implements Runnable{
private int count = 100;
@Override
public void run() {
// 判断票数是否大于0 否则则不卖
while (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + count + "张票");
count--;
}
}
}

Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) {
//定义公共的票
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
Thread t4 = new Thread(ticket,"窗口4");
//启动四个窗口开始卖票
t1.start();
t2.start();
t3.start();
t4.start();
}
}

运行结果:

从运行结果中可以看到出来,例如第一行与第二行输出都是卖出100张牌,存在问题,这就是线程安全问题。后面通过同步锁来解决。

image-20220330214903469

案例2:你和你女朋友共用一张卡,你向卡里面存钱,你女朋友从卡中取钱,用线程模拟此过程。

Card.java

1
2
3
4
5
6
7
8
9
10
11
public class Card {
private int money;

public int getMoney() {
return money;
}

public void setMoney(int money) {
this.money = money;
}
}

AddMoney.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AddMoney implements Runnable{
private Card card;
public AddMoney(Card card){
this.card = card;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
card.setMoney(card.getMoney()+1000);
System.out.println(Thread.currentThread().getName()+"存入1000元,当前余额"+card.getMoney());
}
}
}

SubMoney.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SubMoney implements Runnable{
private Card card;
public SubMoney(Card card){
this.card = card;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (card.getMoney() >=1000){
card.setMoney(card.getMoney()-1000);
System.out.println(Thread.currentThread().getName()+"取出1000元,当前余额"+card.getMoney());
}else{
System.out.println("余额不足,请存钱");
// i-- 防止十次循环执行结束后,仍然没有取出钱
i--;
}
}
}
}

Test.java

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
// 定义一张银行卡
Card card = new Card();
Thread boy = new Thread(new AddMoney(card),"男孩");
Thread girl = new Thread(new SubMoney(card),"女孩");
// 启动两个线程
boy.start();
girl.start();
}
}

运行结果:

结果中可以发现有点问题,第一行存入1000元,余额还是0元,这也是线程安全的问题,后面通过同步锁解决。

image-20220330224011511

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)

案例1:线程的休眠

2.6 线程的状态