参考教程:B站狂神说Java
工具类
- CopyOnWriteArrayList:使用Lock锁保证线程安全,在进行写入操作时先把原来的数组复制并扩容,然后把值放在新的数组中
- ConcurrentHashMap:与HashTable不同,ConcurrentHashMap只在添加时对hash表中的其中一个元素加锁,而不是锁HashTable本身这个对象
- CountDownLatch:减法计数器,在初始化时设置一个计数,调用countDown方法使计数减一,使用await方法使线程阻塞,直到计数器归零
- CyclicBarrier:加法计数器,与减法计数器相反,且可以设置到达设定值时执行一条线程
- Semaphore:信号量,在初始化时设定一个值作为资源数量,通过acquire申请一个资源,没有资源则阻塞,通过release释放一个资源
- ReadWriteLock:读写锁,与普通的锁不同,ReadWriteLock内包含一个读锁和一个写锁,同一时间只能有一个线程占有写锁,但可以有多个线程占有读锁,同时当一个线程占用写锁在进行写操作时,其他线程不能拿到读锁进行读操作
- BlockingDeque:阻塞队列,本质是一个队列,但在进行出队操作时,队列中必须要有元素才能进行出队,同样在入队时队列不是满的状态时才能入队。BlockingDeque同时有4组API进行不同处理的入队出队操作
- add:队列满时进行入队操作会抛出异常;remove:队列为空时进行出队操作会抛出异常;通过element检测队首
- offer:队列满时入队不会抛出异常,返回操作结果;poll:队列空时出队不会抛出异常,返回操作结果;peek检测队首
- put:队列满时入队会阻塞等待;take:队列空时出队会阻塞等待
- offer(传入时间):队列满时入队会等待一段时间,即超时等待,超过时间则失败;poll(传入时间):与offer相同
线程池
3种默认创建
- Executors.newSingleThreadExecutor():创建只有一个线程的线程池
- Executors.newFixedThreadPool(n):创建一个线程数为n的线程池
- Executors.newCachedThreadPool():创建一个可伸缩的线程池
自定义创建线程池
以上3中默认的创建方式本质都是通过ThreadPoolExecutor进行创建,并设置了一些默认参数,因此我们可以自己调用ThreadPoolExecutor进行创建,并通过修改参数来自定义线程。通过ThreadPoolExecutor创建最多可以有7个参数
- 核心线程数。无论有多少个请求,线程池会准备的最小线程数量;在SingleThreadExecutor()中为1,FixedThreadPool(n)中为传入参数n,CachedThreadPool()为0
- 最大线程数。如果请求超过核心线程数,线程池所能准备的最大线程数量;在SingleThreadExecutor()中为1,FixedThreadPool(n)中为传入参数n,CachedThreadPool()为int的最大值(约21亿,若一直请求不释放可能造成OOM)
- 线程存活时间。当核心线程能满足请求时(即额外创建的线程为空闲时),额外线程所存活的最大时间,若超过时间没有被调用则销毁线程释放内存;SingleThreadExecutor()和FixedThreadPool(n)由于核心线程数和最大线程数一致,因此为0,CachedThreadPool()为60秒
- 时间单位。上述线程存活时间的时间单位
- 阻塞队列。当线程池中的所有线程都被请求(执行中的线程达到最大线程数),把新的请求放入阻塞队列中;SingleThreadExecutor()和FixedThreadPool(n)的队列长度约为21亿
- 线程工厂。一般为默认的线程工厂
- 拒绝策略。当阻塞队列满时,对新的请求的拒绝处理方式
- AbortPolicy:不处理新的请求并抛出异常(3种默认创建方式的拒绝策略)
- CallerRunsPolicy:让请求线程池的线程执行
- DiscardPolicy:不处理但不抛出异常
- DiscardOldestPolicy:尝试与最早执行的线程竞争,若竞争失败不会抛出异常
ForkJoin
ForkJoin可以把一个大的任务拆分成多个小任务并分配给不同的线程进行执行,类似于分治法,但与分治法不同的是,ForkJoin存在工作窃取机制,使工作较为简单的线程执行完后会尝试把其他线程的任务偷来执行,以提高程序运行的效率
比如以下情形,总任务为计算从0加到10亿的总和,正常执行的话非常耗时,此时可以使用ForkJoin不断把任务切分,直到需要累加的数小于1万再执行
public class MyForkJoinTask extends RecursiveTask<Long> {
private Long start;
private Long end;
public MyForkJoinTask(Long start, Long end){
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
Long sum = 0L;
if ((end - start) < 10000L) {
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
MyForkJoinTask forkJoinTask1 = new MyForkJoinTask(start, (start + end) / 2);
forkJoinTask1.fork();
MyForkJoinTask forkJoinTask2 = new MyForkJoinTask(((start + end) / 2) + 1, end);
forkJoinTask2.fork();
return forkJoinTask1.join() + forkJoinTask2.join();
}
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask task = new MyForkJoinTask(0L, 10_0000_0000L);
forkJoinPool.execute(task);
System.out.println((task.get()));
}
异步回调
异步回调类似于ajax,开启一个异步任务,在异步执行成功时返回执行结果以及异常,失败时返回异常,也可以使用get方法阻塞等待任务执行结果
由于java中主线程执行完会直接结束,不会等待其他线程,因此在测试时主线程结束前需要休眠等待异步线程执行完成
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println("开始执行异步任务");
sum = 0L;
for (long i = 0L; i < 10_0000_0000L; i++) {
sum += i;
}
return sum;
});
System.out.println(completableFuture.whenComplete((u, t)->{
System.out.println("执行回调");
System.out.println(u);
System.out.println(t);
}).exceptionally(e->{
e.printStackTrace();
return -1L;
}));
System.out.println("主线程等待");
Thread.sleep(5000);
System.out.println("主线程结束");
}
执行结果
开始执行异步任务
java.util.concurrent.CompletableFuture@378bf509[Not completed]
主线程等待
执行回调
500000000500000000
null
主线程结束
JMM
JAVA内存模型,是一种概念。在JAVA程序运行时只有一个主存,程序中的变量都会存储在这个主存中,而每个线程会有自己的工作内存,当线程需要用到主存中的变量时,会把主存中的变量拷贝到自己的工作内存中,并在使用完把修改后的变量同步到主存
在操作内存时涉及到以下操作
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
同时有以下约定
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
以上操作及约定来自https://www.cnblogs.com/null-qige/p/9481900.html
volatile
volatile为java的关键字,使用该关键字作用与变量上可以保证该变量的可见性及有关该变量操作的指令有序性,但不能保证原子性
-
可见性:假设同时有两个线程同时读取了主存中的一个变量拷贝到自己的工作内存中,现在线程A将这个变量修改了,而线程B对此是不知情的,会继续使用原来从主存中拷贝的未修改的变量,此时称这个变量不可见;而在这个变量加了volatile关键字后,线程A修改变量后,线程B会得知这个变量以及被修改,从而再次区主存中读取新的变量拷贝到自己的工作内存中以使用修改后的变量,此时称这个变量可见
-
指令有序性:当执行Object o = new Object()时,计算机会执行至少3条指令
- 指令A:申请一片内存空间
- 指令B:通过构造器初始化对象
- 指令C:把这个对象的引用指向A所申请的内存空间
在计算机执行时不一定会按照ABC的顺序执行,可能会由于指令重排造成执行顺序为ACB,甚至在其中加入别的操作,而使用volatile则可以保证计算机在执行时ABC的操作不会被指令重排,保证指令的有序性
-
不保证原子性:使用volatile修饰的变量与普通变量一样,不保证原子性,也就是不保证操作同步,要保证原子性需使用synchronized关键字或Lock进行加锁
CAS
java.util.concurrent.atomic包下的AtomicXxxx类中有都会有一个compareAndSet方法,该方法的第一个参数为期待的值,第二个参数为执行后的值,如果AtomicXxxx中的值与第一个参数相同,则会修改为第二个参数的值
而compareAndSet方法的底层为调用Unsafe类中的compareAndSwap方法,Unsafe类为java用于操作内存的类,该类中定义了大量本地方法,而compareAndSwap则为其中的一个本地方法,因此compareAndSet本质为调用底层C/C++的代码对值进行比较和更新