线程相关的,现在基本上都是核心,这个必须要会。
正文
有一个大的逻辑前提,就是计算机其实分为用户层和系统层的,操作系统的主要作用是调度硬件资源,同时分配硬件资源。Android AOSP其实一个基于Linux的类似于桌面应用的东西,同时提供了丰富的API和服务,帮助我们调用硬件相关及其处理硬件交互。而JAVA 的虚拟机,他最终也是基于不同的操作系统,调用不同的实现,最后也调用到了系统上面去。所以,这一趴啦下来,就是说,系统是核心。那么像创建线程,创建进程这种底层的活,他其实就是调用到是系统的函数。扯远了。
JAVA程序的执行是以进程为单位,每个JAVA 程序至少包含一个进程,main是函数的入口,他属于主线程,并在该进程中被执行。
线程
在Android 中,JAVA 线程依旧是通过用POSIX线程(pthread)库来创建线程。
volatile
用于确保多线程环境下的内存可见性,当一个变量被声明为volatile的时候,他可以确保每个线程内存中都使用最新的值。
- 禁止指令重排,编译器和处理器可能会对指令进行重排,以提高执行效率,但是多线程环境中,这可能导致数据不一致。确保了每次访问变量的顺序都是一致的。
- 保证内存的可见性,当一个线程修改了一个volatile 变量的时候,其他线程会立即看到这个变化,这是通过缓存一致性写一来实现的。
- 但是不能保证原子性,也就是说,无法保证复合操作,如自增或自减。
synchronized
是一种内置的同步机制,在多线程环境下保护共享资源的访问,他用于确保同一时刻只有一个线程可以访问被synchronized 保护的代码块或方法。当被声明为synchronized时候,JAVA 会在方法内部创建一个监视器锁,每个线程执行该方法之前必须获取这个锁,如果一个线程已经获取到这个锁,其他线程不能进入这个方法,直到锁被释放,从而保护共享资源的访问。通过这种原理,也就是说,这个关键字可以保证内存的可见性与数据的原子性。因为存在线程阻塞的问题,而volatile 可以保证数据的可见性。所以这两者是可以互补的,但是不建议同时使用。
什么是死锁
线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其他线程获取不到这个锁,就会一直等待下去。
atomic
在JAVA类中,atomic 类提供了一种实现原子操作的机制,确保在多线程环境下,对共享变量的操作是原子的,即不会被其他线程中断。
atomic 类的实现原理是通过CAS(compare and swap)指令实现的,CAS是一种基于硬件原语的操作,他可以在不使用锁的情况下实现多线程间的数据同步。CAS指令包括3个操作数,分别是需要更新的内存位置,旧的预期值和新的值。CAS指令会先比较内存位置的值是否等于旧的预期值,如果相对则将新的值写入到该内存中,否则不进行任何操作,CAS指令是一种乐观锁机制,他假设并发冲突 情况很少发生,因此不需要使用互斥锁。
如果多个线程同时对一个atomic对象进行操作,那么只有最先成功完成操作的线程会获得成功结果,其他线程的操作都会被重试或忽略,所以需要避免发生冲突的情况。
ReentrantLock
是JAVA中的一个可重入锁,他是一种比synchronized 关键字更灵活的线程同步机制,他允许一个线程多次获取同一个锁而不会产生死锁。特点:
- 可重入性,允许多次获取同一个锁,而不会产生死锁。
- 公平性,可以配置为公平锁和非公平锁,公平锁按照线程请求锁的顺序分配锁,非公平锁则没有这个限制。
- 中断获取,提供一个中断获取锁的方法,可以在获取锁的过程中中断。
- tryLock,尝试获取锁,如果被占用就返回false。
- lockinterruptibly 方法,用于尝试获取锁,如果被锁,则等待锁释放,如果等待过程中线程被中断,则抛出interruptedException。
需要手动调用lock 和unlock 方法来获取和释放锁,这就比synchronized 更加的灵活。实例代码:
csharp
Lock lock = new ReentrantLock();
public void doSth() {
lock.lock() ;
try {
// 执行某些操作
} finally {
lock.unlock();
}
}
Condition
是object 类同步方法之一,他用于在多线程环境中控制对象访问,condition接口提供了一种机制,让线程等待某个满足条件或者通知其他线程某个条件已经满足。
Condition 接口通常与 Lock 接口一起使用。Lock 接口提供了一种更灵活的锁机制,可以控制多个线程对同一个资源的访问顺序。Condition 接口则提供了在特定 Lock 对象上等待和通知的条件机制。
使用 Condition 接口,可以调用 await() 方法让当前线程等待某个条件满足,调用 signal() 方法通知等待在该 Condition 对象上的某个线程可以继续执行,调用 signalAll() 方法通知等待在该 Condition 对象上的所有线程可以继续执行。
下面是一个简单的示例代码,展示了如何使用 Condition 接口:
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
condition.signalAll(); // 通知所有等待的线程
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
count--;
condition.await(); // 等待其他线程执行 increment() 方法后继续执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
Semaphore
Semaphore 是一个计数信号量,用于控制对共享资源的访问。它维护一个计数器,表示当前可用的许可证数量。当一个线程需要访问共享资源时,它会尝试获取一个许可证,如果计数器大于零,则许可证被成功获取,线程可以继续执行;如果计数器为零,则线程需要等待,直到其他线程释放许可证。当线程使用完共享资源后,它会释放一个许可证,以便其他线程可以继续访问。
Semaphore 的使用非常灵活,可以用于实现多种并发控制逻辑。例如,可以使用 Semaphore 来限制对某个共享资源的并发访问数量,以保证该资源在同一时刻只被一定数量的线程访问。也可以使用 Semaphore 来实现线程间的协作和同步,例如使用 Semaphore 来控制一个生产者线程和消费者线程之间的数据流,保证生产者不会在所有消费者都未准备好接收数据的情况下生产数据。
在 Java 中,Semaphore 的实现类是 java.util.concurrent.Semaphore。它提供了构造函数和方法来设置初始许可证数量和获取/释放许可证。使用 Semaphore 需要对其进行实例化,并调用其 acquire() 和 release() 方法来获取和释放许可证。
下面是一个简单的示例代码,展示了如何使用 Semaphore 来限制对共享资源的并发访问数量:
java
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private Semaphore semaphore = new Semaphore(3); // 限制并发访问数量为3
public void doSomething() {
try {
semaphore.acquire(); // 获取一个许可证,如果没有可用则等待
// 执行需要访问共享资源的代码
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放一个许可证
}
}
}
在这个例子中,Semaphore 的初始许可证数量被设置为3,意味着最多有3个线程可以同时访问共享资源。当一个线程调用 doSomething() 方法时,它会尝试获取一个许可证,如果没有可用则等待。当线程执行完需要访问共享资源的代码后,它会释放一个许可证,以便其他线程可以继续访问。
CyclicBarrier
CyclicBarrier 是 Java 中的一个并发工具类,它可以用于实现多个线程之间的同步和协作。CyclicBarrier 允许一组线程互相等待,直到所有线程都到达一个公共屏障点(Barrier point),然后再一起继续执行。
CyclicBarrier 的特点是它可以重复使用,因此得名"循环"屏障。与 Semaphore 不同的是,CyclicBarrier 的计数器不会自动增加,而是需要显式地调用 reset() 方法来重置计数器。
CyclicBarrier 的使用非常灵活,可以用于实现多种并发控制逻辑。例如,可以使用 CyclicBarrier 来实现多个线程之间的同步,让它们在公共屏障点处进行等待,然后再一起执行后续的操作。也可以使用 CyclicBarrier 来实现分阶段的任务执行,每个阶段由一组线程执行,然后在所有阶段都完成后一起进行下一步操作。
在 Java 中,CyclicBarrier 的实现类是 java.util.concurrent.CyclicBarrier。它提供了构造函数和方法来设置线程数量和在屏障点处执行的回调函数。使用 CyclicBarrier 需要对其进行实例化,并调用 await() 方法来等待其他线程到达屏障点。
下面是一个简单的示例代码,展示了如何使用 CyclicBarrier 来实现多个线程之间的同步:
java
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private CyclicBarrier barrier = new CyclicBarrier(3); // 设置线程数量为3
public void doSomething() {
try {
barrier.await(); // 在公共屏障点处等待其他线程到达
// 执行需要同步的代码
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
在这个例子中,CyclicBarrier 的线程数量被设置为3,意味着只有当3个线程都到达公共屏障点时,才会一起继续执行。当一个线程调用 doSomething() 方法时,它会尝试在公共屏障点处等待其他线程到达。当所有线程都到达公共屏障点后,它们会一起继续执行需要同步的代码。
CountDownLatch
CountDownLatch 是 Java 中的一个并发工具类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch 的主要功能是提供一个计数器,用于统计需要等待的线程数量。当计数器归零时,所有等待的线程才会继续执行。
CountDownLatch 的使用非常方便,可以用于实现多种并发控制逻辑。例如,在生产者-消费者模型中,可以使用 CountDownLatch 来控制生产者和消费者之间的同步。当生产者生产完一定数量的数据后,可以调用 CountDownLatch 的 countDown() 方法减少计数器的值。消费者在消费数据之前需要先调用 CountDownLatch 的 await() 方法等待计数器归零,然后再继续执行后续操作。
在 Java 中,CountDownLatch 的实现类是 java.util.concurrent.CountDownLatch。它提供了构造函数和方法来设置计数器的初始值和获取当前计数器的值。使用 CountDownLatch 需要对其进行实例化,并调用 await() 方法来等待计数器归零,或者调用 countDown() 方法来减少计数器的值。
下面是一个简单的示例代码,展示了如何使用 CountDownLatch 来实现生产者-消费者之间的同步:
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchExample {
private static final int N = 10; // 生产者需要生产的数量
private CountDownLatch latch = new CountDownLatch(N); // 初始化计数器为N
public static void main(String[] args) throws InterruptedException {
ExecutorService producer = Executors.newFixedThreadPool(2); // 生产者线程池
ExecutorService consumer = Executors.newFixedThreadPool(2); // 消费者线程池
// 生产者线程
producer.submit(() -> {
for (int i = 0; i < N; i++) {
try {
System.out.println("Producer produced " + i);
Thread.sleep(100); // 模拟生产过程需要一定时间
latch.countDown(); // 生产完一个数据后减少计数器的值
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 消费者线程
consumer.submit(() -> {
try {
latch.await(); // 在计数器归零之前等待
System.out.println("All products are produced, consumer can start to consume");
// 消费者开始消费数据...
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.shutdown(); // 关闭生产者线程池
consumer.shutdown(); // 关闭消费者线程池
}
}
在这个例子中,我们定义了一个 CountDownLatch 的实例 latch,并初始化为 N。生产者线程会不断生产数据,每次生产完一个数据后调用 latch.countDown() 方法减少计数器的值。消费者线程在开始消费数据之前调用 latch.await() 方法等待计数器归零。当所有产品都生产完后,消费者才会开始消费数据。
线程的wait,sleep,join和yield ,notify,notifyAll
- wait 用于使得当前线程等待,直到其他线程用该线程调用notify或调用nodifyAll 方法,wait 方法需要在synchronized 块或方法中调用,以确保线程间的同步。
- sleep,用于暂停当前线程一段时间,同时也建议在synchronized 中调用。
- join 用于阻止当前线程的执行,直到调用join() 方法的线程结束,这通常用于等待另外一个线程完成其任务。
- yield 告诉操作系统,当前线程愿意放弃剩余的时间片,以使其他线程有机会允许,这个方法并不经常使用,因为他不能保证一定会导致其他线程运行。
- notify 这个方法用于唤醒在此对象上等待的单个线程。
- notifyALl 用于唤醒在此对象上等待的所以线程。
与多线程相关:Callable,future 和FutureTask
这几类都只能运行在线程池中。
- callable 这是一个接口,他允许定义一个有返回值的任务。可以返回任意类型的结果,有一个call 方法,用于执行任务。
- future 是一个接口,表示一个异步计算的结果,其中的get方法用于获取计算结果,如果计算还未完成,则会阻塞直到计算完成。
- futureTask 是future的实现类,FutureTask实现了RunnalbeFuture。因为RunnalbeFuture实现了Runnable 接口,所以这个可以通过thread 包装直接执行,也可以提交给ExcuteService 来执行。
ExecutorService 线程池
newFixedThreadPool(int corePoolSize)
: 这个方法返回一个线程池,该线程池中的线程数量是固定的。如果线程池中的线程数量小于 corePoolSize,那么新的任务将会立即启动。如果线程池中的线程数量已经达到 corePoolSize,那么新的任务将会进入队列等待。newWorkStealingPool(int corePoolSize)
: 这个方法返回一个工作窃取线程池,该线程池中的线程数量是固定的。这个线程池的特点是它的线程可以窃取其他线程池队列的任务来执行,这样可以更有效地利用系统资源。newSingleThreadExecutor()
: 这个方法返回一个单线程执行器,它只会使用一个线程来执行任务。如果这个线程由于未捕获的异常而终止,那么一个新的线程将会被创建来代替它。newCachedThreadPool()
: 这个方法返回一个缓存线程池,它将会使用一个缓存来存储多余的线程,如果线程池中的线程数量超过了需要执行的任务数量,那么这些多余的线程将会被缓存起来等待新的任务。newSingleThreadScheduledExecutor()
: 这个方法返回一个单线程调度执行器,它只使用一个线程来执行定时任务或者周期性任务。newScheduledThreadPool(int corePoolSize)
: 这个方法返回一个定时调度执行器,你可以用它来执行定时任务或者周期性任务。这个线程池中的线程数量可以是固定的,也可以是没有限制的。
线程安全
- StringBuffer
Vector
:这是一个旧的集合,线程安全,但是其性能低于ArrayList
。Vector
的每个方法都被synchronized
修饰,所以在多线程环境下是安全的。Hashtable
:类似于HashMap
,但是线程安全。所有公共的Hashtable
方法都使用synchronized
,所以多个线程可以共享单个Hashtable
。然而,与Vector
一样,Hashtable
也没有达到最高的性能。Collections.synchronizedList
: 通过Collections.synchronizedList
可以将任何List
转换为线程安全的List
。Collections.synchronizedMap
: 可以将任何Map
转换为线程安全的Map
。Collections.synchronizedSet
: 可以将任何Set
转换为线程安全的Set
。ConcurrentHashMap
: 这是一个线程安全的HashMap
实现,设计用于高并发场景。ConcurrentHashMap
中的读取操作可以在没有锁定的情况下进行,而写入操作则需要锁定部分地图。CopyOnWriteArrayList
: 这是一个线程安全的ArrayList
实现。当修改它时,它会创建并修改一个新的底层数组,然后将底层数组的引用切换到新数组。这种设计使得它在写操作时复制数组,但在读操作时不需要锁定。CopyOnWriteArraySet
: 这是一个线程安全的Set
实现,其内部使用CopyOnWriteArrayList
。BlockingQueue
接口的实现,例如ArrayBlockingQueue
,LinkedBlockingQueue
,PriorityBlockingQueue
,SynchronousQueue
, 都是线程安全的。
AysncTask
AsyncTask是Android中一个非常常用的用于在后台线程执行异步操作的工具类。它可以让开发人员在主线程之外执行耗时操作,从而避免主线程阻塞,提高应用程序的用户体验。
AsyncTask的基本原理是创建一个后台线程,将耗时操作放在该线程中执行,然后在执行完毕后通过回调方法将结果返回给主线程。
AsyncTask类需要实现四个方法:
onPreExecute()
:在执行后台任务之前被调用,通常用于在UI线程中执行一些预处理操作,例如显示一个加载对话框。doInBackground(Params...)
:在后台线程中执行耗时操作的方法。该方法需要传入一个参数列表,用于传递数据给后台任务。onProgressUpdate(Progress...)
:用于在后台任务执行过程中更新UI线程中的进度信息。该方法可以在执行doInBackground()
方法时调用,以便将进度信息传递给UI线程。onPostExecute(Result)
:在后台任务执行完毕后被调用。该方法需要传入一个参数,表示后台任务的执行结果。在UI线程中执行该方法,以便将结果展示给用户。
下面是一个使用AsyncTask的简单示例:
typescript
public class MyAsyncTask extends AsyncTask<String, Integer, String> {
private ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setMessage("Loading...");
progressDialog.show();
}
@Override
protected String doInBackground(String... params) {
// 在后台线程中执行耗时操作
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, AsyncTask!";
}
@Override
protected void onProgressUpdate(Integer... values) {
// 在UI线程中更新进度信息
int progress = values[0];
progressDialog.setProgress(progress);
}
@Override
protected void onPostExecute(String result) {
// 在UI线程中展示结果
progressDialog.dismiss();
Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show();
}
}
线程与进程的区别
-
地址空间,进程有独立的地址空间,一个进程崩溃后,在保护模型下不会对其他进程产生影响,而线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
-
资源拥有关系:系统会为进程分配资源,如堆空间,地址空间,全局变量等,这些资源会被其下的线程贡献,系统不会为线程分配内存,线程所使用的资源全部来源于所属的进程资源。
-
依赖关系:一个进程可以包含多个线程,线程依赖于进程。
-
线程有自己的私有属性,如TCB线程控制块,线程id,寄存器,硬件上下文,这些都不会被共享,而进程的私有属性,包括进程控制块PCB。
-
创建方式:进程是系统分配资源的最小单位,可以通过fork 函数创建子进程,而线程是程序执行的最小单位,通过线程的创建,同步,通信和销毁等操作实现。
-
开销:进程需要创建独立的内存空间和资源,因此开销较大。
-
并发性:进程不收其他进程影响,多个线程存在并发,共享内存及其通信。
-
独立性:进程拥有独立的内存空间和系统资源,不受其他进程影响。而一个线程崩溃,会导致进程崩溃。