java线程基础理解

进程、线程、多线程等基本概念:

Java 是一种广泛使用的面向对象编程语言,其并发编程支持使其在同时处理多个任务时表现出色。以下是 Java 进程、线程、多线程等基本概念的简要介绍:

  • 进程:进程是指在计算机上正在执行的一个程序,它拥有自己的内存空间、文件句柄等系统资源。在 Java 中,通过调用 Runtime 类或 ProcessBuilder 类的方法,可以启动新的进程并运行外部命令或程序。

  • 线程:线程是进程内的执行单元,负责程序的运行和控制。在 Java 中,线程可以通过继承 Thread 类或实现 Runnable 接口来创建。每个线程拥有独立的内存栈和程序计数器,但共享进程的内存空间和其他系统资源。

  • 多线程:多线程是指在同一个进程内同时创建并运行多个线程,从而实现并发处理多个任务。Java 中支持多线程编程,通过使用线程池、线程同步、线程通信等技术,可以实现高效、安全、可靠的并发程序。

需要注意的是,Java 中的多线程编程也涉及到一系列的问题,例如并发访问共享数据时的数据竞争和死锁等,采用适当的代码设计和编程技巧可以有效地避免这些问题。同时,Java 中也提供了多种工具和类库来支持多线程编程,例如Executor框架、CountDownLatch类、concurrent包等,可以进一步简化多线程编程的复杂性。

Java 线程的状态包括以下五种:

  • NEW(新建):当线程对象被创建但还没有调用 start() 方法时,线程处于 NEW 状态。

  • RUNNABLE(可运行):当线程处于可运行的状态时,它可以被 JVM 的线程调度器(scheduler)选中执行。

  • BLOCKED(阻塞):线程获取锁失败,或调用了 Thread.sleep()、Object.wait() 等方法被挂起时,线程处于阻塞状态。

  • WAITING(等待):线程调用了 Object.wait()、Thread.join() 或 LockSupport.park() 方法进入等待状态,等待条件的满足。

  • TERMINATED(终止):线程完成了它的工作或者异常终止时,线程处于终止状态。

Java 线程状态之间的变换如下:

  • NEW -> RUNNABLE:调用 start() 方法

  • RUNNABLE -> BLOCKED:请求锁失败

  • RUNNABLE -> WAITING:调用 Object.wait()、Thread.join() 或 LockSupport.park() 方法

  • BLOCKED -> RUNNABLE:持锁线程释放锁

  • WAITING -> RUNNABLE:等待的条件满足

  • RUNNABLE -> TERMINATED:run() 方法执行完成,线程正常终止

  • BLOCKED -> TERMINATED:线程异常终止

  • WAITING -> TERMINATED:线程异常终止

需要注意的是,Java 线程状态的变化是由 JVM 的线程调度器和代码中的锁等机制决定的,处理线程状态的转换需要遵循一定的规则和原则,以确保线程的正确性和效率。

Java 线程的创建和启动主要涉及如下两个步骤:

创建线程对象:可以通过继承 Thread 类或者实现 Runnable 接口的方式来创建线程对象。

  • 继承 Thread 类创建线程对象:
public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
    }
}
// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
  • 实现 Runnable 接口创建线程对象:
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}
// 创建并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();

启动线程:调用线程对象的 start() 方法来启动线程。调用 start() 方法之后,JVM 会安排线程在某个时间开始执行线程的 run() 方法。

需要注意的是,直接调用 run() 方法并不会启动新的线程,只是在当前线程中执行了 run() 方法的代码而已。

上述是最常用的创建和启动线程的方法,还可以使用“匿名内部类”的方式来创建和启动线程:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // 线程执行的代码
    }
});
thread.start();

此外,自 Java 8 开始,还可以使用 Lambda 表达式来创建线程:

Thread thread = new Thread(() -> {
    // 线程执行的代码
});
thread.start();

总之,Java 线程的创建和启动比较简单,关键是要使用合适的方式,采取正确的方法来实现多线程编程。

同步和协作

Java 线程的同步和协作是多线程编程中的关键问题,涉及到如何协调多个线程之间的执行,防止数据竞争、死锁等问题。

