JavaSE进阶——Day09 多线程(一)

该篇主要讲解Java中多线程的实现、线程安全问题和解决、线程的六种状态

线程简介

多任务

多线程

多条路同时进行

程序、进程、线程

操作系统运行的程序(静态概念)就是进程(动态概念)、一个进程拥有多个线程(互不影响)

很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局。

核心概念

  • 线程就是独立的执行路径

  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,GC线程

  • main()称之为主线程,为系统的入口,用于执行整个程序

  • 在一个进程中,如果开辟了多个线程,线程的运行是由调度器(cpu)安排调度的,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的

  • 对同一份资源操作时mm会存在资源抢夺的问题,需要加入并发控制

  • 线程会带来额外的开销,如CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程实现(重点)

三种创建方式

三种创建方式

Thread类

  1. 创建子类,继承Thread类;

  2. 子类中重写run()方法;

  3. 创建子类对象,调用start()开启线程。(其实对于单核CPU的线程之间是交替进行的)

测试案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestThread1 extends Thread { /* 需要继承Thread */
@Override
public void run() { /* 需要重写多线程的run()方法 */
//run()方法线程体
for (int i = 0; i < 300; i++) {
System.out.println("这是重写run()方法");
}
}
public static void main(String[] args) {
//main线程,主线程
//创建一个线程对象
TestThread1 testThread1 = new TestThread1();
//调用start()方法开启线程(不能调用run()方法哦)
testThread1.start();

for (int i = 0; i < 800; i++) {
System.out.println("这是main()方法");
}
}
}

image-20240427111658773

注意,线程开启不一定立即执行,是由CPU调度执行

Runnable接口

  1. 创建一个子类, 实现Runnable接口;
  2. 在子类中,重写Runnable接口中的方法:run (线程任务方法);
  3. 创建Thread类对象, 并把实现了Runnable接口的子类对象,作为参数传递给Thread类对象
1
new Thread(Runnable类型的对象)
  1. 启动线程。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//创建一个子类, 实现`Runnable`接口并重写run方法

package thread;

/**
* @Author EthanLiu 16693226842@163.com
* @Date 2024/4/27 11:37
* @Project JavaCode_SE_Advance
* @Theme Runnable接口线程
*/
public class MyTask implements Runnable {
@Override
//线程任务
public void run() {
//run()方法线程体
for (int i = 0; i < 800; i++) {
System.out.println("run:" + i);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//创建`Thread`类对象, 并把实现了`Runnable`接口的子类对象,作为参数传递给`Thread`类对象

package thread;

/**
* @Author EthanLiu 16693226842@163.com
* @Date 2024/4/27 11:39
* @Project JavaCode_SE_Advance
* @Theme Runnable线程测试
*/
public class RunnableDemo1 {
public static void main(String[] args) {
//创建`Thread`类对象, 并把实现了`Runnable`接口的子类对象,作为参数传递给`Thread`类对象
Thread thread = new Thread(new MyTask());
//启动线程
thread.start();

// new Thread(new MyTask()).start();

//main函数线程体
for (int i = 0; i < 1000; i++) {
System.out.println("main:" + i);
}
}
}

多线程程序的开发方式:

方式1 :Thread

  1. 创建子类,继承Thread类 (不能在继承其他父类了)
  2. 子类中重写run方法 (线程任务)
  3. 创建子类对象,调用start方法(启动线程)

//线程任务、线程功能 全部都在Thread子类中

方式2 : Runnable

  1. 创建子类 ,实现Runnable (可以继承其他父类)
  2. 重写run方法 (线程任务)
  3. 创建Thread类对象, 并指定要执行的线程任务(Runnable实现类对象作为参数,传递到Thread类的构造方法中)
  4. Thread类对象.start() (启动线程)

//线程任务 : Runnable接口实现类
//线程功能 : Thread类
推荐使用Runnable接口的方式,好处:解耦

接口的作用:

  1. 制定规则
  2. 可以降低耦合 (耦合:紧密连接的程度) //程序都是要求低耦合

线程API的基本方法

控制线程

setPriority(int):设置线程的优先级别,可选范围1-10,默认为5,优先级高代表抢到时间片的概率高

static sleep(long):让当前的线程休眠指定毫秒数

static yield();让当前线程直接放弃时间片返回就绪【很可能自投自抢的情况,比较浪费CPU资源,建议少用】

join():让当前线程邀请调用方法的那个线程优先执行,在被邀请的线程执行结束之前当前线程一直处于阻塞状态,不再继续执行

其他常用方法:

setName():设置线程的名字

getName():得到线程的名字

static activeCount():得到程序中所有活跃的线程:就绪+运行+阻塞

setDaemon(true):设置线程成为守护线程(为了给别的线程提供服务的)

当程序当中只剩下守护线程的时候,守护线程会自行消亡(收尾工作 )

注意:

守护线程需要无限循环,防止其过早消亡
设置成为守护线程必须早于自己的start()
守护线程应该具有较低的优先级
interrupt():中断,打断线程中的阻塞状态

*:sleep()和join()都能导致线程阻塞

static currentThread():得到当前正在运行状态的线程对象

在主线程(main方法)当中,用于获得主线程对象,java当中唯一可以获得主线程对象的方式
在run()调用的其他方法中,用于获得当前线程
意事项:不应该出现在run方法体中,因为得到的线程就是this

………

原文链接:线程基本方法

线程安全问题

线程安全产生的原因

  • 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了

解决思想:线程的同步

  • 概述 : java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性

  • 分类

    • 同步代码块
    • 同步方法
    • 锁机制。Lock

同步代码块

image-20240427124652028

同步方法

image-20240427123702858

image-20240427124424769

同步方法:
修饰符号 synchronized 返回值类型 方法名(参数列表){
}

​ 同步方法也有对象锁:

​ this锁 (当前对象锁)

​ 类名.class (静态方法上的对象锁)

Lock锁

image-20240427124556306

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package security.demo1;

//测试类
public class TicketDemo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();

//创建三个卖票窗口(3个线程在执行同一个任务)
Thread t1 = new Thread(ticket , "窗口1 - ");
Thread t2 = new Thread(ticket , "窗口2 - ");
Thread t3 = new Thread(ticket , "窗口3 - ");

t1.start();
t2.start();
t3.start();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package security.demo1;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//线程任务类
public class Ticket implements Runnable {
//成员变量(电影票数)
private int ticketCount = 100;//有100张电影票
Lock lock = new ReentrantLock(); //定义Lock锁对象

@Override
public void run() {
//模拟卖票
while (true) {
//获取锁
lock.lock();
if (ticketCount > 0) {
// 模拟出票的时间
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + " 出售票号:" + ticketCount);
ticketCount--;
}
if (ticketCount == 0) {
break;
}
//释放锁
lock.unlock();
}
}
}

线程的六种状态

image-20240427133559738

image-20240427131136139