MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java多线程编程基础与进阶

2024-02-214.4k 阅读

Java多线程编程基础

线程的基本概念

在操作系统中,线程是比进程更小的执行单元。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。与进程相比,线程的创建和销毁开销更小,线程间的通信和切换也更为高效。

在Java中,线程是一种轻量级的执行单元。Java通过java.lang.Thread类来支持多线程编程。每个线程都有自己的调用栈,用于存储方法调用和局部变量。

创建线程的方式

  1. 继承Thread类
    • 首先创建一个类继承Thread类,然后重写run方法,在run方法中编写线程要执行的代码。
    • 示例代码如下:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("This is a thread created by extending Thread class.");
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
  • 在上述代码中,MyThread类继承了Thread类并重写了run方法。在main方法中,创建了MyThread的实例并调用start方法启动线程。start方法会启动一个新的线程并调用run方法。
  1. 实现Runnable接口
    • 创建一个类实现Runnable接口,实现run方法。然后将该类的实例作为参数传递给Thread类的构造函数来创建线程。
    • 示例代码如下:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("This is a thread created by implementing Runnable interface.");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
  • 这种方式更符合面向对象的设计原则,因为Java不支持多重继承,而实现Runnable接口可以让类在继承其他类的同时还能作为线程执行体。
  1. 使用Callable和Future接口
    • Callable接口类似于Runnable,但Callablecall方法可以返回一个值并且可以抛出异常。Future接口用于获取Callable任务的执行结果。
    • 示例代码如下:
import java.util.concurrent.*;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "This is the result from Callable.";
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(myCallable);
        System.out.println(future.get());
        executorService.shutdown();
    }
}
  • 在上述代码中,MyCallable实现了Callable接口,submit方法提交任务并返回一个Future对象,通过future.get()方法获取任务的执行结果。

线程的生命周期

  1. 新建(New):当使用new关键字创建一个线程对象时,线程处于新建状态。此时线程还没有开始执行,只是一个空的线程对象。
  2. 就绪(Runnable):调用线程的start方法后,线程进入就绪状态。处于就绪状态的线程等待CPU调度,一旦获得CPU资源,就会进入运行状态。
  3. 运行(Running):线程获得CPU资源后开始执行run方法中的代码,此时线程处于运行状态。
  4. 阻塞(Blocked):线程在运行过程中可能会因为某些原因进入阻塞状态,例如等待I/O操作完成、等待获取锁等。处于阻塞状态的线程不会获得CPU资源,直到阻塞条件解除,线程重新进入就绪状态。
  5. 死亡(Dead):当线程的run方法执行完毕或者抛出未捕获的异常时,线程进入死亡状态。一旦线程进入死亡状态,就不能再重新启动。

线程的基本操作

  1. 线程睡眠(sleep)
    • Thread.sleep(long millis)方法可以使当前正在执行的线程暂停指定的毫秒数。
    • 示例代码如下:
public class SleepDemo {
    public static void main(String[] args) {
        try {
            System.out.println("Thread is going to sleep.");
            Thread.sleep(2000);
            System.out.println("Thread woke up.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 在上述代码中,Thread.sleep(2000)使主线程暂停2秒,InterruptedException用于处理在睡眠期间线程被中断的情况。
  1. 线程加入(join)
    • join方法可以使一个线程等待另一个线程执行完毕。例如,在主线程中调用子线程的join方法,主线程会阻塞,直到子线程执行完毕。
    • 示例代码如下:
public class JoinDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Child thread: " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        try {
            thread.join();
            System.out.println("Child thread has finished, main thread continues.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 在上述代码中,主线程调用thread.join()后,会等待thread线程执行完毕后再继续执行。
  1. 线程优先级(priority)
    • 线程可以设置优先级,优先级高的线程有更大的机会获得CPU资源。Java中线程优先级的范围是1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默认优先级是5(Thread.NORM_PRIORITY)。
    • 示例代码如下:
public class PriorityDemo {
    public static void main(String[] args) {
        Thread highPriorityThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("High priority thread: " + i);
            }
        });
        highPriorityThread.setPriority(Thread.MAX_PRIORITY);

        Thread lowPriorityThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Low priority thread: " + i);
            }
        });
        lowPriorityThread.setPriority(Thread.MIN_PRIORITY);

        highPriorityThread.start();
        lowPriorityThread.start();
    }
}
  • 在上述代码中,highPriorityThread设置为最高优先级,lowPriorityThread设置为最低优先级。虽然优先级高的线程有更大机会获得CPU,但并不能保证高优先级线程一定会先执行完。

