多线程

(一)实现多线程

线程是依赖于进程而存在的

进程:是正在运行的程序(一个正在运行的程序就是进程)
是系统进行资源分配和调用的独立单位;
每一个进程都有它自己的内存空间和系统资源。
(任务管理器 -> 进程)

线程
在一个进程内部,它可以执行一个任务,也可以执行多个任务,每个任务可以看做一个线程
线程:是进程中的单个顺序控制流,是一条执行路径。
单线程:一个进程如果只有一条执行路径,则称为单线程程序(记事本程序)
多线程:一个进程如果有多条执行路径,则称为多线程程序(扫雷程序)

※(1)多线程的实现方式1

将一个类声明为一个 thread 的子类,这个子类应该重写 thread 类的 run 方法,然后可以分配并启动子类的实例。

***方式1:继承 Thread 类 ***
①定义一个类 MyThread 继承 Thread 类
②在 MyThread 类中重写 run() 方法
③创建 MyThread 类的对象
④启动线程 start()

两个小问题:
为什么要重写run()方法?
    因为run()是用来封装被线程执行的代码
    (MyThread方法中可能还有其他的方法,并不是所有的代码都需要被线程执行,所以要用run方法来封装可以被线程执行的方法)
run()方法和start()方法的区别?
    run():封装线程执行的代码,直接调用,相当于普通方法的调用
    start():启动线程;然后由JVM调用此线程的run()方法(多线程)

(2)设置和获取线程名称

Thread类中设置和获取线程名称的方法:

  1. void setName(String name): 将此线程的名称更改为等于参数name
  2. String getName(): 返回此线程的名称
  3. 通过构造方法也可以设置线程名称

如何获取main()方法所在的线程名称?
4. Thread.currentThread()
public static native Thread currentThread(); 返回当前正在执行的线程对象的引用

(3)线程调度(抢占式 优先级)

线程有两种调度模型:
分时调度模型: 所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

Java使用的是抢占式调度模型

假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

Thread类中设置和获取线程优先级的方法:
public final int getPriority(): 返回此线程的优先级
public final void setPriority(int newPriority): 更改此线程的优先级

线程优先级高————> 获取CPU时间片的几率高
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果

线程默认优先级是5;线程优先级的范围是:1-10
Thread.MIN_PRIORITY( = 1)
Thread.MAX_PRIORITY( = 10)
Thread.NORM_PRIORITY(=5)

(4)线程控制

方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留 (暂停执行)指定的毫秒数
void join() 等待这个线程死亡(线程插队)(必须等待调用了这个方法的线程执行完毕,其他线程才有机会执行)
void setDaemon(boolean on) 将此线程标记为守护线程(参数传true),当运行的线程都是守护线程时,Java虚拟机将退出(设置为后台线程)(当主线程执行完毕之后,守护线程也会很快执行完毕)

守护线程是指在程序运行过程中,用来执行一些后台任务的线程。
这些线程通常不会影响程序的主要功能,而是用来处理一些辅助性的工作,比如垃圾回收、日志记录等。
守护线程会在程序的主线程结束后自动结束,不会阻止程序的退出。因此,它们也被称为“后台线程”或“守护进程”。

(5)线程生命周期

线程从生到死经历的过程(在每个过程又做了哪些事情?)
线程生命周期.png

※(6)多线程的实现方式2

创建线程的另一种方法是声明一个实现Runnable接口的类。那个类然后实现了run方法。然后可以分配类的实例,在创建Thread时作为参数传进、并启动。

方式2:实现 Runnable接口
①定义一个类 MyRunnable 实现 Runnable 接口
②在 MyRunnable 类中重写run()方法
③创建 MyRunnable 类的对象
④创建 Thread 类的对象,把 MyRunnable 对象作为构造方法的参数
    Thread (Runnable target)
    Thread(Runnable target, String name)
⑤启动线程

相比继承Thread类,实现Runnable接口的好处:
避免了Java单继承的局限性(可以在实现 Runnable 接口的同时再继承一个类)可以有其自己的父类(不影响MyRunnable类继承其他父类)
适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想(同一个资源[MyRunnable类]由多个线程去使用)

(二)线程同步(数据安全问题)

卖票出现了问题:
相同的票出现了多次;
出现了负数的票

问题原因:
线程执行的随机性导致的

为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)
①是否是多线程环境;
②是否有共享数据;
③是否有多条语句操作共享数据(共享数据有修改的行为)。

解决:采用“线程同步机制”
即线程排队执行,不能并发。(③)
也就是说把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可(同步代码块的方式来解决)

(1)synchronized 同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
锁多条语句操作共享数据,可以使用同步代码块实现

1
2
3
4
//格式:
synchronized(任意对象){
多条语句操作共享数据的代码
}

synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

同步的好处和弊端:
好处:解决了多线程的数据安全问题
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

(2)synchronized 同步方法

使用 synchronized 修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着(对方法内部的代码进行锁定)

同步普通方法格式:

1
2
3
public synchronized void method(){
//可能产生安全问题的代码
}

同步静态方法格式:

1
2
3
public static synchronized void method(){
//可能产生安全问题的代码
}

