Java 黑马程序员学习笔记(进阶篇28)

一、Properties

1. 概述

java.util.Properties 类继承自 Hashtable,是 Java 中用于处理配置文件的核心工具类。它以 键值对(key-value) 的形式存储数据,键和值均为字符串类型,常用于读取和写入配置文件(如 .properties 文件)。

2. Properties 基本操作
(1) 创建 Properties 对象
java 复制代码
Properties prop = new Properties();
(2) 设置属性(setProperty)
java 复制代码
prop.setProperty("username", "root");
prop.setProperty("password", "123456");
prop.setProperty("url", "jdbc:mysql://localhost:3306/test");
(3) 获取属性(getProperty)
java 复制代码
String username = prop.getProperty("username");
String password = prop.getProperty("password");
String driver = prop.getProperty("driver", "com.mysql.cj.jdbc.Driver"); // 带默认值
(4) 移除属性(remove)
java 复制代码
prop.remove("password");
(5) 遍历属性
java 复制代码
// 方式1:通过 keySet() 遍历
Set<String> keys = prop.stringPropertyNames();
for (String key : keys) {
    String value = prop.getProperty(key);
    System.out.println(key + "=" + value);
}

// 方式2:通过 entrySet() 遍历
Set<Map.Entry<Object, Object>> entries = prop.entrySet();
for (Map.Entry<Object, Object> entry : entries) {
    System.out.println(entry.getKey() + "=" + entry.getValue());
}
2. Properties 与配置文件
(1) 读取 .properties 文件
java 复制代码
Properties prop = new Properties();

try (InputStream in = new FileInputStream("config.properties")) {
    prop.load(in); // 从输入流加载配置
} catch (IOException e) {
    e.printStackTrace();
}

String username = prop.getProperty("username");
String password = prop.getProperty("password");
(2) 写入 .properties 文件
java 复制代码
Properties prop = new Properties();
prop.setProperty("username", "root");
prop.setProperty("password", "123456");
prop.setProperty("url", "jdbc:mysql://localhost:3306/test");

try (OutputStream out = new FileOutputStream("config.properties")) {
    // 第二个参数是注释,第三个参数是日期格式
    prop.store(out, "Database Configuration");
} catch (IOException e) {
    e.printStackTrace();
}

二、线程的创建方式(3 种核心方式)

1. 方式 1:继承 Thread 类
(1) 步骤
  • 自定义类继承Thread
  • 重写run()方法(定义线程执行的任务);
  • 创建子类对象,调用start()方法启动线程(注意:不可直接调用 run (),否则会以主线程方式执行)。
(2) 代码示例:
java 复制代码
package demo1;

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "HelloWorld");
        }
    }
}
java 复制代码
package demo1;

public class test1{
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程1");  // 不太理解
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}
关键逻辑 1: t1.setName("线程1");

这个方法的作用是给这个线程对象起一个名字

② 为什么需要给线程起名字?

  • 调试和日志:这是最主要的原因。当一个程序中有多个线程在同时运行时,你在控制台打印日志或使用调试器(Debugger)时,很难区分哪一行输出是来自哪个线程的。给线程起一个有意义的名字(比如 "用户登录验证线程"、"数据备份线程")可以让你一目了然,极大地提高调试效率。
  • 监控和管理:在一些复杂的应用中,你可能需要通过线程的名称来监控系统状态、进行性能分析或动态管理线程。
关键逻辑 2:为什么可以直接写getName()

在这段代码中,MyThread继承了Threadextends Thread)。而Thread类本身就提供了getName()方法,用于获取线程的名称(这是Thread类的内置方法)。

2. 方式 2:实现 Runnable 接口
(1) 步骤
  • 自定义类实现Runnable接口;
  • 重写run()方法(定义任务);
  • 创建Runnable实现类对象,作为参数传入Thread构造器;
  • 调用Thread对象的start()方法启动线程。
(2) 代码示例
java 复制代码
// 1. 实现Runnable接口(解耦任务与线程)
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }

    public static void main(String[] args) {
        // 2. 创建任务对象
        MyRunnable task = new MyRunnable();
        // 3. 传入Thread构造器,创建线程对象
        Thread t1 = new Thread(task, "线程1");
        Thread t2 = new Thread(task, "线程2");
        // 4. 启动线程
        t1.start();
        t2.start();
    }
}
(3) 方式 3:实现 Callable 接口(带返回值)
① 场景

需要获取线程执行结果(如多线程计算后汇总结果)。