线程同步的方法

Java 中提供了多种线程同步的方法,如 synchronized、Lock、Atomic 类等。

其中最常用的是 synchronized 关键字,使用 synchronized 可以保证同一时间只有一个线程可以执行该代码块或方法,进而保证多个线程之间对共享数据的访问是安全的。

例如,下面的代码使用 synchronized 对共享变量 count 进行同步:

public class Counter {
    private int count = 0;

    public synchronized void add(int value) {
        count += value;
    }

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

这个简单的类中有一个共享变量 count,add 方法向 count 添加一个值,get 方法返回 count 的值。由于两个方法都使用了 synchronized 关键字,多个线程之间对共享变量进行访问时,可以保证每次只有一个线程执行这些方法,从而避免了数据竞争。

线程协作的方法

Java 中也提供了多种线程协作的方法,如 wait()、notify()、notifyAll() 等。

这些方法通常用于多个线程之间的通信和协作,以达到更加灵活的多线程编程。

例如,下面的代码展示了两个线程之间简单的协作:

public class Message {
    private String str;

    public synchronized void set(String str) {
        while (this.str != null) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.str = str;
        notify();
    }

    public synchronized String get() {
        while (this.str == null) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String temp = this.str;
        this.str = null;
        notify();
        return temp;
    }
}

public class Sender implements Runnable {
    private Message msg;

    public Sender(Message msg) {
        this.msg = msg;
    }

    public void run() {
        String[] strArray = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};
        for (int i = 0; i < strArray.length; i++) {
            msg.set(strArray[i]);
        }
        msg.set("exit");
    }
}

public class Receiver implements Runnable {
    private Message msg;

    public Receiver(Message msg) {
        this.msg = msg;
    }

    public void run() {
        while (true) {
            String str = msg.get();
            if (str.equals("exit")) {
                break;
            }
            System.out.println("Received: " + str);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Message msg = new Message();
        new Thread(new Sender(msg)).start();
        new Thread(new Receiver(msg)).start();
    }
}

这个简单的例子中,有一个类 Message,其中有一个共享变量 str,Sender 类和 Receiver 类都可以向该变量传递信息。

Sender 类和 Receiver 类都实现了 Runnable 接口,分别向 Message 对象中设置和获取信息,当消息发送完成时,Sender 中会向变量中添加 “exit” 标记,Receiver 会在获取到 “exit” 标记时停止接收消息。

在 Message 类中,set() 方法和 get() 方法都使用了 synchronized 关键字,以保证在线程对共享变量进行访问时,同一时间只有一个线程可以执行这些方法。并且,在线程访问共享变量时,如果该变量为空(set 方法)或者非空(get 方法),线程会被阻塞,并等待相关条件(等待其他线程调用 notify() 方法)。当共享变量被修改时,会调用 notify() 方法来唤醒等待的线程。

Java 线程的同步和协作是多线程编程中的核心问题,对于同一时间只有一个线程可以执行的代码块或方法,可以使用 synchronized 关键字;对于多个线程之间的通信和协作,可以使用 wait()、notify()、notifyAll() 等方法。在使用这些方法时,需要仔细考虑多线程之间可能存在的数据竞争、死锁等问题,以尽可能保证线程编程的安全性和可靠性。

线程池

Java 线程池是一种常用的多线程处理方式,可以用于管理多个线程、减小线程创建和销毁的开销、优化多线程的执行效率等。

线程池的实现

Java 线程池的实现主要基于 ThreadPoolExecutor 类,该类提供了多种参数来配置线程池的大小、线程存活时间、队列类型等。常用的构造方法如下:

ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue
)

其中:

