java学习笔记——多线程
一、多线程基础知识
1.概念
(1)程序:静态的代码,存储在硬盘上的 .class 文件。
进程:正在运行的程序,是系统分配资源的最小单位。
线程:进程中的执行单元,是CPU调度的最小单位。
(2)多线程:一个进程中有多个线程并发执行,提高程序效率。
(3)主线程:每个Java程序至少有一个线程——main线程(主线程)。
(4)并发与并行:
二、创建线程方式1:继承Thread类
1.步骤
// 1. 继承 Thread 类 public class MyThread extends Thread { // 2. 重写 run() 方法(线程要执行的代码) @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("子线程:" + i); } } } // 3. 创建线程对象,调用 start() 启动 public class Test { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程(会自动调用 run() 方法) // 主线程的代码 for (int i = 0; i < 20; i++) { System.out.println("主线程:" + i); } } }ps:
start() 和 run() 的区别
start():启动新线程,新线程执行 run() 方法(正确)
run():直接调用,没有启动新线程,只是在当前线程执行(错误用法)
thread.run(); // 错误的!只是普通方法调用,没有新线程 thread.start(); // 正确的!启动新线程三、多线程运行原理
1.线程调度
Java线程调度是抢占式的,不是时间片轮转
哪个线程抢到CPU时间片,哪个线程就执行
每次执行结果可能不同(不可预测)
// 多次运行结果可能不同 Thread t1 = new MyThread("线程1"); Thread t2 = new MyThread("线程2"); t1.start(); t2.start(); // 输出顺序每次可能不同2.多线程内存原理
每个线程都有自己独立的栈空间
所有线程共享堆内存和方法区
四、Thread中常用方法
1.获取和设置线程名
public class MyThread extends Thread { @Override public void run() { // 获取当前线程名 String name = getName(); // 或:Thread.currentThread().getName() System.out.println(name + " 正在执行"); } } // 使用 MyThread t1 = new MyThread(); t1.setName("线程A"); // 设置线程名 t1.start(); MyThread t2 = new MyThread(); t2.setName("线程B"); t2.start();2.获取当前线程
// 在任何位置获取当前正在执行的线程对象 Thread current = Thread.currentThread(); System.out.println("当前线程:" + current.getName());3.sleep() 休眠
// 让当前线程暂停执行指定毫秒 System.out.println("开始"); Thread.sleep(3000); // 休眠3秒(需要处理InterruptedException) System.out.println("3秒后执行");使用场景:模拟耗时操作、控制执行节奏
例子
// 倒计时 for (int i = 10; i >= 1; i--) { System.out.println(i); Thread.sleep(1000); // 每秒输出一个数 } System.out.println("发射!");五、优先级、守护、礼让、插入线程
1.线程优先级
// 设置优先级(1-10,默认5) t1.setPriority(Thread.MIN_PRIORITY); // 1(最低) t2.setPriority(Thread.NORM_PRIORITY); // 5(默认) t3.setPriority(Thread.MAX_PRIORITY); // 10(最高) // 获取优先级 int priority = t1.getPriority();注意:优先级高的线程抢占CPU的概率更高,但不保证一定先执行。
2.守护线程
// 守护线程:当所有非守护线程结束时,守护线程自动结束 // 如:垃圾回收线程(GC) Thread daemon = new MyThread(); daemon.setDaemon(true); // 设置为守护线程(必须在start之前设置) daemon.start();特点:
守护线程为其他线程提供服务
JVM中只剩守护线程时,JVM退出
3.礼让线程(yield)
public class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + ":" + i); Thread.yield(); // 礼让,暂停当前线程,让其他线程有机会执行 } } }注意:yield() 只是"建议"调度器让出CPU,不保证一定礼让。
4.插入线程(join)
// join():等待该线程执行完毕,再继续执行当前线程 public class Test { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); for (int i = 0; i < 5; i++) { System.out.println("主线程:" + i); if (i == 2) { t.join(); // t线程插入,主线程等待t执行完毕 } } } }六、创建线程方式2:实现Runnable接口
1. 步骤
// 1. 实现 Runnable 接口 public class MyRunnable implements Runnable { // 2. 重写 run() 方法 @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } // 3. 创建 Thread 对象,传入 Runnable,启动 public class Test { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread t1 = new Thread(runnable, "线程A"); Thread t2 = new Thread(runnable, "线程B"); t1.start(); t2.start(); } }2.Runnable 和 Thread 对比
推荐:实现 Runnable 接口(更灵活,符合面向接口编程)。
七、匿名内部类创建多线程
// 方式1:匿名内部类继承 Thread new Thread() { @Override public void run() { System.out.println("匿名Thread线程"); } }.start(); // 方式2:匿名内部类实现 Runnable new Thread(new Runnable() { @Override public void run() { System.out.println("匿名Runnable线程"); } }).start(); // Lambda 简化(JDK8+) new Thread(() -> { System.out.println("Lambda线程"); }).start();八、线程安全问题
当多个线程同时操作共享数据时,可能出现数据错乱。
// 卖票案例(线程不安全) public class Ticket implements Runnable { private int count = 100; // 共享的票数 @Override public void run() { while (count > 0) { // 模拟出票耗时 try { Thread.sleep(10); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + "卖第" + count + "张票"); count--; } } } // 可能出现的问题: // 1. 卖出重复的票(两个线程读到了相同的count值) // 2. 卖出0张票或负数(count=1时两个线程都通过了count>0的判断)九、同步代码块(解决线程安全)
1.synchronized 同步代码块
// 语法 synchronized(锁对象) { // 需要同步的代码(操作共享数据的代码) }2.改进卖票案例
public class Ticket implements Runnable { private int count = 100; private Object lock = new Object(); // 锁对象 @Override public void run() { while (true) { synchronized (lock) { // 同步代码块 if (count > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + "卖第" + count + "张票"); count--; } else { break; } } } } }3. 同步原理
线程A进入synchronized块 → 拿到锁 → 执行代码
线程B进入synchronized块 → 锁被占用 → 等待
线程A执行完毕 → 释放锁
线程B拿到锁 → 执行代码
关键:
- 同一把锁的同步代码块,同一时间只能有一个线程执行
- 同步保证了数据安全,但降低了效率
锁对象注意事项:
多个线程必须使用同一个锁对象
锁对象可以是任意引用类型对象
通常用 new Object() 或 this 或 类名.class
十、同步方法
1.语法
public synchronized 返回值 方法名() { // 同步方法的锁对象 // 非静态方法:this // 静态方法:类名.class }2.卖票案例(同步方法)
public class Ticket implements Runnable { private int count = 100; @Override public void run() { while (true) { if (!sellTicket()) break; // 卖完就退出 } } // 同步方法(锁对象是 this) public synchronized boolean sellTicket() { if (count > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + "卖第" + count + "张票"); count--; return true; } return false; } }3.静态同步方法
public static synchronized void method() { // 锁对象是:类名.class(如 Ticket.class) }十一、死锁
1.定义:两个或多个线程互相持有对方需要的锁,都在等待对方释放,导致永远阻塞。
// 死锁示例 public class DeadLock { static Object lockA = new Object(); static Object lockB = new Object(); public static void main(String[] args) { // 线程1:先拿lockA,再拿lockB new Thread(() -> { synchronized (lockA) { System.out.println("线程1:拿到lockA"); try { Thread.sleep(100); } catch (InterruptedException e) { } synchronized (lockB) { System.out.println("线程1:拿到lockB"); } } }).start(); // 线程2:先拿lockB,再拿lockA new Thread(() -> { synchronized (lockB) { System.out.println("线程2:拿到lockB"); try { Thread.sleep(100); } catch (InterruptedException e) { } synchronized (lockA) { System.out.println("线程2:拿到lockA"); } } }).start(); } } // 线程1持有lockA,等待lockB // 线程2持有lockB,等待lockA // → 互相等待,死锁!2.如何避免死锁?
按照相同的顺序获取锁
使用 tryLock() 带超时的锁
减少锁的嵌套使用
十二、线程生命周期
1.线程的六种状态
2.状态说明
// 获取线程状态 Thread t = new MyThread(); System.out.println(t.getState()); // NEW t.start(); System.out.println(t.getState()); // RUNNABLE