java线程入门复习

一、理解线程和进程 进程是操作系统 资源分配的最小单位,拥有独立的地址空间和系统资源。线程是 CPU 调度的最小单位,一个进程可以包含多个线程,它们共享进程的资源(如堆、方法区),但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。 在 Java 中,JVM 启动时会创建一个主线程来运行 main 方法,同时可能启动 GC 等后台线程。线程在底层通常 1:1 映射到操作系统内核线程,所以 Java 多线程能够利用多核实现真正的并行。 多线程的主要优势是提高程序吞吐量和响应速度,但也会带来竞态条件、死锁等问题,需要借助 synchronized、Lock、线程池等手段来管理。 二、java中创建线程三种的方式 一是继承thread类,二是实现runnable接口,三是实现callnable接口。前两种方式重写的方法没有返回值,第三个方式重写的方法有返回值,如果想让线程执行完后返回值,可以用第三种方式。因为java是单继承,所以第一种不能再继承其他类,很少用这个方式,通常用第二种方式创建线程。下面用大白话分别详细介绍这三种创建线程的java代码实现。 1.继承 Thread 类:自定义一个类继承Thread,重写run()方法,run()里写线程要做的事。在主线程业务代码中使用线程,直接创建自定义类的对象,然后调用start()方法启动线程。 2.实现 Runnable 接口:自定义一个类实现Runnable接口,重写run()方法,run()里写线程要做的事。在主线程业务代码中使用线程,先创建MyRunnable对象,再用构造方法把Runnable对象作为参数传给Thread,来创建Thread对象,作为线程对象,然后调用start()方法启动线程。 3.实现 Callable 接口:定义一个类实现Callable接口,泛型是返回值类型,重写call()方法,return返回值。在主线程业务代码中使用线程,先创建MyCallable对象,接着用FutureTask包装Callable(也是通过构造方法传参创建FutureTask对象),再用构造方法把FutureTask对象作为参数传给Thread,来创建Thread对象,作为线程对象,然后调用start()方法启动线程。 三、线程的 5 种状态 新建(New):刚 new 出来 Thread 对象,还没调用 start ()。 就绪(Runnable):调用了 start (),等待 CPU 分配时间片。 运行(Running):获得了 CPU 时间片,正在执行 run ()。 阻塞(Blocked):因为某些原因(比如 sleep ()、wait ()、等待锁)暂停执行,不占用 CPU。 终止(Terminated):run () 执行完,或者出现异常,线程结束。 线程进入阻塞态后先回到就绪态,再到运行态,最后终止 四、线程安全 线程安全:多个线程同时操作同一个共享资源时,不会出现数据错误,就是线程安全的。 怎么保证线程安全:使用 synchronized 关键字、使用 volatile 关键字、使用线程安全的集合类 下面用大白话讲这三个保证线程安全的方式

  1. 使用 synchronized 关键字:在while循环与共享资源判定条件之间,使用 synchronized 关键字修饰方法或者代码块,保证同一时间只有一个线程能执行。
  2. 使用 volatile 关键字:volatile 主要作用是保证变量的可见性和禁止指令重排。一个线程修改加了volatile的全局变量,其他线程能马上看到最新值。
  3. 使用线程安全的集合类: 因为集合还没复习,先不写,等明天复习完集合再回过头来补充 下面是AI参考

Java线程入门到应用

完全从「实际怎么用」出发,不讲太深入的原理,先会写、会用,能应付面试和简单项目 核心目标:搞懂「为什么要用线程」「线程怎么创建」「线程安全怎么保证」,能写简单的多线程代码


一、先搞懂:为什么要用线程?

简单说,线程就是让程序「同时做几件事」,提高效率,或者不阻塞主线程。 举两个你以后做项目肯定会遇到的场景:

场景1:不阻塞主线程(最常用)

比如你做一个员工管理系统,用户上传一个100MB的Excel文件,你需要解析文件并导入数据库。

  • 不用线程:用户点击上传后,页面会一直转圈,直到文件解析完,这期间用户什么都做不了,体验很差。
  • 用线程:用户点击上传后,马上返回“上传成功,正在后台导入”,然后开一个新线程在后台慢慢解析文件,用户可以继续操作其他功能。

场景2:并行计算,提高效率

比如你需要统计100万条数据的总和,单线程跑需要10秒。

  • 用多线程:开4个线程,每个线程统计25万条,最后汇总结果,可能只需要3秒。

二、Java线程的3种创建方式

方式1:继承Thread类(最简单,但不推荐,因为Java单继承)