参数 子参数 描述
corePoolSize 核心线程数:表示线程池中一直保持活动的线程数量。即使这些线程是空闲的,它们也不会被回收。
maximumPoolSize 最大线程数:表示线程池中允许存在的最大线程数量。当任务队列中的任务数量达到一定阈值,且当前线程数已达到 corePoolSize 的时候,线程池会创建新的线程,直到达到 maximumPoolSize。
keepAliveTime 线程活动保持时间:表示当线程池中的线程数超过 corePoolSize 时,空闲线程的最大存活时间。超过这个时间,空闲线程将被销毁,以保持线程池的大小不超过 corePoolSize。
unit 线程活动保持时间的时间单位:表示 keepAliveTime 参数的时间单位。通常使用 TimeUnit 枚举类中的值,如 TimeUnit.SECONDS。
workQueue 任务队列:用来存放等待执行的任务。
有界队列 具有固定的大小限制,如ArrayBlockingQueue或LinkedBlockingQueue指定大小后即为有界队列,当队列满时,线程池无法再添加新的任务,除非已有任务被消费。
无界队列 没有预设的大小限制,可以无限存放任务,如不指定大小的LinkedBlockingQueue,即使队列满了也可以继续插入任务,但可能导致资源耗尽的问题。
无缓冲队列 通常指SynchronousQueue,它不存储元素,每个插入操作必须等待另一个线程调用移除操作,反之亦然,它在生产者和消费者之间直接传递任务,因此不能容纳多余的任务。
threadFactory 线程工厂:用来创建新的线程。
handler 饱和策略:表示当线程池和任务队列都已满时,如何处理新提交的任务。常用的策略有:ThreadPoolExecutor.AbortPolicy(抛出 RejectedExecutionException 异常,默认策略)、ThreadPoolExecutor.CallerRunsPolicy(使用调用线程来执行任务)、ThreadPoolExecutor.DiscardOldestPolicy(丢弃队列中最旧的任务)、ThreadPoolExecutor.DiscardPolicy(丢弃新来的任务)。

除此之外,ThreadPoolExecutor 类还提供了 beforeExecute()、afterExecute()、terminated() 方法,用于在线程池中的线程执行任务前、执行后、线程池关闭后等不同的时刻执行一些操作。

workQueue选择注意:

  1. 行为特性
  • 当线程池达到最大线程数且工作队列已满时,三种队列的行为差异很大:
    • 有界队列:如果新任务到达而队列已满,会触发拒绝策略,比如抛出异常、丢弃任务或者将任务交给调用者等。
    • 无界队列:尽管能持续接受任务,但如果任务生成速度远超处理速度,可能导致内存溢出等问题。
    • 无缓冲队列(同步队列):由于不存在内部存储空间,每来一个任务就必须有相应的工作线程立即开始处理,否则任务提交将会阻塞直到有线程可用。
  1. 性能与资源使用
  • 有界队列可以控制资源消耗,防止系统资源耗尽,但需要合理设置队列大小以避免任务积压或频繁拒绝。
  • 无界队列降低了系统设计复杂性,但可能带来潜在的资源问题。
  • 无缓冲队列提供了最大程度上的任务执行即时性,适合于快速响应场景,但它要求线程池有足够的线程能够及时处理到来的任务。
  1. ArrayBlockingQueue和LinkedBlockingQueue区别
  • 数据结构实现
    • ArrayBlockingQueue:基于固定大小的数组来存储元素,因此在创建时需要指定容量大小,并且一旦创建后容量不可变。由于使用数组,插入和删除操作可能涉及到数组元素的移动。
    • LinkedBlockingQueue:基于单向链表(LinkedList)实现,可以在创建时选择是否指定容量,默认值是Integer.MAX_VALUE,即无界队列。链表结构支持动态扩容,但插入和删除操作通常只需要更新指针。
  • 容量限制
    • ArrayBlockingQueue:有界的,必须在初始化时设置容量大小,并且不允许超出该容量,当队列满时插入会阻塞,当队列空时移除也会阻塞。
    • LinkedBlockingQueue:既可以作为有界队列(通过构造函数指定容量),也可以作为无界队列(不指定容量或传入Integer.MAX_VALUE)。
  • 性能与空间效率
    • ArrayBlockingQueue:对于定长数组,内存占用相对固定,连续存储,访问速度快,但扩容不可行。
    • LinkedBlockingQueue:内存分配随着元素数量增加而增加,理论上在频繁插入删除情况下可能有更高的内存开销,但其插入和删除操作通常是O(1)的时间复杂度。
  • 锁机制
    • ArrayBlockingQueue:通常使用一个可重入锁(ReentrantLock)来控制对整个数组的访问,有一个not-full条件队列对应插入操作,一个not-empty条件队列对应移除操作。
    • LinkedBlockingQueue:内部同样使用两个Condition对象进行同步,但由于数据结构不同,它的加锁粒度可以更细,例如只锁定链表的某个部分来进行插入或删除操作。
  • 是否允许null元素
    • ArrayBlockingQueue:不允许插入null元素。
    • LinkedBlockingQueue:允许插入null元素,但并不推荐这样做,因为null值可能会导致其他线程无法正确理解队列中的有效任务。
  • 总结来说,ArrayBlockingQueue更适合于对资源有限制或者对内存占用要求严格的场景,而LinkedBlockingQueue则提供了更大的灵活性,尤其是在处理大小不确定的任务流时。

