Java 多线程学习笔记

Java 多线程学习笔记

一、什么是线程?

  • **进程:**是操作系统进行资源分配和调度的基本单位,是程序的一次执行过程。
    **例如:**当你打开一个应用程序,如浏览器、音乐播放器等,操作系统就会为其创建一个进程。
  • **线程:**是进程的一个执行单元,是 CPU 调度和分派的基本单位,一个进程可以包含多个线程。
    **例如:**在浏览器进程中,可以有多个线程同时运行,例如一个线程负责页面渲染,另一个线程负责网络请求。

多线程的优点:

  • 提高 CPU 利用率,充分利用多核处理器
    **例如:**下载软件使用多线程可以同时下载多个文件片段,充分利用带宽,提高下载速度。
  • 提高程序响应速度,避免单线程阻塞导致整个程序卡顿
    **例如:**GUI 程序使用多线程可以将耗时操作放在后台线程执行,避免界面卡顿。
  • 简化程序结构,将复杂任务分解成多个简单任务并发执行
    **例如:**网络爬虫可以使用多线程同时爬取多个网页,提高效率。

多线程的缺点:

  • 增加了程序设计的复杂度
    **例如:**需要考虑线程同步、数据共享等问题。
  • 线程同步和数据共享问题
    **例如:**多个线程同时修改共享数据可能导致数据不一致。
  • 线程上下文切换开销
    **案例:**CPU 在多个线程之间切换需要保存和恢复线程的上下文信息,会带来一定的开销。

二、创建和启动线程

Java 中创建线程主要有两种方式:

1. 继承 Thread 类

  • 定义一个继承 Thread 类的子类,重写 run() 方法。
  • 创建子类对象,调用 start() 方法启动线程。
public class ThreadExample1 extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("Thread " + Thread.currentThread().getId() + " is running.");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            ThreadExample1 thread = new ThreadExample1();
            thread.start();
        }
    }
}

案例解释:

  • ThreadExample1 类继承了 Thread 类,并重写了 run() 方法,定义了线程要执行的代码。
  • main() 方法中,创建了 5 个 ThreadExample1 对象,并分别调用 start() 方法启动线程。

2. 实现 Runnable 接口

  • 定义一个实现 Runnable 接口的类,实现 run() 方法。
  • 创建 Runnable 对象,将其作为参数传递给 Thread 构造方法创建 Thread 对象。
  • 调用 Thread 对象的 start() 方法启动线程。
public class ThreadExample2 implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("Thread " + Thread.currentThread().getId() + " is running.");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            ThreadExample2 runnable = new ThreadExample2();
            Thread thread = new Thread(runnable);
            thread.start();
        }
    }
}

案例解释:

  • ThreadExample2 类实现了 Runnable 接口,并实现了 run() 方法,定义了线程要执行的代码。
  • main() 方法中,创建了 5 个 ThreadExample2 对象,并将它们分别作为参数传递给 Thread 构造方法创建线程对象,然后调用 start() 方法启动线程。

推荐使用实现 Runnable 接口的方式创建线程,因为它更灵活,避免了单继承的限制。

三、线程的生命周期

Java 线程的生命周期包含以下几种状态:

  • **新建(New):**线程对象被创建,但还未调用 start() 方法。
    例如: Thread thread = new Thread();
  • **就绪(Runnable):**线程对象调用 start() 方法后,进入就绪状态,等待 CPU 调度。
    例如: thread.start();
  • **运行(Running):**CPU 开始执行线程的 run() 方法。
    例如: 当线程获取到 CPU 资源时,就会执行 run() 方法中的代码。
  • **阻塞(Blocked):**线程因为某些原因暂停执行,例如等待 I/O 操作完成、获取锁失败等。
    **例如:**线程调用 sleep() 方法、等待某个对象的锁等都会导致线程进入阻塞状态。
  • **死亡(Dead):**线程的 run() 方法执行完毕或者发生异常终止。
    例如: run() 方法执行完毕后,线程就会进入死亡状态。

四、线程同步

当多个线程访问共享资源时,需要进行同步控制,避免数据出现不一致的情况。Java 提供了以下几种同步机制:

1. synchronized 关键字

  • 修饰代码块: synchronized(object) {},object 为锁对象。
  • 修饰方法: synchronized void method() {},锁对象为当前对象 this。

