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

Java多线程的基本概念与应用

2023-12-032.8k 阅读

Java多线程的基本概念

在计算机编程领域,多线程是一项至关重要的技术,尤其在Java语言中。线程,从本质上来说,可以看作是程序执行的一条独立路径。在一个Java程序中,当它启动时,就会有一个主线程开始执行main方法中的代码。而多线程允许我们在同一个程序中创建多个这样的执行路径,每个路径都可以独立地执行不同的代码块,从而实现更高效的资源利用和更灵活的程序设计。

线程与进程的区别

在深入探讨Java多线程之前,有必要先区分一下线程与进程的概念。进程是程序在操作系统中的一次执行过程,它是一个资源分配的基本单位。每个进程都有自己独立的内存空间、系统资源(如文件描述符、信号处理等)。当我们启动一个Java程序时,操作系统就会为这个程序创建一个进程。

而线程则是进程中的一个执行单元,是程序执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。例如,在一个Java的图形化界面(GUI)应用程序中,主线程负责绘制界面元素,而可以启动其他线程来处理网络请求或者进行复杂的计算,这些线程共享进程的堆内存等资源。线程之间的切换开销比进程之间的切换开销要小得多,这也是多线程编程能够提高程序性能的一个重要原因。

Java线程的生命周期

Java线程具有自己的生命周期,它经历一系列的状态变化。这些状态包括:

  1. 新建(New):当使用new关键字创建一个线程对象时,线程就处于新建状态。例如:
Thread thread = new Thread(() -> {
    // 线程执行的代码
    System.out.println("线程正在执行");
});

此时,线程对象已经创建,但尚未开始执行。 2. 就绪(Runnable):当调用线程的start()方法后,线程进入就绪状态。处于就绪状态的线程已经具备了执行的条件,但还没有被分配到CPU资源,等待CPU调度执行。

thread.start();
  1. 运行(Running):当CPU调度到就绪状态的线程时,该线程进入运行状态,开始执行run()方法中的代码。
  2. 阻塞(Blocked):线程在执行过程中,可能会因为某些原因进入阻塞状态,暂时无法继续执行。比如,线程调用了Thread.sleep(long millis)方法,使线程进入睡眠状态,在指定的时间内不会执行;或者线程尝试获取一个锁,但该锁被其他线程持有,此时线程会进入阻塞状态等待锁的释放。例如:
try {
    Thread.sleep(1000); // 线程睡眠1秒
} catch (InterruptedException e) {
    e.printStackTrace();
}
  1. 等待(Waiting):线程可以通过调用Object类的wait()方法进入等待状态。处于等待状态的线程需要其他线程调用notify()notifyAll()方法来唤醒。通常用于线程间的协作场景,比如生产者 - 消费者模式。
  2. 计时等待(Timed Waiting):除了Thread.sleep(long millis)方法会使线程进入计时等待状态外,Object类的wait(long timeout)方法也会使线程进入计时等待状态。在指定的时间内,如果没有被唤醒,线程会自动醒来继续执行。
  3. 终止(Terminated):当线程的run()方法执行完毕,或者因为异常导致run()方法提前退出,线程就进入终止状态,此时线程的生命周期结束。

Java多线程的创建方式

在Java中,创建多线程主要有两种方式:继承Thread类和实现Runnable接口。

继承Thread类

继承Thread类是创建线程最直接的方式。通过继承Thread类,并重写其run()方法,在run()方法中定义线程要执行的任务。示例代码如下:

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + "正在执行:" + i);
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.start();
        thread2.start();
    }
}

在上述代码中,MyThread类继承自Thread类,并重写了run()方法。在main方法中,创建了两个MyThread对象,并分别调用start()方法启动线程。每个线程在执行时,会输出自己的线程名和循环变量的值。

实现Runnable接口

实现Runnable接口也是创建线程的常用方式。这种方式更符合面向对象编程中“组合优于继承”的原则。实现Runnable接口的类需要实现run()方法,然后将该类的实例作为参数传递给Thread类的构造函数来创建线程。示例代码如下:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "正在执行:" + i);
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

在这段代码中,MyRunnable类实现了Runnable接口,并实现了run()方法。在main方法中,创建了一个MyRunnable实例,并将其传递给Thread类的构造函数来创建两个线程。