那么同步方法中的同步锁是谁呢?
①对于“实例方法”,同步锁就是 this(在方法内部有一个对象,可以代表本类 -> this);
②对于 “static 静态方法” ,我们使用当前方法所在类的字节码对象(类名.class)
静态的内容是和类相关的,类有一个字节码文件对象(在反射里面会讲解)

(3)锁机制 Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock()

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作

Lock类中常用的方法包括:

  1. **lock()**:获取锁,如果锁不可用则会一直等待。
  2. **unlock()**:释放锁。
  3. tryLock():boolean:尝试获取锁,如果锁可用则获取并返回true,否则立即返回false。
  4. tryLock(long time, TimeUnit unit):boolean:在指定的时间内尝试获取锁,如果锁可用则获取并返回true,否则在指定时间内未获取到锁则返回false。
  5. lockInterruptibly():获取锁,如果线程在等待锁的过程中被中断,则会抛出InterruptedException异常。
  6. newCondition():Condition:创建一个新的条件变量。

Lock是一个接口,不能直接创建对象,要找它的实现类 ReentrantLock

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
public class Ticket implements Runnable {
private int ticket = 100;
private Lock lock = new ReentrantLock();
//执行卖票操作
@Override
public void run() {
while (true) {
try{
lock.lock(); // 加同步锁
sellTicket(); //产生安全问题的方法
}finally{
lock.unlock(); // 释放同步锁
}
}
}
private void sellTicket(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖第" + ticket-- + "张票");
}
}
}

线程安全的类

①StringBuffer
线程安全,可变的字符序列
(用法和StringBuilder一样)
里面的方法都是同步方法加了 synchronized 的,是线程安全的(在多线程中会用到)

②Vector
线程安全的List
(用法和ArrayList相同)
③Hashtable
线程安全
(和 HashMap 一个用法)
(②和③一般不用,被Collections里面的线程安全的方法替代了)

多线程数据安全问题的类型

Java中多线程程序的数据安全问题主要包括以下几种类型:

程安全问题的出现主要是因为多线程并发访问共享资源时可能导致的竞态条件(Race Condition)和并发访问冲突。以下是一些常见的原因:

  1. 竞态条件(Race Condition):多个线程同时访问共享资源,并且执行的顺序不确定,这可能导致不同的线程在不同的时间点对资源进行操作,从而产生意外的结果。
  2. 共享资源:线程安全问题通常发生在多个线程共享同一资源(如变量、数据结构、文件、数据库等)的情况下。如果不正确地管理对共享资源的访问,就会发生冲突。
  3. 并发修改:多个线程同时对数据结构(如集合、数组、映射等)进行修改,如果没有适当的同步控制,可能导致数据结构的损坏或不一致。
  4. 非原子操作:某些操作不是原子操作,它们在多线程环境中可能被中断,导致部分操作被执行,而不是完整的原子操作。
  5. 缓存不一致:多个线程使用不同的本地缓存副本,而不是从主内存中获取最新的值。这可能导致不同线程看到不同的数据状态。
  6. 不同步的访问:没有适当的同步机制(如锁、信号量、条件变量等)来确保多线程之间的协调和互斥,从而导致竞态条件和并发问题。
  7. 死锁:当多个线程在等待彼此释放锁资源时,可能导致死锁问题,其中所有线程都无法继续执行。
  8. 饥饿:某些线程可能会永远无法获得所需的资源,因为其他线程总是优先获得资源。
    为了解决线程安全问题,开发者需要采用适当的同步机制(如锁、信号量、条件变量等),以确保多线程之间的协调和互斥,防止竞态条件和并发问题的发生。此外,选择合适的数据结构和算法,以及遵循最佳的并发编程实践,也是确保线程安全的关键。

(三)生产者消费者模式

生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻所谓生产者消费者问题,实际上主要是包含了两类线程:
①一类是生产者线程用于生产数据
②一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库:
①生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
②消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
生产者消费者模式.png
(生产牛奶的人直接将牛奶放在奶箱,消费者直接去奶箱找牛奶喝)

为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中
(理想状态是生产者生产了数据之后,消费者获取数据进行消费。但是可能会出现生产者生产了数据,消费者还没有来得及消费,这个时候需要提醒他进行消费,或者消费者在消费的时候发现数据还没有生产出来,要提醒生产者抓紧消费)

Object类的等待和唤醒方法(应该在同步中去使用):

方法名 说明
void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法
void notify() 唤醒正在等待对象监视器(锁对象)的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

生产者-消费者模式的优点

  1. 解耦
    由于有缓冲区的存在,生产者和消费者之间不直接依赖,耦合度降低。

  2. 支持并发
    由于生产者与消费者是两个独立的并发体,它们之间是通过缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区中拿数据接口,这样就不会因为彼此的处理速度而发生阻塞。(通过使用多个生产者和消费者线程,可以实现并发处理,提高系统的吞吐量和响应性)

  3. 支持忙闲不均
    缓冲区还有另一个好处:当数据生产过快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等消费者处理掉其他数据时,再从缓存区中取数据来处理。(通过使用缓冲区可以平衡生产者与消费者之间的速度差异,以及处理能力的不匹配)

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2023-2025 Annie
  • Visitors: | Views:

嘿嘿 请我吃小蛋糕吧~

支付宝
微信