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 关键字、使用线程安全的集合类 下面用大白话讲这三个保证线程安全的方式
- 使用 synchronized 关键字:在while循环与共享资源判定条件之间,使用 synchronized 关键字修饰方法或者代码块,保证同一时间只有一个线程能执行。
- 使用 volatile 关键字:volatile 主要作用是保证变量的可见性和禁止指令重排。一个线程修改加了volatile的全局变量,其他线程能马上看到最新值。
- 使用线程安全的集合类: 因为集合还没复习,先不写,等明天复习完集合再回过头来补充 下面是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种状态
不用死记硬背,理解就行:
- 新建(New):刚new出来Thread对象,还没调用start()。
- 就绪(Runnable):调用了start(),等待CPU分配时间片。
- 运行(Running):获得了CPU时间片,正在执行run()。
- 阻塞(Blocked):因为某些原因(比如sleep()、wait()、等待锁)暂停执行,不占用CPU。
- 终止(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是线程不安全的,多线程环境下要用线程安全的替代类。
| 线程不安全 | 线程安全替代 | 说明 |
|---|---|---|
| ArrayList | Vector(不推荐)或 CopyOnWriteArrayList | CopyOnWriteArrayList性能更好,写时复制 |
| HashMap | Hashtable(不推荐)或 ConcurrentHashMap | ConcurrentHashMap性能更好,分段锁(JDK1.7)或CAS+synchronized(JDK1.8) |
| HashSet | CopyOnWriteArraySet | 基于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不会出错 } }
五、线程的常用方法(简单了解,会用就行)
- Thread.sleep(long millis):让当前线程休眠指定毫秒数,进入阻塞态。
- Thread.currentThread():获取当前正在执行的线程对象。
- join():等待该线程执行完,比如
t.join(),主线程会等t执行完再继续。 - setName(String name)/getName():设置/获取线程名字。
- setPriority(int priority)/getPriority():设置/获取线程优先级(1-10,默认5,优先级高只是获得CPU时间片的概率高,不代表一定先执行)。
六、给你的练习建议(动手写3个小例子,马上就熟了)
- 多线程卖票:用synchronized修改上面的代码,确保不会出现重复票和负数票。
- 模拟文件上传:主线程返回“上传成功”,开一个新线程模拟解析文件(循环打印“正在解析第X行”)。
- ConcurrentHashMap练习:用ConcurrentHashMap代替HashMap,开多个线程同时向map中放元素,验证线程安全。
七、面试常问的线程问题
- 线程的三种创建方式及区别?
- 线程的5种状态及转换?
- 什么是线程安全?怎么保证线程安全?
- synchronized和volatile的区别?
- ArrayList和CopyOnWriteArrayList的区别?
- HashMap和ConcurrentHashMap的区别?