两种方式的比较

  1. 继承Thread
    • 优点:代码简单直接,便于理解和实现。
    • 缺点:由于Java不支持多重继承,如果一个类已经继承了其他类,就无法再继承Thread类。同时,继承Thread类可能导致代码的耦合度较高,不利于代码的复用和维护。
  2. 实现Runnable接口
    • 优点:一个类可以在实现Runnable接口的同时继承其他类,符合“组合优于继承”的原则,提高了代码的复用性和可维护性。同时,多个线程可以共享同一个Runnable实例,便于实现资源共享。
    • 缺点:相比继承Thread类,代码结构相对复杂一些,需要创建Thread对象并将Runnable实例传递给它。

Java多线程的同步与互斥

在多线程编程中,当多个线程同时访问共享资源时,可能会出现数据不一致的问题。例如,多个线程同时对一个共享变量进行读写操作,可能会导致数据的错误更新。为了解决这类问题,Java提供了同步和互斥机制。

同步方法

在Java中,可以通过在方法声明中使用synchronized关键字来将方法声明为同步方法。当一个线程调用同步方法时,它会自动获取该方法所属对象的锁。其他线程如果想要调用同一个对象的同步方法,必须等待该锁的释放。示例代码如下:

class Counter {
    private int count = 0;

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

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

public class SynchronizedMethodExample {
    public static void main(String[] args) {
        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();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终的计数:" + counter.getCount());
    }
}

在上述代码中,Counter类的increment()getCount()方法都被声明为同步方法。当thread1thread2线程调用increment()方法时,会依次获取counter对象的锁,从而保证count变量的正确更新。

同步块

除了同步方法,还可以使用同步块来实现同步。同步块的语法为synchronized (object) { /* 同步代码块 */ },其中object是要获取锁的对象。同步块更加灵活,可以只对需要同步的代码部分进行同步,而不是整个方法。示例代码如下:

class SharedResource {
    private int data = 0;

    public void updateData() {
        synchronized (this) {
            data++;
        }
    }

    public int getData() {
        synchronized (this) {
            return data;
        }
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.updateData();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.updateData();
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终的数据:" + resource.getData());
    }
}

在这段代码中,SharedResource类的updateData()getData()方法中使用了同步块,通过synchronized (this)获取this对象的锁,从而保证对data变量的操作是线程安全的。

锁的原理

无论是同步方法还是同步块,其底层都是基于Java的内置锁机制。每个Java对象都有一个与之关联的锁(也称为监视器锁)。当一个线程进入同步方法或同步块时,它会尝试获取对象的锁。如果锁可用,线程获取锁并进入同步代码块执行;如果锁不可用,线程会被阻塞,直到锁被释放。当线程执行完同步代码块或同步方法时,会自动释放锁,允许其他线程获取。

死锁问题

在多线程编程中,死锁是一个需要特别注意的问题。死锁是指两个或多个线程相互等待对方释放锁,从而导致所有线程都无法继续执行的情况。例如,线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1,就会发生死锁。示例代码如下:

class Resource1 {
    public synchronized void method1(Resource2 resource2) {
        System.out.println("线程 " + Thread.currentThread().getName() + " 进入Resource1的method1");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        resource2.method2();
    }
}

class Resource2 {
    public synchronized void method2() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 进入Resource2的method2");
    }
}

public class DeadlockExample {
    public static void main(String[] args) {
        Resource1 resource1 = new Resource1();
        Resource2 resource2 = new Resource2();
        Thread thread1 = new Thread(() -> {
            resource1.method1(resource2);
        });
        Thread thread2 = new Thread(() -> {
            resource2.method2();
        });
        thread1.start();
        thread2.start();
    }
}

在上述代码中,thread1调用resource1.method1(resource2)时,获取了resource1的锁,并试图获取resource2的锁;而thread2调用resource2.method2()时,获取了resource2的锁,并试图获取resource1的锁,从而导致死锁。

为了避免死锁,需要遵循一些原则,如尽量减少锁的持有时间、按照相同的顺序获取锁、使用定时锁(如Lock接口的tryLock(long timeout, TimeUnit unit)方法)等。

Java多线程的通信

在多线程编程中,线程之间往往需要进行协作和通信,以实现更复杂的功能。Java提供了多种线程通信的方式。

使用Object类的wait()、notify()和notifyAll()方法

Object类提供了wait()notify()notifyAll()方法,用于线程间的通信。wait()方法会使当前线程进入等待状态,并释放它持有的锁,直到其他线程调用notify()notifyAll()方法唤醒它。notify()方法会随机唤醒一个等待在该对象上的线程,而notifyAll()方法会唤醒所有等待在该对象上的线程。示例代码如下:

