Java线程基础
线程
1.什么是程序?
是为了完成特定任务,用某种语言编写的一组指令的集合
进程
- 1.进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
- 2.进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。
什么是线程?
1.线程是由进程创建的,是进程的一个实体
2.一个进程可以有多个线程
注意
- 1.单线程:同一个时刻,只允许执行一个线程。
2.多线程:同一个时刻,可以执行多个线程,- 如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。
- 3.并发:同一个时间,多个人任务交替执行,造出一种“貌似同时”的错觉。 简单的说,单核CPU实现的多任务就是并发。
- 例子:一个CPU轮流分配资源给QQ和迅雷,使得看似两者同时运行。
- 4.并行:同一个时间,多个任务同时执行。多核CPU可以实现并行。
- 例子:一个CPU分配资源给QQ和迅雷,同时另一个CPU分配资源给jack(开车)和mary(打电话)使得多个任务真正的同时执行。
实现线程的方法
1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法
子线程与主线程的关系
- 1.创建关系:在多线程编程中,主线程(也称为父线程)通常负责创建子线程。主线程启动后,可以创建一个或多个子线程来执行特定的任务。
- 2.独立执行:一旦子线程被创建,它就独立于主线程运行。这意味着子线程有自己的执行路径和生命周期,主线程可以继续执行其他任务,而子线程可以同时执行。
- 3.资源共享:主线程和子线程可以共享进程内的资源,如内存空间、文件句柄等。但这也意味着需要妥善管理资源访问,避免竞态条件和数据不一致的问题。
- 4.同步与通信:主线程和子线程之间可能需要进行同步和通信。例如,主线程可能需要等待子线程完成任务后才继续执行,或者子线程需要向主线程报告任务完成情况。
- 5.依赖关系:在某些情况下,子线程的生命周期依赖于主线程。如果主线程结束,根据具体的线程策略,可能会导致子线程也被强制结束。
- 6.异常处理:如果子线程中发生未捕获的异常,这可能不会直接影响主线程,除非主线程有特定的异常处理机制来处理子线程的异常。
- 7.终止方式:主线程和子线程的终止方式可能不同。主线程通常负责整个程序的结束,而子线程可能需要通过特定的线程控制方法(如中断)来终止。
start启动线程
继承thread类
class MyThread extends Thread {
public void run() {
// 线程要执行的代码
System.out.println("线程正在运行");
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 启动线程
}
}
// cat. start(); // 启动线程-> 最终会执行cat的run方法
cat.run(); // run方法就是一个普通的方法,没有真正的启动一个线程,就会把run方法执行完毕,才向下执行
// 说明:当main线程启动一个子线程 Thread-0,主线程不会阻塞,会继续执行。
// 这时 主线程和子线程是交替执行。
实现runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程要执行的代码
System.out.println("线程正在运行");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
1.从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口。
2.实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。
实例:购票问题
package com.edu.thread;
import com.edu.thread.SellTicket01;
/*
* @author: HP
* @date: 2024/9/28/10:23
* @description: TODO
* @version: 1.0
* */
public class ShellTicket {
public static void main(String[] args) {
SellTicket01 ticket01 = new SellTicket01();
SellTicket01 ticket02 = new SellTicket01();
SellTicket01 ticket03 = new SellTicket01();
ticket01.start();
ticket02.start();
ticket03.start();
}
}
package com.edu.thread;
import com.edu.thread.SellTicket01;
/*
* @author: HP
* @date: 2024/9/28/10:23
* @description: TODO
* @version: 1.0
* */
public class ShellTicket {
public static void main(String[] args) {
SellTicket01 ticket01 = new SellTicket01();
SellTicket01 ticket02 = new SellTicket01();
SellTicket01 ticket03 = new SellTicket01();
ticket01.start();
ticket02.start();
ticket03.start();
}
}
运行结果:出现-1,-2张票
原因是:出现票数超卖现象
在多线程环境中,如果多个线程尝试同时修改同一个变量,而没有适当的同步机制,就可能出现竞态条件(race condition)。这会导致一些线程读取到过时的值,或者在更新变量时覆盖了其他线程的更改,从而产生不正确的结果。
为了解决这个问题,你可以采取以下措施之一:
- 1.使用同步代码块:在
SellTicket01
类的run
方法中,使用synchronized
关键字来同步对票数的操作。这样可以确保一次只有一个线程可以进入同步块执行代码。 - 2.使用
AtomicInteger
:使用java.util.concurrent.atomic.AtomicInteger
替代普通的int
类型来存储票数。AtomicInteger
提供了原子操作,可以保证即使在多线程环境下,对它的操作也是线程安全的。 - 3.使用
ReentrantLock
:使用显式的锁(如ReentrantLock
)来控制对票数的访问。这允许你更细致地控制锁的获取和释放,以及提供更高级的锁定机制,如尝试获取锁而不阻塞当前线程。 - 4.使用
volatile
关键字:虽然volatile
不能保证复合操作(如先读取再写入)的原子性,但它可以确保变量的读写操作对所有线程都是可见的,从而在某些情况下减少问题。
线程终止
1.线程完成任务之后自动终止;
2.还可以通过使用变量来控制run方法退出的方式停止线程
package com.edu.thread;
/*
* @author: HP
* @date: 2024/9/28/10:45
* @description: TODO
* @version: 1.0
* */
public class ThreadExit {
public static void main(String[] args) {
T t = new T();
t.start();
//主线程修改loop控制子线程
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t.shtoop(false);
}
}
class T extends Thread {
int c= 0;
private boolean loop= true;
public void run() {
while(loop) {
try {
Thread.sleep(50); // 让当前线程休眠50ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AThread 运行中..." + (++c));
}
}
public void shtoop(boolean loop) {
this.loop = loop;
}
}
线程方法
第一组
1.setName //设置线程名称,使之与参数 name 相同
2.getName //返回线程的名称
3.start //使该线程开始执行;Java 虚拟机底层调用该线程的start0 方法
4.run //调用线程对象 run 方法;
5.setPriority //更改线程的优先级
6.getPriority //获取线程的优先级
7.sleep //在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
8.interrupt //中断线程
第二组
- 1.yield: 线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以一定礼让成功。
- 2.join: 线程的插队。插队的线程一旦插队成功,则肯定先执行插入的线程所有的任务。
案例:创建一个子线程,每隔1s输出hello,输出20次。要求:两个线程同时执行,当主线程输出5次后,就让子线程运行完毕,主线程再继续。
用户线程和守护线程
- 1.用户线程:也叫工作线程,当线程的任务执行完成或通知方式结束。
- 2.守护线程:一般是为工作线程服务的,当所有用户线程结束,守护线程自动结束。
- 3.常见的守护线程:垃圾回收机制。
线程的生命周期
- 1.新建(New):当线程对象被创建时,例如使用
new Thread()
,线程处于新建状态。此时,线程尚未开始执行。 - 2.就绪(Runnable):调用线程对象的
start()
方法后,线程进入就绪状态。此时,线程已经准备好运行,等待CPU调度。 - 3.运行(Running):当线程获得CPU时间片后,它开始执行
run()
方法中的代码。一个线程在任何时刻只能处于运行状态之一。 - 4.阻塞(Blocked):线程因为某些原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会再次获得CPU时间片运行。阻塞的情况包括:
- 等待阻塞:调用
wait()
方法,使线程等待某个条件的发生。 - 同步阻塞:线程在获取对象的同步锁时,如果该同步锁被其他线程占用,则该线程会被阻塞。
- 其他阻塞:如执行
Thread.sleep()
或者等待 I/O 操作完成。
- 等待阻塞:调用
- 5.等待(Waiting):线程进入等待状态,等待其他线程执行一个(或多个)特定操作。等待状态的线程不能被CPU调度执行。等待状态的线程可以被
notify()
或notifyAll()
方法唤醒。 - 6.超时等待(Timed Waiting):线程在指定的时间内等待。例如,调用
Thread.sleep(long millis)
方法使线程休眠指定的毫秒数。 - 7.终止(Terminated):线程的
run()
方法执行完毕,或者因异常退出了run()
方法,线程就处于终止状态。线程一旦终止,就不能再次启动或复用。
线程同步
- 1.在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
- 2.也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
package com.edu.thread;
//继承thread
public class SellTicket02 extends Thread {
private static int num = 100;
private boolean loog = true;
public synchronized void sell() {
if (num <= 0) {
System.out.println("售票结束");
loog = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名称" + Thread.currentThread().getName() + "剩余票数" + (--num));
}
public void run() {
while(loog) {
sell();
}
}
}
互斥锁
- 1.Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
- 2.每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 3.关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。
- 4.同步的局限性:导致程序的执行效率要降低。
- 5.同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)。
- 6.同步方法(静态的)的锁为当前类本身。
//1. public synchronized static void m1() { } 锁是加在 SellTicket03.class
//2. 如果在静态方法中,实现一个同步代码块。
public synchronized static void m1() {
public static void m2() {
synchronized (SellTicket03.class) {
System.out.println("m2");
}
}
}
注意事项和细节
- 1.同步方法如果没有使用static修饰:默认锁对象为this
- 2.如果方法使用static修饰,默认锁对象:当前类.class
- 3.实现的落地步骤:需要先分析上锁的代码选择同步代码块或同步方法要求多个线程的锁对象为同一个即可!
线程的死锁
线程死锁是指在多线程环境中,两个或两个以上的线程因为争夺资源而无限等待对方释放资源,导致所有相关线程都无法继续执行的情况。死锁通常发生在以下四个必要条件同时满足时:
- 1.互斥条件:资源不能被多个线程共享,即一次只有一个线程可以使用资源。
- 2.请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 3.不可剥夺条件:线程已获得的资源在未使用完之前,不能被其他线程强行剥夺,只能由该线程自愿释放。
- 4.循环等待条件:存在一种线程资源的循环等待关系,即线程集合{P0, P1, P2, …, Pn}中,P0等待P1持有的资源,P1等待P2持有的资源,…,Pn等待P0持有的资源。
为了避免死锁,可以采取以下策略:
- 破坏互斥条件:尽可能使资源能被共享,或者使用锁粒度更细的锁(如读写锁)。
- 破坏请求与保持条件:要求线程在开始执行前一次性申请所有需要的资源。
- 破坏不可剥夺条件:当一个已经持有其他资源的线程请求新资源而不能立即得到时,释放其占有的资源。
- 破坏循环等待条件:对资源进行排序,并规定所有线程必须按序请求资源。
释放锁
- 当前线程的同步方法、同步代码块执行结束
- 案例:上厕所,完事出来
- 当前线程在同步代码块、同步方法中遇到了break、return。
- 案例:没有正常的完事,经他修改,不得出来
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 案例:没有正常的完事,发现忘带纸,不得已出来
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
- 案例:没有正常的完事,觉得需要酝酿下,所以出来等会再进去
1.同步方法执行完毕:当一个同步方法执行完毕时,它会自动释放当前对象的锁。这是因为同步方法的结束意味着线程对共享资源的访问已经完成。
2.同步代码块执行完毕:在同步代码块中,一旦代码块执行完毕,锁也会被释放。同步代码块提供了更细粒度的控制,允许你指定哪些代码需要同步。
3.显式调用 wait() 方法:当线程执行到同步代码块或同步方法中,并调用当前对象的 wait() 方法时,线程会释放锁并进入等待状态,直到其他线程调用同一个对象的 notify() 或 notifyAll() 方法。调用 wait() 方法后,线程会立即释放锁。
4.线程中断:如果线程在等待锁的过程中被中断(调用 interrupt() 方法),它会抛出 InterruptedException 异常,并且在异常处理中释放锁。
5.异常发生:如果同步代码块中发生异常并且没有被捕获处理,线程会退出同步代码块,锁也会被释放。
6.使用 ReentrantLock:在使用显式锁(如 ReentrantLock)的情况下,需要在适当的时候调用 unlock() 方法来释放锁。显式锁提供了比内置同步机制更灵活的控制,包括尝试获取锁而不阻塞(tryLock())、条件变量(newCondition())等。
在Java中,以下情况通常会导致锁不被释放:
-
1.无限循环:如果同步代码块或同步方法中存在无限循环,且没有适当的退出条件或中断机制,那么线程将永远占用锁,导致锁无法释放。
-
2.长时间运行的任务:如果同步代码块或方法中执行的是一个长时间运行的任务,而没有在适当的时候释放锁,那么其他线程将长时间等待。
-
3.未捕获的异常:如果在同步代码块或方法中发生了未捕获的异常,并且异常导致线程退出,那么锁可能不会被释放。虽然通常情况下,异常会导致同步代码块退出,从而释放锁,但如果异常发生在锁获取之后、同步代码块执行之前,锁可能不会被释放。
-
4.死循环中的
wait()
:如果在同步代码块中调用了wait()
方法,并且该wait()
调用没有被notify()
或notifyAll()
正确唤醒,线程将无限期等待,锁不会被释放。 -
5.线程中断:如果线程在等待锁的过程中被中断,它会抛出
InterruptedException
并释放锁。但如果线程在捕获到InterruptedException
后没有正确处理(例如重新中断线程),可能会导致锁不被释放。 -
6.锁的滥用:在某些情况下,开发者可能会错误地使用锁,例如在不应该同步的代码段上使用同步,或者在不应该持有锁的时候持有锁,这可能导致锁的不必要占用。
-
7.显式锁的使用不当:对于显式锁(如
ReentrantLock
),如果在使用后没有调用unlock()
方法,或者在finally
块中没有正确释放锁,也可能导致锁不被释放。 -
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁。
案例:上厕所,太困了,在坑位上眯了一会儿 -
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。
提示:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用