步骤
  • 自定义类实现Callable<V>接口(V为返回值类型);
  • 重写call()方法(任务逻辑,可抛异常,有返回值);
  • 创建Callable实现类对象,包装为FutureTask(实现FutureRunnable接口,用于接收结果);
  • FutureTask传入Thread构造器,调用start()启动线程;
  • 调用FutureTask.get()方法获取返回值(会阻塞主线程,直到子线程执行完成)。
③ 代码示例:
java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1. 实现Callable,指定返回值类型为Integer
public class MyCallable implements Callable<Integer> {
    private int num; // 计算1~num的和

    public MyCallable(int num) {
        this.num = num;
    }

    // 2. 重写call(),有返回值、可抛异常
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= num; i++) {
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3. 创建Callable任务对象
        MyCallable task = new MyCallable(100);
        // 4. 包装为FutureTask(兼具Runnable和Future功能)
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        // 5. 启动线程
        new Thread(futureTask, "计算线程").start();

        // 6. 获取结果(主线程会阻塞,直到子线程返回结果)
        Integer result = futureTask.get();
        System.out.println("1~100的和:" + result); // 输出5050
    }
}
核心特点
  • 带返回值、可抛异常;
  • FutureTask.get()可取消任务(cancel())、判断任务是否完成(isDone())。

三、线程的常用 API

方法名 作用 注意事项
Thread.currentThread() 获取当前执行的线程对象 静态方法,常用于日志打印或线程身份判断
thread.setName(String) 设置线程名称 建议设置有意义的名称(如 "订单处理线程"),便于问题排查
thread.getName() 获取线程名称 默认名称:Thread-0Thread-1...
Thread.sleep(long millis) 让当前线程休眠指定毫秒数 静态方法,休眠时不会释放已持有的锁;需捕获InterruptedException
thread.join() 让调用者线程等待当前线程执行完毕 t1.join(),主线程会等待 t1 执行完再继续;可传超时时间(join(long)
Thread.yield() 让当前线程主动放弃 CPU,进入就绪状态 静态方法,仅为 "建议",CPU 是否调度其他线程由操作系统决定
thread.setPriority(int) 设置线程优先级(1~10,默认 5) 优先级仅为 "调度提示",高优先级线程不一定先执行(依赖 OS 调度)
Thread.interrupt() 中断线程(设置中断标志位) 不会直接终止线程,需线程内部判断Thread.interrupted()处理
(1) Thread.currentThread()getName()setName()(线程名称操作)

用于获取当前线程对象、获取 / 设置线程名称。

java 复制代码
public class ThreadApiDemo1 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            // 获取当前线程对象
            Thread currentThread = Thread.currentThread();
            // 获取默认线程名(格式:Thread-0、Thread-1...)
            System.out.println("默认线程名:" + currentThread.getName());
            // 自定义线程名称
            currentThread.setName("我的自定义线程");
            System.out.println("修改后线程名:" + currentThread.getName());
        });
        t.start();
    }
}
(2) Thread.sleep(long millis)(线程休眠)

让当前线程暂停执行指定时间(毫秒),常用于模拟耗时操作。

java 复制代码
public class ThreadApiDemo2 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + " 正在执行...");
                try {
                    // 休眠1秒(1000毫秒)
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "休眠演示线程");
        t.start();
    }
}
(3) setPriority(int)getPriority()(线程优先级)

设置 / 获取线程优先级(范围1~10,默认5)。优先级仅为调度 "建议",实际执行顺序仍由操作系统决定。

java 复制代码
public class ThreadApiDemo4 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() 
                    + "(优先级" + Thread.currentThread().getPriority() + ")执行");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "高优先级线程");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() 
                    + "(优先级" + Thread.currentThread().getPriority() + ")执行");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "低优先级线程");

        // 设置t1为最高优先级(10),t2为最低优先级(1)
        t1.setPriority(Thread.MAX_PRIORITY); // Thread.MAX_PRIORITY等价于10
        t2.setPriority(Thread.MIN_PRIORITY); // Thread.MIN_PRIORITY等价于1

        t1.start();
        t2.start();
    }
}
(4) Thread.yield()(主动让出 CPU)

建议性地让当前线程放弃 CPU 使用权,进入 "就绪状态",但最终是否调度其他线程由操作系统决定。

java 复制代码
public class ThreadApiDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " 执行中...");
                // 主动让出CPU,建议调度其他线程
                Thread.yield();
            }
        }, "yield演示线程");
        t.start();
    }
}
(5) thread.join()(线程等待)