案例:

public class Counter {
    private int count;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

案例解释:

  • Counter 类表示一个计数器,count 变量表示计数器的值。
  • increment() 方法使用 synchronized 关键字修饰,保证了多个线程同时调用该方法时,只有一个线程能够进入方法体,从而保证了计数器的线程安全。
  • getCount() 方法也使用了 synchronized 关键字修饰,保证了在读取计数器的值时,不会出现数据不一致的情况。

2. Lock 接口

  • ReentrantLock 类是 Lock 接口的常用实现类。
  • 使用 lock() 方法获取锁,unlock() 方法释放锁。

案例:

public class Counter {
    private int count;
    private ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

案例解释:

  • Counter 类表示一个计数器,count 变量表示计数器的值,lock 是一个 ReentrantLock 对象,用于控制对计数器的访问。
  • increment() 方法在修改 count 变量之前,先调用 lock.lock() 方法获取锁,修改完成后调用 lock.unlock() 方法释放锁。
  • getCount() 方法在读取 count 变量之前,也需要先获取锁,读取完成后释放锁。

Lock 接口相比 synchronized 关键字更加灵活,可以实现更细粒度的锁控制,例如可以实现公平锁、可中断锁等。

五、线程间通信

Java 提供了以下机制实现线程间通信:

1. wait()、notify()、notifyAll() 方法

  • 这些方法都是 Object 类的方法,必须在 synchronized 代码块中使用。
  • wait() 方法:使当前线程进入等待状态,释放锁。
  • notify() 方法:唤醒一个等待该锁的线程。
  • notifyAll() 方法:唤醒所有等待该锁的线程。

案例:生产者消费者模型

public class ProducerConsumer {
    private static final int QUEUE_SIZE = 10;
    private static final Object lock = new Object();
    private static int[] queue = new int[QUEUE_SIZE];
    private static int head = 0;
    private static int tail = 0;

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    while ((tail + 1) % QUEUE_SIZE == head) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue[tail] = (int) (Math.random() * 100);
                    tail = (tail + 1) % QUEUE_SIZE;
                    System.out.println("生产者生产:" + queue[tail]);
                    lock.notifyAll();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    while (head == tail) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    int data = queue[head];
                    head = (head + 1) % QUEUE_SIZE;
                    System.out.println("消费者消费:" + data);
                    lock.notifyAll();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

案例解释:

  • 使用一个数组 queue 模拟队列,head 指向队头,tail 指向队尾。
  • 生产者线程不断生成数据,放入队列中,如果队列满了就等待。
  • 消费者线程不断从队列中取出数据,如果队列为空就等待。
  • 使用 lock 对象作为锁,保证对队列的访问是线程安全的。
  • 使用 wait()notifyAll() 方法实现线程间的通信。

2. Condition 接口