Java多线程编程进阶

线程安全问题

  1. 共享资源与竞争条件
    • 在多线程环境下,当多个线程同时访问和修改共享资源时,就可能会出现竞争条件。例如,多个线程同时对一个共享的计数器进行加1操作,可能会导致最终结果不正确。
    • 示例代码如下:
public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class RaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("Expected count: 2000, Actual count: " + counter.getCount());
    }
}
  • 在上述代码中,Counter类的increment方法对共享变量count进行操作,由于多个线程同时访问和修改count,可能会导致最终结果小于2000。
  1. 原子性、可见性与有序性
    • 原子性:一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。例如,对一个int类型变量的赋值操作在大多数情况下是原子的,但像count++这样的复合操作不是原子的。
    • 可见性:当一个线程修改了共享变量的值,其他线程能够立即看到修改后的值。在Java中,由于线程的工作内存和主内存的存在,可能会导致可见性问题。例如,一个线程修改了共享变量的值,但其他线程可能仍然从自己的工作内存中读取旧的值。
    • 有序性:程序执行的顺序按照代码的先后顺序执行。但在Java中,为了提高性能,编译器和处理器可能会对指令进行重排序,这可能会导致在多线程环境下出现问题。

同步机制

  1. synchronized关键字
    • 同步方法:在方法声明中使用synchronized关键字,该方法就成为同步方法。当一个线程调用同步方法时,会自动获取该方法所属对象的锁,其他线程必须等待锁释放后才能调用该方法。
    • 示例代码如下:
public class SynchronizedMethodCounter {
    private int count = 0;

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

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

public class SynchronizedMethodDemo {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedMethodCounter counter = new SynchronizedMethodCounter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("Expected count: 2000, Actual count: " + counter.getCount());
    }
}
  • 在上述代码中,incrementgetCount方法都是同步方法,通过synchronized关键字保证了线程安全。
  • 同步块:使用synchronized关键字修饰代码块,语法为synchronized(对象) { // 同步代码块 }。线程进入同步块前需要获取对象的锁。
  • 示例代码如下:
public class SynchronizedBlockCounter {
    private int count = 0;
    private final Object lock = new Object();

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

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

public class SynchronizedBlockDemo {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlockCounter counter = new SynchronizedBlockCounter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("Expected count: 2000, Actual count: " + counter.getCount());
    }
}
  • 在上述代码中,通过synchronized(lock)块保证了对count的操作是线程安全的。
  1. ReentrantLock
    • ReentrantLock是Java 5.0引入的一种可重入的互斥锁,它提供了比synchronized更灵活的锁控制。例如,ReentrantLock可以实现公平锁(按照线程等待的顺序分配锁),而synchronized是非公平锁(随机分配锁)。
    • 示例代码如下:
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockCounter {
    private int count = 0;
    private final 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();
        }
    }
}

public class ReentrantLockDemo {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockCounter counter = new ReentrantLockCounter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("Expected count: 2000, Actual count: " + counter.getCount());
    }
}
  • 在上述代码中,ReentrantLock通过lock方法获取锁,unlock方法释放锁,try - finally块保证了锁一定会被释放。

线程间通信

  1. wait()、notify()和notifyAll()
    • 原理:这三个方法是定义在Object类中的,只能在同步方法或同步块中调用。wait方法会使当前线程释放锁并进入等待状态,直到其他线程调用notifynotifyAll方法唤醒它。notify方法随机唤醒一个等待在该对象上的线程,notifyAll方法唤醒所有等待在该对象上的线程。
    • 示例代码如下:
public class ProducerConsumer {
    private static final Object lock = new Object();
    private static int value = 0;

