Java基础-线程
一、线程概述
进程与线程:
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
什么是多线程呢?即就是一个程序中有多个线程在同时执行。
单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。
程序的运行原理:
分时调度:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
主线程:
jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。
那么,能否实现一个主线程负责执行其中一个循环,再由另一个线程负责其他代码的执行,最终实现多部分代码同时执行的效果?
Thread类是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
创建新执行线程有两种方法:
一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。
一种方法是声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。
1.继承Thread类
构造方法:
常用方法:
public class Demo01 { public static void main(String[] args) { //创建自定义线程对象 MyThread mt = new MyThread("新的线程!"); //开启新线程 mt.start(); //在主方法中执行for循环 for (int i = 0; i < 10; i++) { System.out.println("main线程!"+i); } } } //自定义线程类 public class MyThread extends Thread { //定义指定线程名称的构造方法 public MyThread(String name) { //调用父类的String参数的构造方法,指定线程的名称 super(name); } /** * 重写run方法,完成该线程执行的逻辑 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()+":正在执行!"+i); } } }
2. 实现Runnable接口
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。
Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。
构造方法:
常用方法:
public class Demo02 { public static void main(String[] args) { //创建线程执行目标类对象 Runnable runn = new MyRunnable(); //将Runnable接口的子类对象作为参数传递给Thread类的构造函数 Thread thread = new Thread(runn); Thread thread2 = new Thread(runn); //开启线程 thread.start(); thread2.start(); for (int i = 0; i < 10; i++) { System.out.println("main线程:正在执行!"+i); } } } //自定义线程执行任务类 public class MyRunnable implements Runnable{ //定义线程要执行的run方法逻辑 @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("我的线程:正在执行!"+i); } } }
3. 多线程内存图解
多线程执行时,到底在内存中是如何运行的呢?
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
4. 获取线程名称
class MyThread extends Thread {
//继承Thread MyThread(String name){ super(name); } //复写其中的run方法 public void run(){ for (int i=1;i<=20 ;i++ ){ System.out.println(Thread.currentThread().getName()+",i="+i); } } } class ThreadDemo { public static void main(String[] args) { //创建两个线程任务 MyThread d = new MyThread(); MyThread d2 = new MyThread(); d.run();//没有开启新线程, 在主线程调用run方法 d2.start();//开启一个新线程,新线程调用run方法 } }
5. 线程的匿名内部类的使用。
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
//方式1:创建线程对象时,直接重写Thread类中的run方法 new Thread() { public void run() { for (int x = 0; x < 40; x++) { System.out.println(Thread.currentThread().getName() + "...X...." + x); } } }.start(); //方式2:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法 Runnable r = new Runnable() { public void run() { for (int x = 0; x < 40; x++) { System.out.println(Thread.currentThread().getName() + "...Y...." + x); } } }; new Thread(r).start();
二、多线程中线程的安全问题
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟。
//模拟窗口 public class ThreadDemo { 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(); } } //模拟票 public class Ticket implements Runnable { //共100票 int ticket = 100; @Override public void run() { //模拟卖票 while(true){ if (ticket > 0) { //模拟选坐的操作 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--); } } }
运行得到的结果会出现卖重票或者负数的情况。
其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
使用线程同步来解决线性安全问题:线程同步的方式有两种
方式1:同步代码块
方式2:同步方法
同步代码块方式:
同步代码块: 在代码块声明上 加上synchronized
synchronized (锁对象) {
可能会产生线程安全问题的代码
}
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
public class Ticket implements Runnable { //共100票 int ticket = 100; //定义锁对象 Object lock = new Object(); @Override public void run() { //模拟卖票 while(true){ //同步代码块 synchronized (lock){ if (ticket > 0) { //模拟电影选坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--); } } } } }
同步方法方式:
在方法声明上加上synchronized:同步方法中的锁对象是 this
public synchronized void method(){
可能会产生线程安全问题的代码
}
public class Ticket implements Runnable { //共100票 int ticket = 100; //定义锁对象 Object lock = new Object(); @Override public void run() { //模拟卖票 while(true){ //同步方法 method(); } } //同步方法,锁对象this public synchronized void method(){ if (ticket > 0) { //模拟选坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--); } } } //静态同步方法: 在方法声明上加上static synchronized public static synchronized void method(){ 可能会产生线程安全问题的代码 } 静态同步方法中的锁对象是 类名.class
三、死锁问题
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
synchronzied(A锁){ synchronized(B锁){ } }
//死锁情况演示
//定义锁对象类 public class MyLock { public static final Object lockA = new Object(); public static final Object lockB = new Object(); } //线程任务类 public class ThreadTask implements Runnable { int x = new Random().nextInt(1);//0,1 //指定线程要执行的任务代码 @Override public void run() { while(true){ if (x%2 ==0) { //情况一 synchronized (MyLock.lockA) { System.out.println("if-LockA"); synchronized (MyLock.lockB) { System.out.println("if-LockB"); System.out.println("if大口吃肉"); } } } else { //情况二 synchronized (MyLock.lockB) { System.out.println("else-LockB"); synchronized (MyLock.lockA) { System.out.println("else-LockA"); System.out.println("else大口吃肉"); } } } x++; } } } //测试类 public class ThreadDemo { public static void main(String[] args) { //创建线程任务类对象 ThreadTask task = new ThreadTask(); //创建两个线程 Thread t1 = new Thread(task); Thread t2 = new Thread(task); //启动线程 t1.start(); t2.start(); } }
四、Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
Lock接口中常用的方法:
使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:
public class Ticket implements Runnable { //共100票 int ticket = 100; //创建Lock锁对象 Lock ck = new ReentrantLock(); @Override public void run() { //模拟卖票 while(true){ //synchronized (lock){ ck.lock(); if (ticket > 0) { //模拟选坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--); } ck.unlock(); //} } } }
五、等待唤醒机制
在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制所涉及到的方法:
wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?
因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。
输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:
1.当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
2.当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。
//实例演示 public class Resource { private String name; private String sex; private boolean flag = false; public synchronized void set(String name, String sex) { if (flag) try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } // 设置成员变量 this.name = name; this.sex = sex; // 设置之后,Resource中有值,将标记该为 true , flag = true; // 唤醒output this.notify(); } public synchronized void out() { if (!flag) try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } // 输出线程将数据输出 System.out.println("姓名: " + name + ",性别: " + sex); // 改变标记,以便输入线程输入数据 flag = false; // 唤醒input,进行数据输入 this.notify(); } } //输入线程任务类 public class Input implements Runnable { private Resource r; public Input(Resource r) { this.r = r; } @Override public void run() { int count = 0; while (true) { if (count == 0) { r.set("小明", "男生"); } else { r.set("小花", "女生"); } // 在两个数据之间进行切换 count = (count + 1) % 2; } } } //输出线程任务类 public class Output implements Runnable { private Resource r; public Output(Resource r) { this.r = r; } @Override public void run() { while (true) { r.out(); } } } //测试类 public class ResourceDemo { public static void main(String[] args) { // 资源对象 Resource r = new Resource(); // 任务对象 Input in = new Input(r); Output out = new Output(r); // 线程对象 Thread t1 = new Thread(in); Thread t2 = new Thread(out); // 开启线程 t1.start(); t2.start(); } }
六、线程池
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
1. 使用线程池方式—Runnable接口
通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。
Executors:线程池创建工厂类
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。
ExecutorService:线程池类。
Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤: 创建线程池对象 创建Runnable接口子类对象 提交Runnable接口子类对象 关闭线程池 //代码演示: public class ThreadPoolDemo { public static void main(String[] args) { //创建线程池对象 ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象 //创建Runnable实例对象 MyRunnable r = new MyRunnable(); //自己创建线程对象的方式 //Thread t = new Thread(r); //t.start(); ---> 调用MyRunnable中的run() //从线程池中获取线程对象,然后调用MyRunnable中的run() service.submit(r); //再获取个线程对象,调用MyRunnable中的run() service.submit(r); service.submit(r); //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中 //关闭线程池 //service.shutdown(); } } //Runnable接口实现类 public class MyRunnable implements Runnable { @Override public void run() { System.out.println("我要一个教练"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("教练来了: " +Thread.currentThread().getName()); System.out.println("教我游泳,交完后,教练回到了游泳池"); } }
2. 使用线程池方式—Callable接口
Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。
ExecutorService:线程池类。
<T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行线程中的call()方法。
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤: 创建线程池对象 创建Callable接口子类对象 提交Callable接口子类对象 关闭线程池 代码演示: public class ThreadPoolDemo { public static void main(String[] args) { //创建线程池对象 ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象 //创建Callable对象 MyCallable c = new MyCallable(); //从线程池中获取线程对象,然后调用MyRunnable中的run() service.submit(c); //再获取个教练 service.submit(c); service.submit(c); //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中 //关闭线程池 //service.shutdown(); } } //Callable接口实现类,call方法可抛出异常、返回线程任务执行完毕后的结果 public class MyCallable implements Callable { @Override public Object call() throws Exception { System.out.println("我要一个教练:call"); Thread.sleep(2000); System.out.println("教练来了: " +Thread.currentThread().getName()); System.out.println("教我游泳,交完后,教练回到了游泳池"); return null; } }
七、其他:
1. 线程休眠
调用sleep()方法,该方法需要一个参数用于指定线程休眠时间,单位为ms,通常用在run()方法内的循环中被使用,注意要使用try···catch包围。语法格式如下:Thread.sleep();
使用了sleep()方法的线程在一段时间内会醒来,但是并不能保证它醒来后进入运行状态,只能保证它进入就绪状态。
2. 线程加入
假设有一个线程A,现在需要插入线程B,并要求线程B先执行完毕,然后再继续执行线程A,此时可以使用join()方法来完成。当某个线程使用join()方法加入到另一个线程时,另一个线程会等待该线程执行完毕再继续执行。语法格式如下:Thread.join();
3. 线程的中断(stop/interrupt)
stop()方法可以停止线程,但JDK早已废除,同时也不建议使用stop()方法来停止一个线程的运行。可在run()方法中使用无限循环的形式,然后使用一个布尔型标记控制循环的停止。
如果线程因为使用了sleep()或wait()方法进入了就绪状态,这时可以使用Thread类中的interrupt()方法使线程离开run()方法,同时结束线程,但会抛出InterruptedException异常。语法格式如下:Thread.interrupt();
4. 线程礼让
Thread类中提供了一种礼让方法yield(),用于给当前正处于运行状态下的线程一个提醒,告知它可以将资源礼让给其他线程。
yield()方法使具有同样优先级的线程有进入可执行状态的机会,当当前线程放弃执行权时会再度回到就绪状态。
总结:生命周期
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?