调用者线程 等待目标线程执行完毕后再继续。例如:主线程等待子线程t1执行完,再执行后续逻辑。

java 复制代码
public class ThreadApiDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程t1 执行第 " + (i+1) + " 轮");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        Thread mainWaitThread = new Thread(() -> {
            try {
                // 主线程等待t1执行完毕
                t1.join();
                System.out.println("t1执行完毕,主线程继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "主线程-等待");

        t1.start();
        mainWaitThread.start();
    }
}

四、线程同步与线程安全

1. 线程安全问题的原因

① 线程安全问题指多线程并发访问共享资源时,出现结果与预期不符的情况(如超卖、余额计算错误),核心原因有 3 点:

(1) 抢占式执行:CPU 随机调度线程,导致线程执行顺序不确定;

(2) 共享资源:多个线程访问同一份可变资源(如共享变量、数据库记录);

(3) 非原子操作 :对共享资源的操作需多步完成(如i++,实际分为 "读 i→i+1→写 i" 三步),中间可能被其他线程打断。

② 代码示例(线程安全问题演示):

java 复制代码
package demo3;

public class MyThread extends Thread{

    static int ticket = 0;

    @Override
    public void run() {
        while (true) {
            if (ticket < 100) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
            } else {
                break;
            }
        }
    }
}
java 复制代码
package demo3;

public class test1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
2. 解决线程安全:synchronized 关键字

synchronized是 Java 内置的 "互斥锁",确保同一时间只有一个线程进入 "同步代码块",从而保证操作的原子性、可见性和有序性(避免指令重排序)。

① synchronized 的用法
用法 1:同步代码块(锁定指定对象)
java 复制代码
synchronized (锁对象) {
    // 需保证线程安全的代码(共享资源操作)
}

(1) 锁对象要求 :必须是 "多线程共享的对象"(如this、静态对象、Class 对象),若每个线程用独立对象,锁会失效。

(2) 代码示例(修复线程安全问题):

java 复制代码
package demo3;

public class MyThread extends Thread{

    static int ticket = 0;
    static Object lock = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }
            }
        }
    }
}
用法 2:同步实例方法(锁定当前对象this
java 复制代码
public synchronized void method() {
    // 线程安全的代码
}
  • 等价于:synchronized (this) { 方法体 }
  • 注意:若多个线程操作不同的实例对象 ,锁会失效(因this不同)。
用法 3:同步静态方法(锁定当前类的 Class 对象)
java 复制代码
public static synchronized void method() {
    // 线程安全的代码
}
  • 等价于:synchronized (当前类.class) { 方法体 }
  • 特点:Class 对象是全局唯一的,即使多个实例对象,也会共享同一把锁。
3. 解决线程安全:Lock 接口(JUC)

java.util.concurrent.locks.Lock是 JUC 提供的锁接口,比synchronized更灵活(可手动加锁 / 解锁、可中断、可超时获取锁),常用实现类是ReentrantLock(可重入锁)。

(1) ReentrantLock 的基本用法
java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private static int count = 0;
    // 创建ReentrantLock对象(默认非公平锁,可传true设为公平锁)
    private static final Lock LOCK = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < 1000; i++) {
            threads[i] = new Thread(() -> {
                try {
                    // 1. 手动加锁(必须在try块外或try块首,避免加锁前抛异常)
                    LOCK.lock();
                    // 2. 线程安全操作
                    count++;
                } finally {
                    // 3. 手动解锁(必须在finally中,确保锁一定释放,避免死锁)
                    LOCK.unlock();
                }
            });
        }
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        System.out.println("最终count:" + count); // 1000
    }
}
相关推荐
晨晖21 小时前
springboot的Thymeleaf语法
java·spring boot·后端
p***95001 小时前
【SpringBoot】日志文件
java·spring boot·spring
Oll Correct1 小时前
Excel基础操作(二)
笔记·excel
LFly_ice2 小时前
学习React-23-React-router
前端·学习·react.js
i***27952 小时前
【golang学习之旅】使用VScode安装配置Go开发环境
vscode·学习·golang
b***66612 小时前
【springboot】健康检查 监控
java·spring boot·后端
hd51cc2 小时前
文档与视图 学习笔记
笔记·学习
明洞日记2 小时前
【设计模式手册010】组合模式 - 树形结构的优雅处理
java·设计模式·组合模式
q***47182 小时前
Spring Boot 3.3.4 升级导致 Logback 之前回滚策略配置不兼容问题解决
java·spring boot·logback