  • Condition 接口提供了更灵活的线程间通信机制。
  • 通过 Lock 对象的 newCondition() 方法获取 Condition 对象。
  • await() 方法类似 wait() 方法,signal() 方法类似 notify() 方法,signalAll() 方法类似 notifyAll() 方法。

案例:使用 Condition 实现生产者消费者模型

public class ProducerConsumerCondition {
    private static final int QUEUE_SIZE = 10;
    private static final Lock lock = new ReentrantLock();
    private static final Condition notFull = lock.newCondition();
    private static final Condition notEmpty = lock.newCondition();
    private static int[] queue = new int[QUEUE_SIZE];
    private static int head = 0;
    private static int tail = 0;

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while ((tail + 1) % QUEUE_SIZE == head) {
                        try {
                            notFull.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue[tail] = (int) (Math.random() * 100);
                    tail = (tail + 1) % QUEUE_SIZE;
                    System.out.println("生产者生产:" + queue[tail]);
                    notEmpty.signal();
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while (head == tail) {
                        try {
                            notEmpty.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    int data = queue[head];
                    head = (head + 1) % QUEUE_SIZE;
                    System.out.println("消费者消费:" + data);
                    notFull.signal();
                } finally {
                    lock.unlock();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

案例解释:

  • 使用 ReentrantLock 对象作为锁,并通过该对象创建了两个 Condition 对象:notFullnotEmpty
  • notFull 用于生产者线程等待队列不满,notEmpty 用于消费者线程等待队列不空。
  • 生产者线程在生产数据后,调用 notEmpty.signal() 方法唤醒等待 notEmpty 条件的消费者线程。
  • 消费者线程在消费数据后,调用 notFull.signal() 方法唤醒等待 notFull 条件的生产者线程。

Condition 接口可以实现更精确的线程控制,例如可以实现多个生产者线程和多个消费者线程之间的同步。

六、线程池

  • **概念:**预先创建多个线程,放入线程池中,需要时从线程池获取线程执行任务,任务结束后将线程归还线程池。

  • 优点:

    • 降低线程创建和销毁的开销
    • 提高线程利用率
    • 方便管理线程
  • 常用线程池:

    • **newFixedThreadPool:**固定大小线程池
    • **newCachedThreadPool:**缓存线程池,可动态调整线程数量
    • **newScheduledThreadPool:**定时任务线程池
    • **newSingleThreadExecutor:**单线程线程池

案例:使用线程池执行任务

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小线程池,线程池大小为 5
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        // 提交 10 个任务到线程池
        for (int i = 0; i < 10; i++) {
            final int taskIndex = i;
            threadPool.execute(() -> {
                System.out.println("Task " + taskIndex + " is running in thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        // 关闭线程池
        threadPool.shutdown();
    }
}

案例解释:

  • 使用 Executors.newFixedThreadPool(5) 创建了一个固定大小为 5 的线程池。
  • 使用 threadPool.execute() 方法提交了 10 个任务到线程池。
  • 线程池会从线程池中获取空闲线程执行任务,如果所有线程都在忙,则任务会被放入队列中等待。
  • 调用 threadPool.shutdown() 方法关闭线程池,不再接受新的任务,已提交的任务会继续执行完毕。

七、volatile 关键字

  • **作用:**保证变量的可见性和有序性。
  • **可见性:**当一个线程修改了 volatile 变量的值,其他线程能立即看到修改后的值。
  • **有序性:**禁止指令重排序,保证代码执行顺序符合预期。

案例:使用 volatile 关键字保证变量可见性

public class VolatileExample {
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            while (!flag) {
                // do something
            }
            System.out.println("Thread 1 exits.");
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("Thread 2 sets flag to true.");
        });

        thread1.start();
        thread2.start();
    }
}

案例解释:

  • flag 变量使用 volatile 关键字修饰,保证了 thread2 修改 flag 变量的值后,thread1 能立即看到修改后的值。
  • 如果没有使用 volatile 关键字,thread1 可能看不到 thread2flag 变量的修改,导致线程无法退出。

八、ThreadLocal 类

  • **作用:**为每个线程提供一个独立的变量副本,避免线程安全问题。
  • **使用场景:**存储线程私有的数据,例如数据库连接、用户 session 等。

案例:使用 ThreadLocal 存储用户 session 信息

public class ThreadLocalExample {
    private static final ThreadLocal<String> session = new ThreadLocal<>();

    public static void main(String[] args) {
        // 模拟多个用户请求
        for (int i = 0; i < 5; i++) {
            final int userId = i;
            new Thread(() -> {
                // 设置用户 session 信息
                session.set("user" + userId);
                // 获取用户 session 信息
                System.out.println("Thread " + Thread.currentThread().getId() + " session: " + session.get());
            }).start();
        }
    }
}

案例解释:

  • 使用 ThreadLocal<String> 创建了一个线程局部变量 session,用于存储用户 session 信息。
  • 每个线程都可以通过 session.set() 方法设置自己的 session 信息,并通过 session.get() 方法获取自己的 session 信息。
  • 不同线程之间不会互相影响,保证了线程安全。

九、学习建议

  • 掌握线程的基本概念、创建、启动、生命周期等基础知识。
  • 深入理解线程同步机制,掌握 synchronized 关键字和 Lock 接口的使用。
  • 学习线程间通信机制,了解 wait()notify()notifyAll() 方法和 Condition 接口的使用。
  • 掌握线程池的使用,了解不同类型线程池的特点和应用场景。
  • 阅读相关书籍和博客,进行实战练习,加深对 Java 多线程的理解。

十、参考资料

  • Oracle 官方文档:https://docs.oracle.com/javase/tutorial/essential/concurrency/
  • 23
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值