1.进程
进程是指在系统中正在运行的一个应用程序
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间
2.线程
线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行
一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程)
所有进程,至少会有一个线程即主线程,即main方法开始执行,就会有一个看不见的主线程存在。
3.多线程
一个进程中可以开启多条线程,每条线程可以并行执行不同的任务
多线程技术可以提高程序的执行效率
3.1多线程原理
同一时间,CPU只能处理一条线程,只有一条线程在工作;多线程并发执行,其实是CPU
快速地在多条线程之间调度,如果CPU调度线程的时间足够快,就造成了多线程并发执
行的假象
并发和并行:并发是一个时间段多线程同时运行,一个时间点多线程不同时;并行是一个时间点多线程同时工作。
3.2多线程优缺点
优点: | 缺点: |
能适当提高程序的执行效率 | 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能 |
线程越多,CPU在调度线程上的开销就越大 | |
能适当提高资源利用率(CPU、内存利用率) | 程序设计更加复杂:比如线程之间的通信、多线程的数据共享 |
4.时间片
即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。即该进程允许运行的时间,使各个程序从表面上看是同时进行的
5.创建线程
5.1继承 Thread 类
public class MyThread extends Thread {
public void run() {
// 线程要执行的任务
}
}
// 创建线程对象并启动线程
MyThread myThread = new MyThread();
myThread.start();
5.2实现 Runnable 接口
public class MyRunnable implements Runnable {
public void run() {
// 线程要执行的任务
}
}
// 创建线程对象并启动线程
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
5.3实现Callable接口
5.4匿名类继承Thread
Thread t1= new Thread(()->{ //用lambda表达式简化run方法
System.out.println("luobei");
});
t1.start();
6.线程的生命周期
-
新建状态(New):当线程对象创建后,它便处于新建状态。此时它并没有开始执行,也没有被分配到系统资源。
-
就绪状态(Runnable):当调用线程的
start()
方法后,它便处于就绪状态。此时它已经被系统分配到了资源(获得了除CPU以外的所有资源),但它的执行还没有开始。此外,就绪状态的线程也有可能因为等待某些资源而被挂起。 -
运行状态(Running):当线程开始执行
run()
方法时,它便处于运行状态。此时它真正开始执行自己的任务,并占用了系统资源。 -
阻塞状态(Blocked):当线程在执行过程中遇到了某些阻塞操作(如等待 I/O 或获取锁等)时,它会进入阻塞状态。此时它将释放占用的系统资源,直到满足某些条件后重新进入就绪状态。
-
终止状态(Terminated):当线程的
run()
方法执行完毕或抛出了未捕获的异常时,线程将进入终止状态。此时它已经释放了所有占用的系统资源,无法再次进入任何其他状态。
7.线程常用方法
start() | 使该线程开始执行,Java 虚拟机调用该线程的 run 方法 |
sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响 |
currentThread() | 返回对当前正在执行的线程对象的引用 |
getName() | 返回该线程的名称 |
join(long millis) | 等待该线程终止的时间最长为 millis 毫秒 |
setDaemon(boolean on) | 将该线程标记为守护线程或用户线程 |
setPriority(int newPriority) | 更改线程的优先级, 取值范围1-10,线程优先其次变大,被cpu分配资源的概率越高 |
getPriority() | 返回线程的优先级 |
interrupt() | 中断线程 |
yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
isAlive() | 测试线程是否处于活动状态 |
线程sleep的时候,有可能被停止,这时就会抛出 InterruptedException(中断异常)
在 Java 中,通过 setDaemon() 方法将一个线程设置为守护线程。当程序中只剩下守护线程时,Java 虚拟机会结束所有的守护线程并退出程序。守护线程通常用于在后台执行某种任务,比如垃圾回收线程就是一个守护线程。
8.同步代码块
同步代码块是Java中的一种同步机制,用于控制多个线程的并发访问。在同步代码块中,通过synchronized关键字来锁定一个对象,以保证同一时间只有一个线程可以执行该代码块。
通常情况下,同步代码块是在某个共享对象上进行锁定,这个共享对象可以是任意类型的Java对象。在同步代码块中,只有获取了该对象的锁,才能执行该代码块中的语句。而其他线程在获取该对象的锁之前,将被阻塞,等待当前线程释放该对象的锁。
synchronized(object) {
// 同步代码块
}
当同步代码块执行完毕后,该线程会释放object的锁,其他线程就可以获取该锁,进入同步代码块执行相应的任务。
9.同步方法
可以使用synchronized
关键字来修饰方法,使得这个方法成为同步方法。当一个线程访问同步方法时,它会自动获取当前对象的锁,其他线程将无法进入该方法,直到该线程执行完毕并释放锁。只有获得了锁的线程才能访问这个方法,这样就能保证在同一时刻只有一个线程可以访问这个方法,从而实现了对共享资源的互斥访问和同步控制。
public synchronized void add(int num) {
count += num;
}
10.死锁
死锁是指两个或多个线程在执行过程中,因争夺资源而互相等待的现象,导致所有线程都被挂起无法继续执行。
死锁会导致系统资源浪费、程序崩溃等问题,因此应该尽量避免死锁的发生。可以通过以下措施来预防死锁:
1.避免使用多个锁。
2.按照固定的顺序获得锁。
3.设置超时时间,在等待一段时间后放弃等待并释放已经获得的锁。
4.使用死锁检测机制,及时发现和解决死锁问题。
5.尽量降低锁的持有时间,减少锁的竞争。
11.lock锁
11.1Lock锁的特点:
- Lock锁是一个接口,定义了加锁(lock)和释放锁(unlock)的方法。
- Lock锁的实现类一般都是通过一个具体的类ReentrantLock来实现。
- Lock锁的加锁和释放锁都需要手动实现,因此需要在finally语句块中释放锁,避免发生死锁的情况。
- Lock锁支持公平锁和非公平锁。
11.2Lock锁的优点:
- 可以精确控制锁的范围和释放时间,不会像synchronized关键字一样只能同步方法和代码块。
- Lock锁支持公平锁和非公平锁,可以更加灵活地满足不同的需求。
- Lock锁是基于CAS操作实现的,可以提高并发性能。
11.3Lock锁的缺点:
- Lock锁需要手动加锁和释放锁,代码量比synchronized关键字更多。
- Lock锁的性能优于synchronized关键字,但也不是绝对的,具体情况需要根据实际测试来判断。
11.4在使用Lock锁时需要注意以下几点:
- 加锁和释放锁必须成对出现,不能出现死锁的情况。
- 如果使用了Lock锁,就必须手动释放锁,否则会导致其他线程无法获取到锁而一直等待,从而引发死锁的情况。
- 如果使用了公平锁,那么线程获取锁的顺序是按照先来先得的原则,如果使用了非公平锁,那么线程获取锁的顺序是随机的,不能保证公平性。
11.5Lock锁与synchronized之间的区别
-
lock锁是一个接口,synchronized是一个关键字
-
锁的获取方式不同:synchronized是隐式获取锁,而Lock是显式获取锁。
-
锁的释放方式不同:synchronized是在执行完同步代码块或同步方法之后自动释放锁,而Lock必须在finally块中手动释放锁,否则可能会出现死锁等问题。
-
锁的粒度不同:synchronized锁的粒度比较大,只能对整个方法或代码块进行加锁,而Lock可以对任意代码块进行加锁,可以细化锁的范围,提高代码的并发性能。
-
锁的功能不同:synchronized锁是重量级锁,它内置了锁的排队、唤醒等机制,因此可以保证线程的同步性和可见性;而Lock锁是轻量级锁,它只提供了基本的线程同步机制,不能保证线程的可见性。
12.计时器
计时器通常使用 Java 自带的 Timer 和 TimerTask 类来实现。Timer 类允许您在未来某个时刻执行一个任务,而 TimerTask 类则定义要执行的任务。
import java.util.Timer;
import java.util.TimerTask;
public class MyTimerTask extends TimerTask {
public void run() {
System.out.println("Timer task executed.");
}
}
public class TimerExample {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task = new MyTimerTask();
timer.schedule(task, 5000); // 在5秒后执行任务
}
}
13.实现线程之间的通信(交替执行)
wait() | 是当前线程进入等待状态 |
notify() | 随即唤醒正在等待状态的线程 |
notifyAll() | 唤醒所有正在等待状态的线程 |
14.线程池
线程池是管理线程的一种机制,可以减少线程的创建和销毁所带来的性能开销。使用线程池,可以重复利用已经创建的线程,并且可以限制同时执行的线程数量,从而避免资源过度占用。线程池包括了线程的创建、调度和销毁等功能,使得开发人员无需关心线程的细节,只需要向线程池提交任务即可。
线程池中线程可以执行,并且线程池中线程在执行完成任务之后不会马上销毁,而是回到线程池中等待下一次被调用,直到线程池关闭才会被销毁。
14.1构建线程池:
static ExecutorService | newSingleThreadExecutor() |
static ExecutorService | newFixedThreadPool(int nThreads) 创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。 |
public static void main(String[] args) {
// 创建一个固定大小的线程池,大小为2
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交任务到线程池中
executor.submit(new Task("Task 1"));
executor.submit(new Task("Task 2"));
executor.submit(new Task("Task 3"));
executor.submit(new Task("Task 4"));
// 关闭线程池
executor.shutdown();
}
static class Task implements Runnable {
private String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Executing task: " + name);
}
}
14.2线程池常用方法:
<T> Future<T> | submit(Callable<T> task) |
Future<?> | submit(Runnable task) |
void | shutdown() |
14.3构建线程池实际使用的方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize : | 线程池中的核心线程数,一旦线程池被创建,即使这些线程闲置也会保留在池中。 | |
maximumPoolSize : | 线程池中允许的最大线程数。 | |
keepAliveTime : | 当线程池中的线程数量大于核心线程数时,这是额外的空闲线程在终止前等待新任务的最长时间。 | |
unit : | 上述等待时间的时间单位。 | |
workQueue :执行任务的队列,通常有以下四种可选的队列类型: | SynchronousQueue : | 直接将任务提交给线程池,如果线程池中没有可用线程,则尝试创建一个新的线程执行任务。如果线程池的最大线程数也已达到,那么提交任务的线程将被阻塞,直到有一个线程可用为止。 |
LinkedBlockingQueue : | 一个无界队列,将任务存储在其中等待执行,因此最大线程数将被忽略。 | |
ArrayBlockingQueue : | 一个有界队列,可以限制任务的数量和频率,但可能导致线程饥饿。 | |
PriorityBlockingQueue : | 一个无界队列,根据任务的优先级排序执行。 | |
threadFactory : | 创建新线程的工厂,如果没有指定,则使用默认的线程工厂。 | |
handler :当线程池和队列都已满,无法接受新任务时,用于处理这些新任务的策略。Java中提供了四种内置的拒绝策略: | AbortPolicy : | 默认策略,如果无法执行任务,则直接抛出RejectedExecutionException 异常。 |
CallerRunsPolicy : | 如果无法执行任务,则将该任务退回给调用者执行。这样可以减慢任务提交速度,降低系统负载,但同时可能导致调用者线程也被阻塞。 | |
DiscardOldestPolicy : | 如果无法执行任务,则丢弃队列中最早的任务,并尝试重新提交当前任务。 | |
DiscardPolicy : | 如果无法执行任务,则默默地丢弃它,不做任何处理。 |
14.4ThreadPoolExecutor与其他线程池实现相比有以下优势:
-
灵活的参数设置:ThreadPoolExecutor可以灵活地设置线程池的核心线程数、最大线程数、空闲线程超时时间、任务队列大小等参数,根据具体应用场景进行优化。
-
可扩展性强:ThreadPoolExecutor可以通过继承ThreadPoolExecutor类,重写线程池的方法,实现更复杂的线程池功能,例如记录日志、统计执行时间、监控线程池运行状态等。
-
丰富的任务队列:ThreadPoolExecutor提供了多种任务队列,例如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue和SynchronousQueue等,可以根据具体的应用场景选择合适的队列类型。
-
提供线程池拒绝策略:当任务队列已满且线程池中线程数已达到最大值时,ThreadPoolExecutor提供了多种线程池拒绝策略,例如直接抛出异常、丢弃任务、丢弃队列中最早的任务、将任务分配给调用线程等。
-
统一管理线程池:ThreadPoolExecutor可以统一管理线程池中的所有线程,例如关闭线程池、设置线程池中所有线程的优先级等。
综上所述,ThreadPoolExecutor相比其他线程池实现,具有更高的可定制性和扩展性,更适合复杂的多线程应用场景。
14.5ThreadPoolExecutor线程池的生命周期
-
RUNNING:线程池处于运行状态,可以接受新任务并处理已添加的任务。
-
SHUTDOWN:线程池停止接受新任务,但可以处理已添加到队列中的任务。如果调用了ThreadPoolExecutor.shutdown()方法,则线程池将进入该状态。
-
STOP:线程池停止接受新任务,并尝试停止正在执行的任务。如果调用了ThreadPoolExecutor.shutdownNow()方法,则线程池将进入该状态。
-
TERMINATED:线程池已经完全终止,不再接受新的任务,已经提交的任务都已经处理完毕并且线程池中的所有线程也已经关闭。
package com.daily;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class WaitTest {
private static int num = 1;
private static Lock lock = new ReentrantLock(); // 创建一个可重入锁对象
private static Condition c1 = lock.newCondition();
// 创建一个Condition对象,表示t1的条件
private static Condition c2 = lock.newCondition();
// 创建一个Condition对象,表示t2的条件
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
lock.lock(); // 加锁
try {
if (num == 1) {
c1.await(); // 等待c1条件满足,使得程序进入等待
}
System.out.print("射");
System.out.print("人");
System.out.print("先");
System.out.print("射");
System.out.println("马");
num = 1;
c1.signal(); // 唤醒t2线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
});
Thread t2 = new Thread(() -> {
lock.lock(); // 加锁
try {
if (num == 0) {
c2.await(); // 等待c2条件满足,使得程序进入等待
}
System.out.print("擒");
System.out.print("贼");
System.out.print("先");
System.out.print("擒");
System.out.println("王");
num = 0;
c2.signal(); // 唤醒t1线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
});
t1.start();
t2.start();
}
}