根据cpu计算最大线程数

最大线程数量 = CPU核心数 * CPU利用率 * (1 + 等待系数)

其中,CPU核心数是服务器的物理核心数或逻辑核心数,CPU利用率是我们希望将服务器CPU的多少比例用于Java线程的计算,等待系数是用于预留一定比例的线程用于等待IO等操作。

首先,您需要确定服务器的CPU核心数。可以通过不同的方法来获取,如使用操作系统命令或Java代码。

然后,您需要决定将多少比例的CPU用于Java线程的计算。这个比例根据您的实际需求和服务器的负载来决定。例如,如果您愿意将服务器的80% CPU用于Java线程的计算,那么CPU利用率将为0.8。

最后,您可以选择一个合适的等待系数。等待系数是用于预留一定比例的线程用于等待IO等操作,以充分利用CPU资源。

举个例子,假设您的服务器有8个CPU核心,您希望将80%的CPU用于Java线程的计算,等待系数为0.5。那么,最大线程数量可以计算如下:

最大线程数量 = 8 * 0.8 * (1 + 0.5) = 9.610

这意味着您可以设置Java的最大线程数量为10。请注意,这只是一个估计值,实际应用中的最大线程数量可能需要根据实际情况进行调整。另外,还需要考虑服务器的其他资源限制,如内存等。

根据内存计算最大线程数

最大线程数量 = (总内存 - 最大堆内存 - 操作系统消耗的内存) / 每个线程使用的内存

其中,总内存是服务器的物理内存或可用内存,最大堆内存是Java应用程序允许使用的最大堆内存,操作系统消耗的内存是操作系统及其他应用程序消耗的内存,每个线程使用的内存是每个Java线程所需的内存。

首先,您需要确定服务器的总内存。可以通过操作系统命令或Java代码获取。

然后,您需要确定Java应用程序允许使用的最大堆内存(-Xmx参数)。这个值通常是根据应用程序的需求和服务器的内存容量来设定的。

接下来,您需要估计操作系统消耗的内存。这个值可以根据操作系统的要求和其他正在运行的应用程序来确定。可以参考操作系统的文档或使用监控工具来获取。

最后,您需要确定每个Java线程所需的内存。这个值可能因应用程序的特性和使用的库而有所差异。通常来说,每个线程的内存消耗包括Java对象、线程栈和其他相关资源。

举个例子,假设您的服务器有16GB的总内存,Java应用程序允许使用的最大堆内存为4GB,操作系统消耗的内存为2GB,每个线程使用的内存为1MB。那么,最大线程数量可以计算如下:

最大线程数量 = (16GB - 4GB - 2GB) / 1MB = 10240

这意味着您可以设置Java的最大线程数量为10,240。请注意,这只是一个估计值,实际应用中的最大线程数量可能需要根据实际情况进行调整。另外,还需要考虑服务器的其他资源限制,如CPU利用率等。

线程池的使用场景

Java 线程池适用于同时处理多个任务且任务比较短暂的情况,例如 Web 服务器处理请求时,每个请求可以分配给一个线程去执行。此外,还可以用于批处理任务,例如批量导入数据、批量处理文件等。

好处

