Я участвую в "Наггетс·Стартовый план"
Параллелизм заключается в одновременном выполнении нескольких действий, таких как сбор сетевых данных, загрузка файлов, чтение и запись базы данных и другая трудоемкая бизнес-логика. Все это требует от нас использования подпотоков для обработки, каждый поток имеет четкое разделение труда, основной поток может лучше справляться с другими вещами, избегая ANR.
На самом деле наше приложение имеет много потоков, потому что скорость вычислений процессора слишком высока, и он может быстро создавать потоки и переключаться между ними. Глядя на время, кажется, что одновременно выполняется несколько потоков. Если с точки зрения сокращения времени, он по-прежнему выполняет другой поток после выполнения потока.
Разница между процессом и потоком
- Процесс — это основная единица распределения ресурсов операционной системы.Каждый процесс имеет свое виртуальное адресное пространство и не зависит друг от друга. Когда приложение Android запущено, для его размещения создается процесс, который выделяется и управляется системой, включая такие ресурсы, как память, занимаемая процессом. В Android каждая прикладная программа будет выполняться в независимом процессе, что может обеспечить изоляцию между приложениями и повысить стабильность системы.
- Поток — это единственный путь выполнения внутри процесса, а процесс может запускать несколько потоков одновременно. Потоки используют общую память для связи, поэтому при доступе к общим данным между потоками необходимо учитывать безопасность потоков. В Android нам нужно создавать дочерние потоки для выполнения трудоемких операций, чтобы основной поток мог беспрепятственно реагировать на действия пользователя.
Потоки выполняются в определенном порядке
использовать присоединиться
join может обрабатывать некоторые сценарии, в которых необходимо дождаться завершения задачи, прежде чем продолжить ее выполнение.
Thread t1 = new Thread(() -> System.out.println("Executing thread 1"));
Thread t2 = new Thread(() -> System.out.println("Executing thread 2"));
Thread t3 = new Thread(() -> System.out.println("Executing thread 3"));
try {
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
复制代码
Использование однопоточного пула потоков
Thread t1 = new Thread(() -> System.out.println("Executing thread 1"));
Thread t2 = new Thread(() -> System.out.println("Executing thread 2"));
Thread t3 = new Thread(() -> System.out.println("Executing thread 3"));
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(t1);
executorService.submit(t2);
executorService.submit(t3);
复制代码
Используйте ожидание/уведомление, чтобы дождаться механизма уведомления
wait является методом объекта, его функция заключается в том, чтобы заставить текущий поток войти в состояние ожидания, позволить текущему потоку снять удерживаемую им блокировку, пока другие потоки не вызовут метод notify или notifyAll этого объекта, и текущий поток не будет пробужден.
boolean run1, run2;
final Object lock1 = new Object();
final Object lock2 = new Object();
复制代码
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Executing thread 1");
run1 = true;
lock1.notify();
}
});
Thread t2 = new Thread(() -> {
synchronized (lock1) {
try {
if (!run1) {
lock1.wait();
}
synchronized (lock2) {
System.out.println("Executing thread 2");
lock2.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3 = new Thread(() -> {
synchronized (lock2) {
try {
if (!run2) {
lock2.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Executing thread 3");
});
t1.start();
t2.start();
t3.start();
复制代码
Thread.sleep(0) спать или нет
После того, как поток освобождает ресурсы ЦП, операционная система пересчитывает приоритеты всех потоков для перераспределения ресурсов ЦП, поэтому реальный смысл сна состоит не в том, чтобы сделать паузу, а в том, чтобы не участвовать в соревновании ЦП в следующий период времени, и ждать, пока ЦП перераспределяется.После завершения, если приоритет не изменился, продолжайте выполнение, поэтому реальное значение сна (0) состоит в том, чтобы вызвать перераспределение ресурсов ЦП.
synchronized 到底锁住了啥
实例方法
public synchronized void method() {
// 锁住的是该类的实例对象
}
复制代码
静态方法
public static synchronized void method() {
// 锁住的是类对象
}
复制代码
实例对象代码块
synchronized (this) {
// 锁住的是该类的实例对象
}
复制代码
类对象代码块
synchronized (Test.class) {
// 锁住的是类对象
}
复制代码
任意实例对象代码块
Object obj = new Object();
synchronized (obj) {
// 锁住的是配置的实例对象
}
复制代码
多线程的三个特性
- 可见性:如果一个线程对于某个共享变量进行更新之后,后续访问该变量的线程可以读取到该更改的结果,那么我们就说这个线程对于共享变量的更新是可见的。
- 原子性:访问某个共享变量的操作从其执行线程之外的线程来看,该操作要么已经执行完毕,要么尚未发生,其他线程不会看到执行操作的中间结果。非原子操作存在线程安全问题,需要我们使用同步,比如使用 sychronized 让它变成一个原子操作。一个操作是原子操作,那么称它具有原子性。
- 有序性:程序在执行的时候,程序的代码执行顺序和语句顺序是一致的。
volatile,synchronized 和 Lock
- volatile:可以保证线程对变量的修改对其他线程可见,其作用是强制将修改后的值立即写入主存,并通知其他线程刷新缓存。volatile 可以保证变量在线程之间的可见性,但并不能保证线程安全,因为不具备原子性,当多个线程同时进行读写操作时,仍然可能出现数据不一致的情况。
- synchronized:可以保证在同一时间只有一个线程访问一个共享资源,其他线程需要等待该线程访问结束才能继续执行,既具有原子性又具有可见性,可以保证多个线程访问共享资源时数据的一致性和正确性,但使用 synchronized 可能会使其他线程阻塞,导致性能下降。
- Lock:是一个接口,提供了一种比 synchronized 更加灵活的方式来实现同步,允许多个线程同时访问同一个共享资源,在使用 Lock 机制时,必须手动加锁和释放锁。
public class LockTest {
public static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
testSync();
}, "t1").start();
new Thread(() -> {
testSync();
}, "t2").start();
}
public static void testSync() {
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
复制代码
volatile 可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或某个变量的当前值与修改后的值之间没有约束。
Atomic 包
Atomic 包(java.util.concurrent.atomic)提供了一系列用于处理原子操作的类和方法,以确保多线程环境下的线程安全和数据一致性,如 AtomicInteger,AtomicBoolean,AtomicLong 等。
public class AtomicTest {
private static AtomicInteger count = new AtomicInteger(1);
public static void main(String[] args) {
new Thread(() -> {
// 以原子方式将输入的数值与实例中的值相加,并返回结果。
count.addAndGet(2);
}).start();
new Thread(() -> {
System.out.println(count.get());
}).start();
}
}
复制代码
线程池
- 核心线程:有新任务提交时,先检查核心线程数,如果核心线程都在工作,而且数量也已经达到最大核心线程数,则不会继续新建核心线程,而会将任务放入等待队列。
- 等待队列 :等待队列用于存储当核心线程都在忙时,继续新增的任务,核心线程在执行完当前任务后,会去等待队列拉取任务继续执行,这个队列一般是一个线程安全的阻塞队列,它的容量也可由开发者根据业务来定制。
- 非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,其实,核心线程和非核心线程本质上没有什么区别。
- 饱和策略:当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。
- 线程活动保持时间:线程空闲下来之后,保持存活的持续时间,超过这个时间还没有任务执行,该工作线程结束。
线程池的好处:
- 降低资源消耗,通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性,使用线程池可以进行统一分配和监控。
线程池的构造
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize, //允许创建的最大线程数
long keepAliveTime, //工作线程空闲后保持存活的时间
TimeUnit unit, //保持存活的时间单位
BlockingQueue<Runnable> workQueue, //任务队列
//拒绝策略
RejectedExecutionHandler handler) { ... }
复制代码
任务队列有几个可供选择:
- ArrayBlockingQueue:一个基于数组结构的阻塞队列,按照先进先出的原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,按照先进先出的原则对元素进行排序,newFixedThreadPool 就是使用这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列,每插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,newCachedThreadPool 就是使用这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
public class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("Perform Tasks");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(1);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(new Task());
}
复制代码
提交任务有两种方式:submit 和 execute。submit 会返回一个 Future 类型的对象,通过这个对象可以判断任务是否执行成功,并且可以通过 get 方法来获取返回值,而 execute 用于提交不需要返回值的任务。
关闭线程池有两种方式:shutdown 和 shutdownNow。shutdown 只是将线程池的状态设置为 SHUTWDOWN 状态,正在执行的任务会继续执行下去,没有被执行的则中断。而 shutdownNow 则是将线程池的状态设置为 STOP,正在执行的任务则被停止,没被执行任务的则返回。
线程池的种类:
newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
try {
Thread.sleep(1000);
System.out.println(" CurrentThread: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdown();
复制代码
newFixedThreadPool:创建一个定长的线程池,可控制最大并发数,超出的线程进行队列等待。
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Current Thread: " + Thread.currentThread().getName());
});
}
executorService.shutdown();
复制代码
newCacheTreadPool:创建一个可以缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程。该线程池的最大核心线程为无限大,当执行第二个任务时第一个任务已经完成,则会复用执行第一个任务的线程,否则会新建一个线程。
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
System.out.println("Current Thread: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
复制代码
newScheduledThreadPool:创建一个定长的线程池,支持定时或周期任务执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
scheduledThreadPool.scheduleAtFixedRate(() -> {
System.out.println("Current Thread: " + Thread.currentThread().getName());
}, 2, 1, TimeUnit.SECONDS); // 周期任务:延迟2秒钟后每隔1秒执行一次任务
复制代码