程序(program)
程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码(还没有运行起来),静态对象。
进程(process)
进程是程序的一次执行过程,也就是说程序运行起来了,加载到了内存中,并占用了cpu的资源。这是一个动态的过程:有自身的产生、存在和消亡的过程,这也是进程的生命周期。
进程是系统资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread)
- 进程可进一步细化为线程,是一个程序内部的执行路径。
- 若一个进程同一时间并行执行多个线程,那么这个进程就是支持多线程的。
- 线程是cpu调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一堆中分配对象,可以访问相同的变量和对象。
自定义线程继承 Thread 并重写run方法
- 创建一个继承于Thread类的子类。
- 重写Thread类的run()方法。
- 创建Thread类的子类的对象。
- 通过此对象调用start()来启动一个线程。
实例化并开启线程
线程常用的方法
start() : 启动当前线程, 调用当前线程的run()方法
getName() : 获取当前线程的名字
setName() : 设置当前线程的名字
isAlive() :判断当前线程是否存活
休眠的方法 Thread.sleep(5000); 括号中填入的是毫秒
打印线程信息 Thread.currentThread()
Java 的 Thread 类会自动调用 toString() 方法,输出当前线程的名称、优先级和线程组。
Thread[name, priority, group]
- name: 线程的名称。
- priority: 线程的优先级(范围从 1 到 10,默认值为 5)。
- group: 线程所属的线程组。
也可以直接打印出线程的名字 Thread.currentThread().getName()
设置优先级 setPriority();
优先级越高,获取CPU资源的几率越大
优先级1-10,默认为5,设置其他值报错误
礼让 yeild();
- 作用:释放当前CPU的执行权,让CPU重新分配
- 防止一条线程长时间占用CPU资源,达到让CPU资源合理分配资源
- sleep(0)也可以达到这种效果
join() 成员、方法加入(插队)方法
让一个线程等待另一个线程完成,在A线程中执行了a.join()a线程执行完毕后b在运行
中断线程
在 Java 中,中断线程是一种通知机制,用于请求线程停止当前的工作并尽快结束。中断并不会强制线程停止,而是给线程发送一个中断信号,线程需要通过适当的检查和响应机制来处理中断。
关闭线程
1、执行stop方法
2、调用interrupt()设置中断状态,这个线程不会中断,
我们需要在线程内部判读,中断状态是都被设置,然后经中断操作
3、优雅退出: 线程应定期检查中断状态,并在必要时清理资源并安全地退出。自定义一个状态属性,在线程外部设置此属性,影响线程内部的运行
stop() 方法曾经是用来强制停止线程的,但它已经被标记为不推荐使用并且过时了。使用 stop() 方法会导致一些严重的问题,例如资源泄漏和数据损坏,因此不应该再使用它。
调用 Thread.interrupt() 方法: 这是中断其他线程的方式。调用线程的 interrupt() 方法会设置线程的中断状态标志位。
Thread.currentThread().isInterrupted() 是 Java 中用于检查当前线程是否已经被中断。这个方法不会清除中断标志,它只是检查中断状态
。
检查中断状态: 在线程执行过程中,需要定期检查 Thread.currentThread().isInterrupted() 或通过捕获 InterruptedException 异常来响应中断。
捕获 InterruptedException 异常来响应中断。
线程生命周期
object.wait() | 是一个用于线程间通信的重要方法。它使当前线程等待,直到其他线程调用同一对象的 notify() 或 notifyAll() 方法。 |
Thread.join() | 在 Java 中是一个用于协调多个线程执行顺序的方法。它使当前线程等待另一个线程的完成。使用 join() 方法,当前线程会阻塞,直到调用 join() 方法的线程执行完毕。 |
Thread.sleep (long millis) | 在 Java 中用于使当前线程暂停执行一段时间。这个方法非常有用,特别是在需要临时暂停线程的执行以等待某些条件变化或进行轮询操作时。 |
LockSupport.parkNanos (long nanos) | 在 Java 中用于阻塞当前线程指定的一段时间,具体以纳秒为单位。它是一个底层的线程同步工具,通常用于实现高级的同步结构。这个方法可以比 Thread.sleep(long millis) 提供更精确的时间控制。 |
LockSupport.parkUntil(long deadline) | 在 Java 中用于阻塞当前线程直到指定的绝对时间,这个时间是以毫秒为单位的。这个方法也是一个底层的线程同步工具,常用于实现高级的同步结构。它相比于 parkNanos 更适合用于需要在特定时间点唤醒的场景。 |
- object.wait、Thread.join、sleep(long)、LockSupport.park、LockSupport.parkNanos、LockSupport.parkUntil
- 等待 -> 就绪:notify() 或 notifyAll()、join 线程执行完成、sleep(long) 时间到、LockSupport.unpark、parkNanos 时间到、parkUntil 时间到
- 新建(New):
- 当一个线程对象被创建时,线程处于新建状态。
- 调用 start() 方法后,线程进入就绪状态。
- 就绪(Ready):
- 线程处于就绪状态,等待获取 CPU 资源以进入运行状态。
- 线程获取 CPU 资源后,进入运行状态。
- 线程可以通过调用 yield() 方法或者失去 CPU 资源返回到就绪状态。
- 运行(Running):
- 线程正在执行。
- 线程完成执行 (run() 方法执行完成) 或发生未处理异常,进入终止状态。
- 线程可以等待同步锁(进入阻塞状态)或者执行完成一段时间(进入等待状态)。
- 阻塞(Blocked):
- 线程等待获取同步锁时进入阻塞状态。
- 线程获得同步锁后,进入就绪状态。
- 等待(Waiting):
- 线程等待特定条件或事件发生。
- 可以通过 notify() 或 notifyAll() 被唤醒,进入就绪状态。
- join 线程执行完成、 sleep(long) 时间到、LockSupport.unpark、parkNanos 时间到、parkUntil 时间到等情况下,进入就绪状态。
- 终止(Terminated):
- 线程执行完成或发生未处理异常后,进入终止状态。
volatile、transient
在 Java 中,volatile 关键字用于修饰变量,确保对该变量的读写操作都是直接从主存中进行的,而不是从而不是线程的工作内存中。这样可以保证所有线程对该变量的访问是可见的,从而解决线程之间的可见性问题。
volatile 关键字的作用
- 可见性:当一个线程修改了 volatile 变量的值,新值会立即被刷新到主存中,其他线程读取这个变量时会得到最新的值。
- 禁止指令重排序:volatile 变量在读写时,会在底层加入内存屏障,防止 JVM 和 CPU 进行指令重排序,从而保证了变量的有序性。
volatile 的使用场景
volatile 关键字通常用于以下场景:
- 标志变量:用于标识线程的状态,例如终止线程的标志。
- 单例模式:用于实现双重检查锁定的单例模式。
- 简单的状态变量:用于在多个线程之间共享简单的状态信息。
transient 关键字在 Java 中用于指示序列化过程中应当忽略某些字段。换句话说,当对象被序列化时,transient 修饰的变量不会被序列化到字节流中。当对象被反序列化时,这些变量的值不会恢复,而是被初始化为其默认值(例如,数字类型为 0,对象类型为 null,布尔类型为 false)
线程安全
多个线程操作一个对象,不会出现结果错乱的情况(缺失数据),顺序错误不是线程错乱
线程不安全
StringBuilder 就是线程不安全
常见的线程安全问题
synchronized 锁对象 需要指定锁对象 是悲观锁
对代码块或方法进行同步,确保同一时间只有一个线程执行同步代码块。
修饰方法 成员方法 this
静态方法 类的对象 Object、getCalss()、Easy.class
要做到线程安全,我们可以使用synchronized
对方法或者代码加锁,达到线程同步的效果
使用synchronized 关键字修饰的方法或者代码块,同一时间内只能允许一个线程执行此代码块
- 数据竞争:多个线程同时读写同一个变量,导致数据不一致。
- 死锁:多个线程相互等待对方释放资源,导致程序无法继续执行。
- 活锁:多个线程因为对共享资源的频繁操作导致相互阻碍,虽然没有死锁,但系统无法取得进展。
- 资源饥饿:某些线程由于资源被长期占用而无法获得执行机会。
线程同步
线程同步的基本原理是使用锁(Locks)来控制对共享资源的访问。锁是一种机制,用于在多个线程之间协调资源的使用,以防止数据的不一致性。
常用的同步机制
1. 同步方法和同步块:使用 synchronized 关键字修饰
2. 重入锁(ReentrantLock)
ReentrantLock 提供了更灵活的锁操作,可以显式地获取和释放锁。
3.读写锁(ReadWriteLock)
ReadWriteLock 提供了更细粒度的锁控制,允许多个线程同时读取,但只有一个线程可以写入。
4. 原子变量
使用 java.util.concurrent.atomic 包中的原子变量类(如 AtomicInteger)来确保原子操作。
5.线程安全的集合类
使用 java.util.concurrent 包中的线程安全集合类,如 ConcurrentHashMap 和 CopyOnWriteArrayList。
锁Lock
锁的分类
根据有无锁对象:悲观锁(有锁对象)和乐观锁(没有锁对象)
synchronized是悲观锁,Java中都是悲观锁
乐观锁实现的方式:CAS和版本号控制
公平分类:公平锁和非公平锁
公平锁就是先来后到 非公平锁
可重入锁:在同步代码块中遇到相同的锁对象的同步代码块,不需要在获取锁对象的权限,直接进入执行
Java中都是可重入锁
根据线程状态不同:偏向锁、轻量级锁(自旋锁)、重量级锁
乐观锁(Optimistic Locking)是一种用于解决并发问题的机制,它假设冲突很少发生,因此不立即对数据加锁,而是在提交数据时检查冲突。乐观锁常用于需要高并发和性能的场景。常见的实现方式包括使用 CAS(Compare and Swap)操作和版本号控制。
1. CAS(Compare and Swap)操作
CAS 是一种无锁操作,它通过比较和交换来实现乐观锁。在 Java 中,CAS 操作主要通过 java.util.concurrent.atomic 包中的原子类实现,例如 AtomicInteger, AtomicBoolean, AtomicReference 等。
CAS 基本原理:
- 读取当前值。
- 比较当前值和期望值是否相等。
- 如果相等,则更新为新值;如果不相等,则重试或失败。
2. 版本号控制
版本号控制是另一种乐观锁的实现方式,它通过维护一个版本号来检测并发修改。在每次更新数据时检查版本号是否与读取时一致,如果一致则更新版本号和数据,否则重试或失败。
版本号控制基本步骤:
- 读取数据和版本号。
- 更新数据时检查版本号是否与读取时一致。
- 如果一致,更新数据和版本号;如果不一致,重试或失败。
乐观锁适用于读操作多、写操作少的场景,因为它在大多数情况下不会加锁,从而提高了系统的并发性能。通过 CAS 和版本号控制两种方式可以有效实现乐观锁:
CAS(Compare and Swap):适用于单变量的并发更新。
版本号控制:适用于需要维护一致性的复杂数据结构更新。
线程锁对象
锁对象 lock
加锁 lock()和 tryLock()
try Lock()尝试加锁 返回Boolean值 加锁成功返回true,失败返回false
解锁 unlock()
创建锁对象 默认false创建非公平锁,true创建公平锁
加锁
尝试加锁
解锁
锁容器 ReentrantReadWriteLock
ReentrantReadWriteLock:一个可重入的读写锁。包含一个读锁和一个写锁。
读锁:允许多个读操作并发进行。
写锁:只允许一个线程进行写操作,并且写操作是独占的。
读锁
写锁
wait和sleep
wait和sleep的区别
- wait 是OBject中定义的方法,必须在同步块或同步方法中使用。可以让有锁对象调用,让直行到该代码的线程进入到等待状态
- sleep 是Thread类中定义的方法,可以在任何地方使用。可以让执行到该行代码的线程进入等待状态
1、wait() 不能自动唤醒,必须调用notify()或者notifyAll()方法唤醒
sleep() 需要传入一个毫秒数,到达时间后自动唤醒
2、wait()方法会解除锁状态,其他线程可以进入运行
sleep()方法保持锁状态进入等待状态
3、wait()主要用于线程间的协调与通信。典型的使用场景是生产者-消费者问题。
sleep()主要用于让线程休眠一段时间,通常用于控制线程的执行节奏或模拟延时。
线程池
线程池 池(pool)==重用
完成线程创建和管理,销毁线程
线程任务 Runnable Callable
Java 主要是通过构建 ThreadPoolExecutor 来创建线程池的。接下来我们看一下线程池是如何构造出来的
ThreadPoolExecutor 的构造方法:七个参数
- corePoolSize:线程池中用来工作的核心线程数量。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
- keepAliveTime:超出 corePoolSize 后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,是一个阻塞队列,当线程数达到核心线程数后,会将任务存储在阻塞队列中。
- threadFactory :线程池内部创建线程所用的工厂。
- handler:拒绝策略;当队列已满并且线程数量达到最大线程数量时,会调用该方法处理任务。
线程池的运行原理 任务放置在工作队列中
1)池中是否有空闲的线程,如果有就让该线程执行任务
2)如果没有空闲的线程,判断池中的线程数量有没有达到核心线程数
3)如果没有达到核心线程数,创建新的线程执行任务,直到填满核心线程数,
如果已经达到,优先在队列中存储,直到队列填满
4)工作队列填满后在添加新的任务,是否达到最大线程数,
如果没有则创建新的线程执行任务,直到填满最大线程数
5)填满最大线程数,队列也已经填满,没有空闲的线程,就执行回绝策略
线程池中的线程达到(超过)核心线程数,超出的数量会根据存活时间,进行销毁,直到数量达到核心线程数,如果线程的数量少于核心线程数,不会消亡
四种回绝策略
ThreadPoolExecutor. AbortPolicy(); | 默认的回绝策略放弃该任务并抛出一个异常 |
ThreadPoolExecutor. CallerRunsPolicy(); | 调用者执行,让传递任务的线程执行此任务 |
ThreadPoolExecutor. DiscardOldestPolicy(); | 放弃队列中时间最长的任务,不会抛出异常,将新任务加入到队列中 |
ThreadPoolExecutor. DiscardPolicy(); | 直接放弃新任务,不会抛出异常 |
Java中内置的线程池对象
Executors.newCachedThreadPool(); | 缓存线程池:可以根据工作任务创建线程,如果没有空闲的线程就创建新的线程,线程存活60s边界线程池:设置最大线程数量 |
Executors.newFixedThreadPool(10); | 边界线程池:设置最大线程数量 |
Executors.newScheduledThreadPool(10); | 定时运行线程池:提供定时运行的处理方案 |
Executors.newSingleThreadExecutor(); | 单例线程池:创建一个具有单个线程的线程池,保障任务队列完全按照顺序执行 |