java
复制代码
// 1. 定义一个类继承Thread class MyThread extends Thread { @Override public void run() { // 这里写线程要执行的代码 for (int i = 0; i < 5; i++) { System.out.println("线程1执行:" + i); try { Thread.sleep(100); // 模拟耗时操作,休眠100毫秒 } catch (InterruptedException e) { e.printStackTrace(); } } } } // 2. 使用线程 public class ThreadDemo { public static void main(String[] args) { MyThread t1 = new MyThread(); t1.start(); // 注意:必须调用start(),不能直接调用run()! // 主线程也执行 for (int i = 0; i < 5; i++) { System.out.println("主线程执行:" + i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }

关键点

  • 必须重写run()方法,run()里写线程要做的事。
  • 必须调用start()方法启动线程,不能直接调用run()(直接调用run()就是普通方法调用,不会开新线程)。

方式2:实现Runnable接口(推荐,更灵活)

java
复制代码
// 1. 定义一个类实现Runnable接口 class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程2执行:" + i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 2. 使用线程 public class RunnableDemo { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread t2 = new Thread(myRunnable); // 把Runnable对象传给Thread t2.start(); // 主线程 for (int i = 0; i < 5; i++) { System.out.println("主线程执行:" + i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }

为什么推荐这种方式?

  • Java是单继承,继承了Thread就不能继承其他类了,但实现Runnable接口还可以继承其他类。
  • 适合多个线程共享同一个资源(后面讲线程安全会用到)。

方式3:实现Callable接口(可以返回结果,用得少但要知道)

前两种方式run()没有返回值,如果你需要线程执行完返回一个结果(比如计算总和),就用Callable。

java
复制代码
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; // 1. 定义一个类实现Callable接口,泛型是返回值类型 class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } return sum; // 返回计算结果 } } // 2. 使用线程 public class CallableDemo { public static void main(String[] args) throws Exception { MyCallable myCallable = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(myCallable); // 用FutureTask包装Callable Thread t3 = new Thread(futureTask); t3.start(); // 获取线程执行结果,get()会阻塞,直到线程执行完 Integer result = futureTask.get(); System.out.println("线程3计算结果:" + result); } }

三、线程的5种状态

不用死记硬背,理解就行:

  1. 新建(New):刚new出来Thread对象,还没调用start()。
  2. 就绪(Runnable):调用了start(),等待CPU分配时间片。
  3. 运行(Running):获得了CPU时间片,正在执行run()。
  4. 阻塞(Blocked):因为某些原因(比如sleep()、wait()、等待锁)暂停执行,不占用CPU。
  5. 终止(Terminated):run()执行完,或者出现异常,线程结束。

简单的状态转换例子