    static class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                synchronized (lock) {
                    while (value != 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    value = i;
                    System.out.println("Produced: " + value);
                    lock.notify();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                synchronized (lock) {
                    while (value == 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Consumed: " + value);
                    value = 0;
                    lock.notify();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread producerThread = new Thread(new Producer());
        Thread consumerThread = new Thread(new Consumer());
        producerThread.start();
        consumerThread.start();
    }
}
  • 在上述代码中,ProducerConsumer线程通过waitnotify方法实现了线程间的协作,避免了生产者生产过多或消费者消费不足的问题。
  1. Condition
    • Condition是Java 5.0引入的接口,配合ReentrantLock使用,提供了比waitnotify更灵活的线程间通信方式。Condition可以创建多个等待队列,每个等待队列可以有不同的唤醒策略。
    • 示例代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionProducerConsumer {
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();
    private static int value = 0;

    static class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                lock.lock();
                try {
                    while (value != 0) {
                        condition.await();
                    }
                    value = i;
                    System.out.println("Produced: " + value);
                    condition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                lock.lock();
                try {
                    while (value == 0) {
                        condition.await();
                    }
                    System.out.println("Consumed: " + value);
                    value = 0;
                    condition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread producerThread = new Thread(new Producer());
        Thread consumerThread = new Thread(new Consumer());
        producerThread.start();
        consumerThread.start();
    }
}
  • 在上述代码中,ProducerConsumer线程通过Conditionawaitsignal方法实现了线程间通信,与waitnotify类似,但基于ReentrantLock,更加灵活。

线程池

  1. 线程池的概念与优势

    • 线程池是一种管理和复用线程的机制。它预先创建一定数量的线程并保存在池中,当有任务需要执行时,从线程池中取出一个线程来执行任务,任务执行完毕后,线程不会被销毁,而是返回线程池供下次使用。
    • 线程池的优势包括:
      • 减少线程创建和销毁的开销:创建和销毁线程是比较昂贵的操作,线程池复用线程可以避免频繁的线程创建和销毁。
      • 控制并发线程数量:可以设置线程池的最大线程数,防止过多线程同时运行导致系统资源耗尽。
      • 提高系统的稳定性和响应性:通过合理配置线程池参数,能够提高系统在高并发情况下的稳定性和响应速度。
  2. 创建线程池的方式

    • 使用ThreadPoolExecutor类ThreadPoolExecutor是Java线程池的核心实现类,通过它可以灵活地配置线程池的各种参数。
    • 示例代码如下:
import java.util.concurrent.*;

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // corePoolSize
                4, // maximumPoolSize
                10, // keepAliveTime
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5), // workQueue
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 10; i++) {
            int taskNumber = i;
            executor.submit(() -> {
                System.out.println("Task " + taskNumber + " is running on thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executor.shutdown();
    }
}
  • 在上述代码中,corePoolSize表示核心线程数,maximumPoolSize表示最大线程数,keepAliveTime表示非核心线程的存活时间,workQueue是任务队列,ThreadFactory用于创建线程,RejectedExecutionHandler用于处理任务拒绝的情况。
  • 使用Executors工具类Executors类提供了一些静态方法来创建不同类型的线程池,如newFixedThreadPool(固定大小线程池)、newCachedThreadPool(缓存线程池)、newSingleThreadExecutor(单线程线程池)等。
  • 示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " is running on thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}
  • 在上述代码中,Executors.newFixedThreadPool(3)创建了一个固定大小为3的线程池。虽然Executors类创建线程池很方便,但在使用时需要注意其默认参数可能不符合实际需求,可能会导致资源耗尽等问题,所以在生产环境中建议直接使用ThreadPoolExecutor来创建线程池。

并发容器

  1. ConcurrentHashMap
    • ConcurrentHashMap是线程安全的哈希表,在多线程环境下提供了高效的并发访问。与Hashtable不同,ConcurrentHashMap采用了分段锁技术,允许多个线程同时访问不同的段,从而提高了并发性能。
    • 示例代码如下:
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("one", 1);
        map.put("two", 2);
        System.out.println(map.get("one"));
    }
}
  • 在上述代码中,ConcurrentHashMap可以在多线程环境下安全地进行插入和读取操作。
  1. CopyOnWriteArrayList
    • CopyOnWriteArrayList是线程安全的ArrayList,它的特点是在进行写操作(如添加、删除元素)时,会创建一个原数组的副本,在副本上进行操作,操作完成后再将原数组指向副本。读操作则直接读取原数组,因此读操作是线程安全的,并且不需要加锁,提高了读性能。
    • 示例代码如下:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}
  • 在上述代码中,CopyOnWriteArrayList适用于读多写少的场景,保证了线程安全的同时,在读操作上有较好的性能。
  1. ConcurrentLinkedQueue
    • ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用了链表结构。在多线程环境下,ConcurrentLinkedQueue提供了高效的入队和出队操作。
    • 示例代码如下:
import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentLinkedQueueDemo {
    public static void main(String[] args) {
        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
        queue.add(1);
        queue.add(2);
        System.out.println(queue.poll());
    }
}
  • 在上述代码中,ConcurrentLinkedQueue可以在多线程环境下安全地进行入队(add方法)和出队(poll方法)操作。

通过深入理解和掌握Java多线程编程的基础知识和进阶内容,开发者能够编写出高效、稳定且线程安全的后端应用程序,充分利用多核处理器的性能,提升系统的并发处理能力。