使用 Java 线程池的好处主要有以下几点:

  • 降低线程创建和销毁的开销:线程池中的线程可以反复利用,避免频繁创建和销毁线程所带来的开销。
  • 提高线程的利用率:线程池中的线程可以并行地执行任务,提高了 CPU 和内存等资源的利用率。
  • 控制线程的数量:通过设置线程池的大小,可以有效地控制并发度,避免系统过度占用资源。
  • 提高代码的可读性:使用线程池可以将任务的提交和具体的线程操作进行分离,对于代码的可读性和可维护性都有所提高。
  • 提高系统的稳定性:线程池可以有效地避免因为线程过多而导致的系统崩溃等问题,从而提高了系统的稳定性。

不足

Java 线程池也有一些不足之处,比如:

  • 线程池大小的设置可能会带来性能天花板的问题:当线程池中的线程数量过少时,可能会限制并发度,从而影响系统的效率;当线程池中的线程数量过多时,可能会带来过多的上下文切换和内存开销等问题,进而影响系统的响应速度和并发度。

  • 处理比较长时间的阻塞任务可能会影响系统的稳定性:如果线程池中的线程过多地处理一些比较长时间的阻塞任务,可能会导致线程池中的线程数逐渐增加,并导致内存不足等问题。

  • 对于一些需要按照顺序执行的任务,线程池可能并不适合,因为线程池的线程调用顺序并不是固定的。

Java 线程池是一种常用的多线程处理方式,可以提高线程的利用率和系统的稳定性,降低线程创建和销毁的开销,并提升代码的可读性和可维护性。但是需要注意线程池大小的设置以及处理比较长时间的阻塞任务等问题。

Java线程安全问题和线程问题排查

Java多线程在实际应用中经常出现的问题包括死锁、同步问题和共享变量问题等,为了保证线程安全,需要了解这些问题的产生原因以及排查方法。

死锁

死锁是指两个或以上的线程在互相持有对方需要的资源而导致的无法继续执行的现象。一般来说,死锁需要满足四个条件:

  • 互斥条件:每个资源要么已经分配给了一个线程,要么就是可用的。
  • 请求与保持条件:一个线程因请求资源而被阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:已获得的资源在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干个线程之间形成一种头尾相接的循环等待资源的关系。

要想避免死锁,主要有以下几种方法:

  • 避免过多的锁竞争。
  • 避免持有锁的时间过长。
  • 避免循环等待。
  • 使用专门的死锁检测工具或代码。

同步问题

同步问题是指多个线程同时访问共享变量或共享资源所引起的数据不一致性问题,主要包括线程安全问题和可见性问题。

线程安全问题是指多个线程同时访问共享变量或资源时引起数据不一致的问题,常见的解决方法是使用 synchronized 关键字或 Lock 接口等来对共享资源进行加锁。

可见性问题则是指一个线程修改了共享变量的值,但其它线程无法立即看到这种变更,从而导致数据不一致的问题。通常可以使用 volatile 关键字或 Atomic 类型的变量来解决可见性问题。

共享变量问题

共享变量问题是指多个线程同时访问一个变量时可能引起的数据不一致性问题。在多线程环境下,当多个线程操作共享变量时,某个线程正在操作时,其他线程也可能会同时进行操作,这时候就有可能引起数据冲突。为了避免这种问题,可以使用 synchronized 关键字或 Lock 接口等来为共享变量进行加锁。

线程问题的排查

当 Java 多线程应用发生问题时,如何定位线程问题并及时解决就非常关键了。一般来说,排查线程问题需要遵循如下步骤:

  • 确定是否是线程问题,通过日志、异常信息等来定位问题所在。
  • 确认问题是哪一个线程引起的,可以通过 Java VisualVM、JConsole 等工具来监控线程状态和性能指标。
  • 查找可能引起线程问题的代码段,定位代码中的同步问题或竞争问题等。
  • 分析线程问题的原因并进行排查,可以通过加锁、使用 volatile 关键字、使用线程安全的类等手段来解决问题。
  • 进行测试和优化,测试解决方案的正确性和性能,优化代码和代码结构,提高系统的稳定性和性能。
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!