Java多线程基础知识
什么是多线程
在Java中,多线程(Multithreading)是并发编程的一种形式,它允许在一个程序中同时运行多个线程。每个线程都是程序的一个独立执行流,拥有自己的堆栈和程序计数器,但共享程序的其他部分(如内存和静态变量)。
以下是多线程的几个关键概念:
- 线程:线程是程序执行流的最小单元。Java虚拟机(JVM)允许一个进程并发地执行多个线程。
- 并发:并发意味着多个线程在同一时间段内执行。然而,由于CPU资源有限,实际上同一时刻只有一个线程在执行,但线程的切换速度非常快,使得多个线程看起来像是在同时执行。
- 并行:并行是并发的特例,当多个线程在同一时刻(在不同的CPU或处理器核心上)同时执行时,称为并行。
- 线程状态:Java中的线程具有多种状态,包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。
- 线程同步:由于多个线程共享程序的资源,因此需要对这些资源进行同步以防止数据不一致或冲突。Java提供了多种同步机制,如synchronized关键字、Lock接口和相关的类(如ReentrantLock)、volatile关键字和原子类(如AtomicInteger)等。
- 线程安全:线程安全意味着多个线程在并发执行时能够正确地访问共享资源,而不会导致数据不一致或其他问题。编写线程安全的代码是并发编程中的一项重要挑战。
- 线程池:线程池是一种用于管理和复用线程的机制。通过线程池,可以减少线程的创建和销毁开销,提高程序的性能。Java提供了几种线程池实现,如FixedThreadPool、CachedThreadPool和ScheduledThreadPool等。
在Java中,可以通过实现Runnable接口或继承Thread类来创建线程。然后,可以使用Thread类的start()方法启动线程。此外,Java还提供了丰富的并发工具类库,如java.util.concurrent包中的类,以支持更复杂的并发编程需求。
第一种实现方式
package com.mohuanan.test01;public class Test01 {public static void main(String[] args) {/*** 多线程的第一种实现方式:* 继承Thread* 重写run方法* 创建子类的对象**/MyThread mt1 = new MyThread();MyThread mt2 = new MyThread();mt1.setName("线程一");mt2.setName("线程二");//开启线程mt1.start();mt2.start();}
}
第二种实现方式
package com.mohuanan.test02;public class Test02 {public static void main(String[] args) {/** 多线程的第二种实现方式* 实现Runnable接口* 重写Run方法* 创建实现Runnable接口的类的对象* 创建Thread对象* 开启线程* */MyRun mr = new MyRun();Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);t1.setName("线程1");t2.setName("线程2");//开启线程t1.start();t2.start();}
}
第三种实现方式
package com.mohuanan.test03;import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;public class Test01 {public static void main(String[] args) throws ExecutionException, InterruptedException {/** 第三种实现方式:* 带有返回值* 实现Callable接口* 重写里面的call方法** 创建实现Callable接口的类的对象(表示进程要执行的任务)* 创建FutureTask对象(管理多线程运行的结果)* 创建Thread对象 并开启线程(表示线程)** */MyCallable mc = new MyCallable();FutureTask ft = new FutureTask(mc);Thread t = new Thread(ft);//开启进程t.start();//获取进程的返回值Integer sum = (Integer) ft.get();System.out.println(sum);}
}
在Java多线程里面常见的成员方法
在Java多线程编程中,Thread
类和其他相关类(如 Runnable
、ExecutorService
、Future
等)提供了许多用于管理和控制线程的成员方法。以下是一些在Java多线程编程中常见的成员方法:
- Thread类的方法:
start()
: 启动一个新线程并执行此线程中的run()
方法。run()
: 如果此线程是使用独立的Runnable
运行对象构造的,则调用该Runnable
对象的run
方法;否则,该方法不执行任何操作并返回。currentThread()
: 返回对当前正在执行的线程对象的引用。interrupt()
: 中断此线程。除非此线程当前正在等待、睡眠或以其他方式被阻塞,否则该线程将不会受到中断的影响。isInterrupted()
: 测试此线程是否已被中断。线程的中断状态由interrupt()
方法设置。sleep(long millis)
: 使当前正在执行的线程在指定的毫秒数内处于休眠(暂停执行)状态。join()
: 等待该线程终止。setPriority(int newPriority)
: 更改此线程的优先级。getPriority()
: 返回此线程的优先级。setDaemon(boolean on)
: 将此线程标记为守护线程或用户线程。isDaemon()
: 测试此线程是否为守护线程。getState()
: 返回此线程的状态。yield()
: 暂停当前正在执行的线程对象,并执行其他线程。stop()
: 已废弃。停止此线程的执行。此方法会引发ThreadDeath
异常。resume()
: 已废弃。恢复此线程的执行。suspend()
: 已废弃。暂停此线程的执行。
- Runnable接口的方法:
run()
: 由线程执行的方法。实现此接口的类必须定义此未指定的方法。
- ExecutorService接口的方法(常用于线程池):
submit(Runnable task)
: 提交一个Runnable
任务用于执行,并返回一个表示该任务的未来结果的Future
。submit(Callable<T> task)
: 提交一个Callable
任务用于执行,并返回一个表示该任务的未来结果的Future
。shutdown()
: 启动此执行程序的有序关闭序列,在此序列中,先前提交的任务将被执行,但不会接受新任务。shutdownNow()
: 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。execute(Runnable command)
: 在未来某个时间点为给定命令执行一个任务。awaitTermination(long timeout, TimeUnit unit)
: 如果此执行程序在此给定的超时时间之前终止,则返回true
(以及如果已终止则为true
)。isTerminated()
: 如果所有任务都已完成执行,则返回true
。isShutdown()
: 如果此执行程序已关闭,则返回true
。
- Future接口的方法(常与
ExecutorService
一起使用):
get()
: 如有必要,等待计算完成,然后检索其结果。get(long timeout, TimeUnit unit)
: 如有必要,最多等待给定的时间以获取结果(如果可用)。cancel(boolean mayInterruptIfRunning)
: 尝试取消此任务的执行。isDone()
: 如果任务已完成,则返回true
。isCancelled()
: 如果任务在完成前被取消,则返回true
。
这些只是Java多线程编程中常见的一部分方法。根据具体的需求和场景,可能会使用到更多的方法和类。
同步代码块
在Java中,多线程同步代码块是一种确保在某一时刻只有一个线程能够执行某段代码的机制。这通常用于保护共享资源,避免数据不一致或脏读等问题。
同步代码块是什么?
同步代码块是Java中使用synchronized
关键字来定义的代码块。当一个线程进入同步代码块时,它会获取一个锁(通常是该同步代码块所在对象的锁),然后执行代码块中的代码。在此期间,其他尝试进入该同步代码块的线程将被阻塞,直到第一个线程释放锁。
同步代码块有什么用?
同步代码块的主要用途是确保多线程环境下共享资源的访问是同步的,从而防止竞态条件(race condition)和数据不一致。
如何使用同步代码块?
同步代码块的基本语法如下:
synchronized (object) {// 需要同步的代码
}
其中,object
是用作锁的任意对象。线程在进入同步代码块时会尝试获取该对象的锁,如果锁已经被其他线程持有,则该线程将被阻塞。
注意事项:
- 死锁:如果两个或多个线程相互等待对方释放资源,就会发生死锁。这可能导致应用程序停止响应。避免死锁的一个好方法是始终按照相同的顺序获取锁。
- 性能:同步代码块会引入额外的开销,因为线程在尝试获取锁时可能会被阻塞。因此,应该尽可能减少同步代码块的范围,只包含确实需要同步的代码。
- 避免过度同步:过度同步可能导致性能下降,甚至可能导致死锁。应该仔细分析代码,确定哪些部分需要同步,哪些部分可以安全地并行执行。
- 锁的粒度:锁的粒度越细,并发性能就越好,但编程复杂度也会增加。需要根据具体情况权衡。
- 选择正确的锁对象:同步代码块使用的锁对象应该能够唯一标识需要同步的资源。通常,可以使用共享资源的实例或类对象作为锁。
- 避免在同步代码块中调用可能阻塞的方法:如果在同步代码块中调用了一个可能会阻塞的方法(如I/O操作或等待用户输入),那么持有锁的线程可能会被阻塞很长时间,从而导致其他需要访问该资源的线程也被阻塞。这可能会导致性能下降或甚至死锁。
- 注意异常处理:在同步代码块中抛出异常时,需要确保锁能够被正确释放。否则,可能会导致其他线程永远无法获取锁。一种常见的做法是在finally块中释放锁,但请注意,在Java中,同步代码块在退出时会自动释放锁,所以你通常不需要在finally块中显式释放锁。
死锁
在Java中,死锁是指两个或多个线程在互相请求对方占用的资源时处于等待状态,导致程序无法继续执行的现象。具体来说,每个线程都持有一个资源并且同时等待另一个资源,形成一种僵局,没有任何一个线程能够释放其持有的资源,也无法获得它所需的资源。
以下是关于Java死锁的一些注意事项:
- 死锁的必要条件:死锁的发生通常需要满足以下四个条件,也被称为死锁的必要条件:
- 互斥条件:至少有一个资源在任意时刻只能被一个线程占用。
- 保持和等待条件:一个线程至少持有一个资源,并等待获取一个当前被其他线程持有的资源。
- 不可剥夺条件:资源只能由持有它的线程释放,资源不可以被剥夺。
- 循环等待条件:存在一个等待资源的循环,即每个线程都在等待下一个线程释放资源。
为了避免Java中的死锁,可以采取以下策略:
- 避免嵌套锁:尽量避免一个线程在持有一个锁的同时去请求另一个锁。如果确实需要获取多个锁,应该确保所有线程以相同的顺序请求和释放锁。
- 使用定时锁:使用
tryLock()
方法来尝试获取锁,并设置一个超时时间。如果超时时间内没有获取到锁,那么就放弃获取锁,并做一些其他的处理,比如重试或者返回错误。 - 锁分割:将大的锁分割成几个小的锁,使得不同的线程可以同时访问不同的资源。
- 检测死锁:使用工具或JVM内置功能(如JConsole)来监控和检测系统中的死锁,然后进行相应的处理。
- 资源分配策略:设计合理的资源分配策略,确保线程在请求资源时不会形成循环等待的情况。
总之,死锁是Java多线程编程中需要特别注意的问题。为了避免死锁的发生,需要深入理解死锁的原理和必要条件,并采取相应的策略来预防和处理死锁。
等待唤醒机制
在Java中,等待唤醒机制是线程间通信的一种重要手段,它允许一个线程等待某个条件成立,而另一个线程可以通知等待的线程该条件已经成立。等待唤醒机制主要依赖于wait()
和notify()
/notifyAll()
这几个方法。
使用方法:
- wait() 方法:
- 当线程调用某个对象的
wait()
方法时,它会释放该对象的锁,并进入等待状态,直到其他线程调用该对象的notify()
或notifyAll()
方法。 wait()
方法只能在同步方法或同步块中调用,否则会抛出IllegalMonitorStateException
异常。
- notify() 方法:
- 当线程调用某个对象的
notify()
方法时,它会唤醒在该对象上等待的单个线程(如果有的话)。 notify()
方法也必须在同步方法或同步块中调用,否则会抛出IllegalMonitorStateException
异常。
- notifyAll() 方法:
- 当线程调用某个对象的
notifyAll()
方法时,它会唤醒在该对象上等待的所有线程。 - 同样,
notifyAll()
方法也必须在同步方法或同步块中调用。
注意事项:
- 必须在同步块或同步方法中调用:
wait()
、notify()
和notifyAll()
方法必须在同步块或同步方法中调用,因为它们是用于操作对象锁的。 - 释放锁:当线程调用
wait()
方法时,它会释放当前持有的锁,并进入等待状态。这使得其他线程有机会获取该锁并执行。 - 唤醒机制:
notify()
方法只会唤醒等待队列中的一个线程(如果有的话),而notifyAll()
方法会唤醒等待队列中的所有线程。但是,这并不意味着被唤醒的线程会立即执行,它们需要等待重新获取锁。 - 虚假唤醒:即使没有线程调用
notify()
或notifyAll()
方法,等待的线程也可能被唤醒(这被称为虚假唤醒)。因此,在wait()
方法后,应该使用一个循环来检查条件是否真正满足。 - 异常处理:
wait()
方法可能会抛出InterruptedException
异常,因此在使用时需要处理这个异常。通常的做法是捕获该异常并重新等待,或者根据具体情况进行其他处理。 - 避免死锁:虽然等待唤醒机制本身不会导致死锁,但在多线程编程中仍然需要注意避免死锁的发生。例如,确保线程在获取多个锁时以相同的顺序进行,或者使用其他避免死锁的策略。
例子代码1
package com.mohuanan.test06;public class Desk {//总个数public static int count = 10;//食物状态// 0没有食物// 1有食物public static int foodFlag = 0;//锁对象 唯一public static Object lock = new Object();
}package com.mohuanan.test06;public class Cook extends Thread{@Overridepublic void run() {while(true){synchronized (Desk.lock){//判断共享数据是否到了末尾if(Desk.count==0){//到了末尾break;}else{//没有到末尾//判断桌子上有没有面条if(Desk.foodFlag ==0){//没有面条System.out.println("厨师正在制作面条");Desk.foodFlag = 1;//唤醒吃货Desk.lock.notifyAll();}else{//有面条try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}
package com.mohuanan.test06;public class Foodie extends Thread {//多线程的四部走/** 1. 循环* 2. 同步代码块(锁)* 3. 判断共享数据是否到了末尾(到了末尾)* 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)* */@Overridepublic void run() {while (true) {synchronized (Desk.lock) {if (Desk.count == 0) {//共享数据到了末尾 退出break;} else {//共享数据没有到达末尾//判断桌子上有没有食物if (Desk.foodFlag == 0) {//没有食物 等待try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}else{//有食物Desk.count--;System.out.println("吃货正在吃第"+Desk.count+"碗面条");//修改桌子上食物的状态Desk.foodFlag = 0;//唤醒厨师继续做面条Desk.lock.notifyAll();}}}}}
}package com.mohuanan.test06;public class ThreadDemo {public static void main(String[] args) {//创建线程的对象Cook c = new Cook();Foodie f = new Foodie();//给线程设置名字c.setName("厨师");f.setName("吃货");//开启线程c.start();f.start();}
}
例子代码2
package com.mohuanan.test07;import java.util.concurrent.ArrayBlockingQueue;public class Cook extends Thread{ArrayBlockingQueue<String> abq;public Cook(ArrayBlockingQueue<String> abq) {this.abq = abq;}@Overridepublic void run() {while(true){//不断往队列里面放面条try {//put 和 take方法在Java中源代码里面是有锁的 所以不用再自己写锁abq.put("面条");System.out.println("放了一碗面条");} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
package com.mohuanan.test07;import java.util.concurrent.ArrayBlockingQueue;public class Foodie extends Thread{ArrayBlockingQueue<String> abq;public Foodie(ArrayBlockingQueue<String> abq) {this.abq = abq;}@Overridepublic void run() {while (true){//不断的从队列里面获取食物try {String food = abq.take();System.out.println(food);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
package com.mohuanan.test07;import java.util.concurrent.ArrayBlockingQueue;public class Test {public static void main(String[] args) {//创建堵塞队列//使Cook 和 Foodie使用的同一个队列ArrayBlockingQueue<String> abq = new ArrayBlockingQueue<>(1);//创建线程的对象Cook c = new Cook(abq);Foodie f = new Foodie(abq);//设置线程的名字c.setName("厨师");f.setName("吃货");//开启线程c.start();f.start();}
}
Java多线程里面的六种状态
在Java多线程中,线程共有六种状态,它们分别是:
- NEW(新建):这是线程的初始状态。线程对象已经被创建,但是还没有调用
start()
方法。 - RUNNABLE(可运行):这个状态实际上包括了就绪(ready)和运行中(running)两种子状态。当线程对象创建后,其他线程(比如main线程)调用了该对象的
start()
方法,该线程就进入可运行线程池中,等待被线程调度选中,获取CPU的使用权。此时,线程处于就绪状态。当线程获得CPU时间片后,它就变为运行中状态。 - BLOCKED(阻塞):当线程尝试获取某个对象的内置锁,而该锁已经被其他线程持有时,该线程就会进入阻塞状态。此时,线程会暂停执行,直到它获得锁或者等待条件变为真。
- WAITING(等待):当线程执行了
wait()
、join()
或park()
等方法时,线程就会进入等待状态。进入该状态的线程需要等待其他线程做出一些特定动作(如通知或中断)才能继续执行。 - TIMED_WAITING(定时等待):这个状态与WAITING状态类似,但是线程可以在指定的时间后自行返回,而不需要等待其他线程的动作。这通常是因为线程调用了带有时间参数的
sleep()
、wait()
或join()
等方法。 - TERMINATED(终止):当线程执行完毕,或者因为异常而退出时,线程就进入终止状态。这个状态是线程生命周期的最后一个状态。
以上就是Java多线程中的六种状态。
线程池
线程池是Java并发编程中的一种重要技术,它通过预先创建一定数量的线程,并将这些线程放入一个池中,等待任务到来。当有任务到来时,线程池会从池中取出可用的线程来执行任务。这种方式可以有效降低线程创建和销毁的开销,提高系统的性能和稳定性。
使用线程池的一般步骤如下:
- 创建线程池:Java提供了几种创建线程池的方式,最常用的是使用
ThreadPoolExecutor
类或者Executors
工厂类。例如,可以使用Executors.newFixedThreadPool(int nThreads)
来创建一个固定大小的线程池,其中nThreads
表示线程池中的线程数。 - 提交任务:提交任务到线程池通常有两种方式。一是使用
execute()
方法提交不需要返回结果的任务;二是使用submit()
方法提交需要返回结果的任务。任务需要实现Runnable
接口或Callable
接口,并重写run()
或call()
方法。 - 关闭线程池:在使用完线程池后,需要及时关闭以释放资源。关闭线程池可以使用
shutdown()
方法。
使用线程池时需要注意以下事项:
- 合理设置线程池参数:线程池的参数包括核心线程数、最大线程数、任务队列容量、线程空闲时间等。需要根据实际业务场景和需求来合理设置这些参数,以避免资源浪费或性能瓶颈。
- 避免任务堆积:当线程池中的线程都在忙碌时,新提交的任务会被放入任务队列中等待执行。如果任务队列容量设置过小,可能会导致任务堆积和响应延迟。因此,需要根据实际情况合理设置任务队列容量。
- 处理异常和错误:线程池中的任务执行过程中可能会出现异常和错误。需要在任务实现中妥善处理这些异常和错误,避免影响整个程序的稳定性和可用性。
- 合理控制线程数量:过多的线程会消耗大量的系统资源,并可能导致上下文切换过多,从而降低程序的性能。因此,需要根据实际需求和系统资源情况来合理控制线程数量。
- 避免死锁和饥饿:多线程编程中容易出现死锁和饥饿等问题。在使用线程池时,需要注意线程之间的同步和通信机制,避免这些问题的发生。
- 注意线程安全:线程池中的任务是并发执行的,因此需要注意线程安全问题。在共享数据访问、资源竞争等方面需要采取相应的措施来确保线程安全。
自定义多线池
在Java中,你可以通过实现java.util.concurrent.ExecutorService
接口的某个类(通常是ThreadPoolExecutor
)来自定义线程池。ThreadPoolExecutor
类提供了丰富的配置选项,允许你控制线程池的行为。
以下是一个自定义线程池的示例,使用ThreadPoolExecutor
类:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class CustomThreadPool {// 创建一个自定义的线程池public static ExecutorService createThreadPool() {// 核心线程数int corePoolSize = 5;// 最大线程数int maxPoolSize = 10;// 线程池维护线程所允许的空闲时间long keepAliveTime = 60L;// 线程池所使用的缓冲队列java.util.concurrent.BlockingQueue<Runnable> workQueue = new java.util.concurrent.LinkedBlockingQueue<Runnable>();// 线程工厂,用于创建新线程java.util.concurrent.ThreadFactory threadFactory = new java.util.concurrent.Executors.DefaultThreadFactory();// 拒绝策略,当任务太多来不及处理,如何拒绝任务java.util.concurrent.RejectedExecutionHandler handler = new java.util.concurrent.ThreadPoolExecutor.AbortPolicy();// 使用ThreadPoolExecutor构造函数创建线程池ExecutorService executorService = new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,TimeUnit.SECONDS,workQueue,threadFactory,handler);return executorService;}public static void main(String[] args) {// 获取自定义的线程池ExecutorService executorService = createThreadPool();// 提交任务到线程池for (int i = 0; i < 15; i++) {final int taskId = i;executorService.execute(() -> {System.out.println("Running task with id: " + taskId);// 模拟任务执行时间try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}// 关闭线程池(先关闭新任务的提交,然后等待已有任务执行完毕)executorService.shutdown();try {// 如果线程池中的任务在指定时间内没有执行完毕,则抛出异常if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {executorService.shutdownNow(); // 强制关闭线程池}} catch (InterruptedException e) {executorService.shutdownNow(); // 强制关闭线程池}}
}
在这个示例中,我们创建了一个自定义的线程池,并设置了核心线程数、最大线程数、线程空闲时间、工作队列、线程工厂和拒绝策略。然后,我们在main
方法中提交了一些任务到线程池,并最后关闭了线程池。
注意:shutdown()
方法用于平滑地关闭线程池,它会停止接受新任务,但会等待已提交的任务执行完毕。shutdownNow()
方法则会尝试立即停止所有正在执行的任务,并返回等待执行的任务列表。如果你希望在关闭线程池时等待一段时间让任务完成,但不想无限期地等待,可以使用awaitTermination()
方法。
例子代码
package com.mohuanan.test08;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class Test {public static void main(String[] args) {//自定义线程池对象ThreadPoolExecutor pool = new ThreadPoolExecutor(3,//核心线程的个数6,//最大线程的个数 必须大于核心线程的个数60,//空闲线程的最大空闲时间TimeUnit.MICROSECONDS,//最大空闲时间的单位 使用TimeUnitnew ArrayBlockingQueue<>(30),//堵塞队列 最大值为3new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略);}
}