java
复制代码
Thread t = new Thread(() -> { System.out.println("线程运行中"); // 运行态 try { Thread.sleep(1000); // 进入阻塞态,休眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程结束"); // 回到就绪态,再到运行态,最后终止 }); t.start(); // 新建态 -> 就绪态

四、线程安全:最关键的部分

什么是线程安全?

多个线程同时操作同一个共享资源时,不会出现数据错误,就是线程安全的。 举个经典的例子:多线程卖票

java
复制代码
class TicketRunnable implements Runnable { private int ticket = 10; // 共享资源:10张票 @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票"); ticket--; try { Thread.sleep(100); // 模拟卖票耗时 } catch (InterruptedException e) { e.printStackTrace(); } } } } public class TicketDemo { public static void main(String[] args) { TicketRunnable ticketRunnable = new TicketRunnable(); // 开3个线程同时卖票 new Thread(ticketRunnable, "窗口1").start(); new Thread(ticketRunnable, "窗口2").start(); new Thread(ticketRunnable, "窗口3").start(); } }

运行结果会出现问题

  • 可能会出现同一张票被卖多次。
  • 可能会出现卖第0张票甚至负数票。

这就是线程不安全,因为多个线程同时操作ticket变量,没有同步。


怎么保证线程安全?

1. 使用synchronized关键字(最常用,必须会)

可以修饰方法或者代码块,保证同一时间只有一个线程能执行。 修改上面的卖票代码

java
复制代码
class TicketRunnable implements Runnable { private int ticket = 10; @Override public void run() { while (true) { // 用synchronized修饰代码块,this是锁对象 synchronized (this) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票"); ticket--; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } } }

关键点

  • synchronized (锁对象):锁对象可以是任意对象,但多个线程必须用同一个锁对象才有效。
  • 也可以直接修饰方法:public synchronized void sellTicket(),此时锁对象是this

2. 使用volatile关键字(保证可见性,不保证原子性)

之前背八股文时提到过,volatile主要作用是:

  • 保证变量的可见性:一个线程修改了变量,其他线程能马上看到最新值。
  • 禁止指令重排

举个简单的例子

java
复制代码
class VolatileDemo { // 不加volatile的话,线程2可能永远看不到flag的变化 private volatile boolean flag = false; public void changeFlag() { flag = true; System.out.println("flag已改为true"); } public void checkFlag() { while (!flag) { // 一直循环,直到flag变为true } System.out.println("检测到flag变为true,结束循环"); } } public class Main { public static void main(String[] args) { VolatileDemo demo = new VolatileDemo(); // 线程1:修改flag new Thread(() -> { try { Thread.sleep(1000); // 先休眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } demo.changeFlag(); }).start(); // 线程2:检测flag new Thread(() -> { demo.checkFlag(); }).start(); } }

注意:volatile不保证原子性,比如count++这种操作,用volatile还是会有线程安全问题,必须用synchronized或者原子类。


3. 使用线程安全的集合类

之前背的:ArrayList、HashMap是线程不安全的,多线程环境下要用线程安全的替代类。

线程不安全线程安全替代说明
ArrayListVector(不推荐)或 CopyOnWriteArrayListCopyOnWriteArrayList性能更好,写时复制
HashMapHashtable(不推荐)或 ConcurrentHashMapConcurrentHashMap性能更好,分段锁(JDK1.7)或CAS+synchronized(JDK1.8)
HashSetCopyOnWriteArraySet基于CopyOnWriteArrayList实现

举个ConcurrentHashMap的例子

java
复制代码
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapDemo { public static void main(String[] args) throws InterruptedException { // 用ConcurrentHashMap代替HashMap Map<String, Integer> map = new ConcurrentHashMap<>(); // 开10个线程,每个线程向map中放1000个元素 Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { final int threadIndex = i; threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { map.put(threadIndex + "-" + j, j); } }); threads[i].start(); } // 等待所有线程执行完 for (Thread thread : threads) { thread.join(); } System.out.println("map的大小:" + map.size()); // 应该是10000,用ConcurrentHashMap不会出错 } }

五、线程的常用方法(简单了解,会用就行)

  1. Thread.sleep(long millis):让当前线程休眠指定毫秒数,进入阻塞态。
  2. Thread.currentThread():获取当前正在执行的线程对象。
  3. join():等待该线程执行完,比如t.join(),主线程会等t执行完再继续。
  4. setName(String name)/getName():设置/获取线程名字。
  5. setPriority(int priority)/getPriority():设置/获取线程优先级(1-10,默认5,优先级高只是获得CPU时间片的概率高,不代表一定先执行)。

六、给你的练习建议(动手写3个小例子,马上就熟了)

  1. 多线程卖票:用synchronized修改上面的代码,确保不会出现重复票和负数票。
  2. 模拟文件上传:主线程返回“上传成功”,开一个新线程模拟解析文件(循环打印“正在解析第X行”)。
  3. ConcurrentHashMap练习:用ConcurrentHashMap代替HashMap,开多个线程同时向map中放元素,验证线程安全。

七、面试常问的线程问题

  1. 线程的三种创建方式及区别?
  2. 线程的5种状态及转换?
  3. 什么是线程安全?怎么保证线程安全?
  4. synchronized和volatile的区别?
  5. ArrayList和CopyOnWriteArrayList的区别?
  6. HashMap和ConcurrentHashMap的区别?
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
凌空零
作者分享
口述学习内容 5.8 事务隔离级别:为解决多个事务并行操作数据库可能出现脏读、不可重复读与幻读这三种问题,得出了四种事务级别,作为不同场景下的事务问题解决方案。 读未提交;问题全有。 脏读:读到了别人已修改但还没提交的数据,后面别人的事务可能回滚,读到的就成了假数据。 不可重复读:同一事务内,两次读数据的值不同,因为期间被别人修改了。 幻读:同一事务内,两次查询数据的行数不同,因为期间数据被别人增加或删除了。 读已提交:通过锁表?将还没提交的行锁起来,解决脏读。 可重复读:通过给数据库加版本号,同一事务内读到的数据是同一个版本的数据库,解决了不可重复读。 串行:通过锁数据库?同一时间只有一个事务内操作数据库,解决了幻读。 5.7 mysql:DDL、DML。单表函数、多表函数:聚合函数。 项目:1.黑马苍穹外卖实现员工登录、新增员工功能。2.登录功能将明文登录密码转为密文存入数据库的功能。3.通过转换工具将员工DTO的属性拷贝到员工实体中,实现便捷编写代码。4.新增员工的用户名有唯一索引且与数据库表中已有数据重复而发现运行时异常报错,通过编写自定义用户已存在异常,编写全局异常处理器捕获自定义异常,实现异常处理。且统一异常处理,代码可复用、异常与业务代码分离,代码可读性高。
2
java高级与sping的学习
3
Spring 框架实现“统一逻辑处理”的两种核心方式
3
java集合复习
3
集合的迭代器遍历复习
2
下载 APP