class Message {
    private String content;
    private boolean ready = false;

    public synchronized void write(String content) {
        while (ready) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.content = content;
        ready = true;
        notifyAll();
    }

    public synchronized String read() {
        while (!ready) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        ready = false;
        notifyAll();
        return content;
    }
}

public class ThreadCommunicationExample {
    public static void main(String[] args) {
        Message message = new Message();
        Thread writerThread = new Thread(() -> {
            message.write("Hello, World!");
        });
        Thread readerThread = new Thread(() -> {
            System.out.println("读取到的内容:" + message.read());
        });
        writerThread.start();
        readerThread.start();
    }
}

在上述代码中,Message类通过wait()notifyAll()方法实现了线程间的通信。writerThread线程写入消息后,通过notifyAll()唤醒readerThread线程读取消息。

使用Condition接口

从Java 5开始,引入了java.util.concurrent.locks.Condition接口,它提供了比Object类的wait()notify()notifyAll()方法更灵活的线程通信方式。Condition对象需要与Lock对象配合使用。示例代码如下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class SharedData {
    private int data;
    private boolean available = false;
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void setData(int data) {
        lock.lock();
        try {
            while (available) {
                condition.await();
            }
            this.data = data;
            available = true;
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public int getData() {
        lock.lock();
        try {
            while (!available) {
                condition.await();
            }
            available = false;
            condition.signalAll();
            return data;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return -1;
    }
}

public class ConditionExample {
    public static void main(String[] args) {
        SharedData sharedData = new SharedData();
        Thread producerThread = new Thread(() -> {
            sharedData.setData(42);
        });
        Thread consumerThread = new Thread(() -> {
            System.out.println("获取到的数据:" + sharedData.getData());
        });
        producerThread.start();
        consumerThread.start();
    }
}

在这段代码中,SharedData类使用LockCondition实现了线程间的通信。producerThread线程设置数据后,通过condition.signalAll()唤醒consumerThread线程读取数据。

Java多线程的高级应用

除了上述基本的多线程概念和技术,Java还提供了一些高级的多线程应用场景和工具。

线程池

线程池是一种管理和复用线程的机制。在多线程应用中,如果频繁地创建和销毁线程,会带来较大的开销。线程池可以预先创建一定数量的线程,并将这些线程保存在池中。当有任务需要执行时,从线程池中获取一个线程来执行任务,任务完成后,线程不会被销毁,而是返回线程池等待下一个任务。Java提供了java.util.concurrent.ExecutorService接口和java.util.concurrent.Executors工具类来创建和管理线程池。示例代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    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("线程 " + Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

在上述代码中,通过Executors.newFixedThreadPool(3)创建了一个固定大小为3的线程池。然后提交了5个任务,线程池中的3个线程会依次执行这些任务。

Future和Callable

在Java中,Callable接口和Future接口用于异步任务的执行和获取任务的执行结果。Callable接口类似于Runnable接口,但Callable接口的call()方法可以返回一个值,并且可以抛出异常。Future接口用于获取Callable任务的执行结果。示例代码如下:

import java.util.concurrent.*;

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;
    }
}

public class FutureExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<Integer> future = executorService.submit(new MyCallable());
        try {
            System.out.println("任务执行结果:" + future.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }
}

在上述代码中,MyCallable类实现了Callable接口,call()方法计算1到100的和并返回。通过executorService.submit()提交任务,并通过future.get()获取任务的执行结果。

并发集合

Java提供了一系列线程安全的并发集合类,如ConcurrentHashMapCopyOnWriteArrayList等。这些集合类在多线程环境下能够提供高效的并发访问,并且保证数据的一致性。例如,ConcurrentHashMap采用了分段锁的机制,允许多个线程同时对不同的段进行读写操作,提高了并发性能。示例代码如下:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentCollectionExample {
    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创建了一个线程安全的哈希表,并进行了插入和获取操作。

通过深入理解和掌握Java多线程的基本概念、创建方式、同步机制、通信方式以及高级应用,开发人员可以编写出高效、可靠的多线程程序,充分利用多核处理器的性能,提升应用程序的响应速度和吞吐量。在实际开发中,需要根据具体的业务需求和场景,合理地选择和运用多线程技术,避免出现性能问题和线程安全问题。同时,不断学习和研究新的多线程技术和工具,以适应不断发展的软件行业需求。