知识梳理
Java虚拟机
工作区与共享区
共享区(主内存)
• 存储内容:共享区是所有线程共享的内存区域,存储了 Java 对象的实例变量、类变量等。例如,当创建一个类的多个实例时,这些实例的成员变量都存储在共享区中。
• 作用:它是线程之间进行数据交互的基础。多个线程可以访问和修改共享区中的数据,从而实现数据的共享和通信。比如,一个线程修改了共享区中的某个变量,其他线程可以读取到这个修改后的值。
工作区(线程的本地内存)
• 存储内容:每个线程都有自己独立的工作区。工作区中存储了该线程使用到的变量的副本,这些副本是从共享区拷贝而来的。例如,当一个线程访问共享区中的某个变量时,会先将该变量的值拷贝到自己的工作区中进行操作。
• 作用:工作区的存在主要是为了提高线程访问数据的效率。由于线程在自己的工作区中操作变量副本,避免了频繁地访问共享区,减少了线程之间的竞争和冲突。但同时,也可能导致数据不一致的问题,因为不同线程的工作区中的变量副本可能与共享区中的原始值不同步。
Java内存区域与内存模型
JVM中垃圾收集算法及垃圾收集器详解
GC Root&引用链
JVM类加载机制
线程启动方式:
1.继承Thread
继承 Thread类并重写 run方法,之后创建该类的实例并调用 start 方法启动线程。示例代码如下
|
typescript class MyThread extends Thread { @Override public void run() { System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行"); } } public class ThreadInheritanceExample { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } }
|
2.实现 Runnable 接口
实现 Runnable接口的 run方法,创建 Runnable实现类的实例,再将其作为参数传递给 Thread类的构造函数,最后调用 start 方法启动线程。示例代码如下:
|
typescript class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行"); } } public class RunnableExample { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); } }
|
3.实现 Callable 接口
实现 Callable接口的 call方法,该方法可以有返回值。借助 FutureTask类包装 Callable对象,再将 FutureTask对象作为参数传递给 Thread类的构造函数,最后调用 start 方法启动线程。示例代码如下:
|
java class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "线程 " + Thread.currentThread().getName() + " 执行完毕"; } } public class CallableExample { public static void main(String[] args) throws Exception { MyCallable myCallable = new MyCallable(); FutureTask<String> futureTask = new FutureTask<>(myCallable); Thread thread = new Thread(futureTask); thread.start(); System.out.println(futureTask.get()); } }
|
线程状态
1,NEW(新建状态)
当你创建一个 Thread对象,但还没有调用其 start() 方法时,线程就处于新建状态
2.RUNNABLE(可运行状态)
调用线程的 start() 方法之后,线程进入可运行状态。处于此状态的线程可能正在 Java 虚拟机中运行,也可能在等待操作系统分配 CPU 资源
3.BLOCKED(阻塞状态)
当线程试图获取一个对象的锁,而该锁当前被其他线程持有,那么这个线程就会进入阻塞状态,直到获取到锁为止
|
java class LockExample { private static final Object lock = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println("t2 获取到锁"); } }); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); // t2 可能会进入 BLOCKED 状态,等待 t1 释放锁 } }
|
4.WAITING(等待状态)
当线程调用了 Object.wait()、Thread.join()或者 LockSupport.park()方法后,会进入等待状态。处于等待状态的线程需要其他线程调用 Object.notify()、Object.notifyAll()或者 LockSupport.unpark() 方法来唤醒。示例代码如下:
|
java class WaitExample { private static final Object monitor = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (monitor) { try { System.out.println("t1 进入等待状态"); monitor.wait(); System.out.println("t1 被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(() -> { synchronized (monitor) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } monitor.notify(); System.out.println("t2 唤醒 t1"); } }); t1.start(); t2.start(); } }
|
5.TIMED_WAITING(定时等待状态)
当线程调用了带有超时参数的方法,像 Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)或者 LockSupport.parkNanos()、LockSupport.parkUntil() 时,会进入定时等待状态。在超时时间到达后,线程会自动唤醒
6.TERMINATED(终止状态)
线程的 run() 方法执行完毕,或者因为异常而提前结束,线程就会进入终止状态。一旦线程进入终止状态,就不能再重新启动
线程状态之间的转换关系可以用以下图表示
|
sql start() NEW -----------------> RUNNABLE | ^ | sleep() | | wait() | notify()/notifyAll() | join() | unpark() v | TIMED_WAITING <------ WAITING | ^ | timeout | | | v | BLOCKED <---- | ^ | get lock| v | TERMINATED <-------- RUNNABLE task done/exception
|
线程池
在Java中,提供了线程池的方法,包括但不限于:
|
Java // 创建一个固定大小为 3 的线程池 ExecutorService executor = Executors.newFixedThreadPool(3); // 创建一个缓存线程池 ExecutorService executor = Executors.newCachedThreadPool(); // 创建一个单线程线程池 ExecutorService executor = Executors.newSingleThreadExecutor();
|
本质上都是基于ExecutorService 实现的线程池,实际开发中,建议直接使用 ThreadPoolExecutor 类来创建线程池,这样可以更灵活地配置线程池的参数。
锁
乐观锁与悲观锁
乐观锁:假定在大多数情况下不会发生数据竞争,所以在操作数据时不会加锁。仅在更新数据时,会检查数据是否被其他线程修改过。如果未被修改,就进行更新;若已被修改,则采取相应措施(如重试或报错)
实现方式:通常使用版本号机制或 CAS(Compare-And-Swap)算法来实现。例如,在数据库中更新数据时,可以为每条记录添加一个版本号字段,每次更新时检查版本号是否与读取时一致。
应用场景:适用于读多写少的场景,如缓存系统
|
java import java.util.concurrent.atomic.AtomicInteger; public class OptimisticLockExample { private AtomicInteger value = new AtomicInteger(0); public void increment() { int oldValue; int newValue; do { oldValue = value.get(); newValue = oldValue + 1; } while (!value.compareAndSet(oldValue, newValue)); } }
|
悲观锁:认为在操作数据时很可能会发生数据竞争,所以在操作数据前就会加锁,以防止其他线程同时访问该数据。
实现方式:Java 中的 synchronized关键字和 ReentrantLock 类都是悲观锁的实现
应用场景:适用于写多读少的场景,如银行转账操作。
|
java public class PessimisticLockExample { private int value = 0; public synchronized void increment() { value++; } }
|
公平锁与非公平锁
公平锁
• 原理:公平锁会按照线程请求锁的顺序来分配锁,即先请求的线程先获得锁,保证了线程获取锁的公平性。
• 实现方式:ReentrantLock类可以通过构造函数传入 true 来创建公平锁。
• 应用场景:适用于对线程执行顺序有严格要求的场景。
|
java import java.util.concurrent.locks.ReentrantLock; public class FairLockExample { private ReentrantLock lock = new ReentrantLock(true); public void doSomething() { lock.lock(); try { // 执行操作 } finally { lock.unlock(); } } }
|
非公平锁
• 原理:非公平锁不保证线程获取锁的顺序,当锁被释放时,任何等待的线程都有机会获得锁。
• 实现方式:ReentrantLock 类默认创建的是非公平锁,synchronized 关键字也是非公平锁。
• 应用场景:由于非公平锁减少了线程切换的开销,所以在大多数情况下性能比公平锁高,适用于对性能要求较高的场景。
|
java import java.util.concurrent.locks.ReentrantLock; public class NonFairLockExample { private ReentrantLock lock = new ReentrantLock(); public void doSomething() { lock.lock(); try { // 执行操作 } finally { lock.unlock(); } } }
|
可重入锁与不可重入锁
可重入锁
• 原理:可重入锁允许同一个线程在持有锁的情况下,再次获取该锁,而不会发生死锁。每获取一次锁,锁的计数器就会加 1;每释放一次锁,计数器就会减 1,当计数器为 0 时,锁才会被真正释放。
• 实现方式:Java 中的 synchronized关键字和 ReentrantLock 类都是可重入锁。
不可重入锁
• 原理:不可重入锁不允许同一个线程在持有锁的情况下再次获取该锁,如果再次获取会导致死锁。
• 实现方式:Java 标准库中没有直接提供不可重入锁的实现,通常需要开发者自己实现。
• 应用场景:由于不可重入锁容易导致死锁,所以在实际开发中很少使用。
synchronized
使用场景
• 保护共享资源:当多个线程需要访问和修改共享资源时,使用 synchronized 可以保证数据的一致性。
• 保证原子操作:对于一些需要保证原子性的操作,如计数器的递增、递减等,可以使用 synchronized 来确保操作的原子性。
优点
• 使用简单:只需在方法或代码块前加上 synchronized 关键字即可,无需手动管理锁的获取和释放。
• 自动释放锁:当线程执行完被 synchronized 修饰的代码块或方法后,会自动释放锁,避免了因忘记释放锁而导致的死锁问题。
缺点
• 性能开销大:获取和释放锁的过程会带来一定的性能开销,尤其在高并发场景下,可能会成为性能瓶颈。
• 不够灵活:synchronized 是一种粗粒度的锁,无法实现更细粒度的控制,例如无法实现公平锁、超时等待等功能。
volatile关键字
保证可见性
当一个变量被 volatile修饰时,对该变量的写操作会立即刷新到主内存,读操作会直接从主内存读取。这样就保证了一个线程对 volatile 变量的修改,其他线程能够立即看到。
保证有序性
volatile 关键字可以禁止指令重排序。在 Java 中,为了提高性能,编译器和处理器会对指令进行重排序,但这种重排序可能会影响多线程程序的正确性。volatile 关键字可以保证在写操作之前的所有操作都不会被重排序到写操作之后,读操作之后的所有操作都不会被重排序到读操作之前。
|
java class VolatileOrdering { private int a = 0; private volatile boolean flag = false; public void writer() { a = 1; // 操作 1 flag = true; // 操作 2 } public void reader() { if (flag) { // 操作 3 int i = a; // 操作 4 System.out.println(i); } } }
|
在上述代码中,由于 flag变量被 volatile 修饰,操作 1 不会被重排序到操作 2 之后,操作 4 不会被重排序到操作 3 之前,从而保证了程序的正确性。
不保证原子性
volatile 关键字无法保证变量操作的原子性。原子操作是指不可分割的操作,要么全部执行,要么全部不执行。
|
java class VolatileAtomicity { private volatile int count = 0; public void increment() { count++; } public int getCount() { return count; } } public class MainAtomicity { public static void main(String[] args) throws InterruptedException { VolatileAtomicity example = new VolatileAtomicity(); Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { example.increment(); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println("Count: " + example.getCount()); } }
|
在这个例子中,count变量被 volatile修饰,但 count++ 操作不是原子操作,它包含读取、加 1 和写入三个步骤。在多线程环境下,可能会出现多个线程同时读取到相同的值,然后各自加 1 后写入,导致最终结果小于预期值。
Activity
生命周期
[图片上传失败...(image-d99c30-1744184152044)]
来源Android官网
启动模式
** standard(标准模式)**
• 特点:
○ 这是 Activity 的默认启动模式。每次启动一个 Activity,系统都会在启动它的任务栈中创建一个新的实例,不管这个实例是否已经存在。
○ 新创建的 Activity 会被压入任务栈的栈顶,遵循后进先出(LIFO)的原则。
○ 多个相同的 Activity 实例可以同时存在于任务栈中。
singleTop(栈顶复用模式)
• 特点:
○ 如果要启动的 Activity 已经位于任务栈的栈顶,系统不会重新创建该 Activity 的实例,而是调用其 onNewIntent() 方法,将新的 Intent 传递给它。
○ 如果要启动的 Activity 不在栈顶,系统会创建一个新的实例并将其压入栈顶。
singleTask(栈内复用模式)
• 特点:
○ 如果要启动的 Activity 已经存在于任务栈中,系统会将该 Activity 之上的所有 Activity 出栈,使该 Activity 成为栈顶元素,并调用其 onNewIntent() 方法。
○ 如果要启动的 Activity 不存在于任务栈中,系统会创建一个新的实例并将其压入任务栈。
○ singleTask 模式的 Activity 默认会在一个新的任务栈中启动,也可以通过 android:taskAffinity 属性指定其所属的任务栈。
singleInstance(单实例模式)
• 特点:
○ 系统会为 singleInstance 模式的 Activity 创建一个新的任务栈,并且该任务栈中只能有这一个 Activity 实例。
○ 当再次启动该 Activity 时,系统会直接复用该实例,不会创建新的实例。
○ 其他 Activity 无法和 singleInstance 模式的 Activity 位于同一个任务栈中。
Activity跳转
从 Activity1跳转到 Activity2
|
Java Activity1: onPause() Activity2: onCreate() -> onStart() -> onResume() Activity1: onStop()
|
横竖屏切换
|
Java onPause() -> onStop() -> onDestroy() -> onCreate() -> onStart() -> onResume()
|
避免 Activity 重建
若不想让 Activity 在横竖屏切换时重建,可以在 AndroidManifest.xml 文件中为 Activity 添加 android:configChanges 属性,会调用onConfigurationChanged
|
Java <activity android:name=".MainActivity" android:configChanges="orientation|screenSize"> </activity>
|
Service
Service是四大组件之一,启动的时候是IPC跨进程通信,与Activity类似,但没有界面,是运行在后台的,启动过程分为两种分别是startService和bindSerrvice,当Service启动后,多次启动有且仅有一个Service实例。
生命周期
[图片上传失败...(image-88c48b-1744184152044)]
启动方式
|
启动方式
|
停止
|
用途
|
|
startService
|
stopService
|
启动后台服务
|
|
bindService
|
unbindService
|
通信
|
startService和bindService混合型
当一个Service在被启动(startService)的同时又被绑定(bindService),该Service将会一直在后台运行,并且不管调用几次,onCreate方法始终只会调用一次,onStartCommand的调用次数与startService调用的次数一致(使用bindService方法不会调用onStartCommand)。同时,调用unBindService将不会停止Service,必须调用stopService或Service自身的stopSelf来停止服务。