JAVA多线程相关知识学习总结 |
阅读目录
一、信号量。
二、线程池。
三、Lock接口及重入锁。
四、ThreadLocal。
五、原子操作类。
六、生产者-消费者模式。
一、信号量
信号量的概念:简单来说,信号量就是一种在多线程情况下,协调多个线程并分配公共资源的工具。它维护了一个许可集,在许可不可用时堵塞所有的资源请求。线程可以通过申请和释放方法获取或者释放访问许可。信号量也有方法可以增加一个许可。
- 举个例子:以前去上海的时候,在陆家嘴有一家迪士尼旗舰店。去的时候人很多,有一个工作人员在门口组织顾客排队,保持店内人数一个量,里面的人出来多少,外面多少人再进去。这个工作人员就相当于“信号量”。
实现一个基础的信号量:
package MyThread;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class Semaphore {
private List<Object> locks = Collections.synchronizedList(new ArrayList<Object>());
private int permitNum = 1;
private int nowPermitNum = 1;// 当前的许可数量
private boolean permitNumGrow = false;
private boolean fair = false;
Random random = new Random();
public Semaphore(int permitNum, boolean permitNumGrow, boolean fair) {
this.fair = fair;
this.permitNum = permitNum;
this.nowPermitNum = permitNum;
this.permitNumGrow = permitNumGrow;
}
public Semaphore(int permitNum) {
this(permitNum, true, false);
}
public Semaphore() {
this(1);
}
public void acquire() {
Object lock = new Object();
synchronized (lock) {
if (nowPermitNum > 0) {
nowPermitNum--;
} else {
locks.add(lock);
try {
lock.wait();// 没有许可就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void release() {
if (locks.size() > 0) {
int index = 0;
if (!fair) {
index = Math.abs(random.nextInt()) % locks.size();
}
Object lock = locks.get(index);
locks.remove(lock);
synchronized (lock) {
lock.notify();// 唤醒一个等待的线程
}
} else if (nowPermitNum < permitNum || permitNumGrow) {
nowPermitNum++;
}
}
public int getAvailablePermitNum() {
return nowPermitNum;
}
}
(JDK5过后提供了新号量的默认实现)
二、线程池
线程池的产生:线程的创建和销毁都需要毕竟大的系统资源,在JVM里过多的创建线程可能会导致系统内存消耗过度资源耗尽。为了减少线程的创建和销毁,尽可能的利用现有的对象进行服务,线程池就诞生了。
线程池的作用:线程池通过对多个任务重复使用线程,线程的创建消耗就分摊到了这多个任务上,这样的同时在请求到达的时候,线程就已经创建完毕,减少了创建线程带来的延迟。解决了线程生命周期开销资源消耗大的问题,响应也变得更快。
线程池的组件:
-
线程池管理器:
- 创建、销毁、管理线程,将工作中的线程放入线程池。
-
工作线程
- 可以循环执行任务的线程,在没有任务执行的时候进行等待。
-
任务队列
- 提供一种缓冲机制,将没有处理的任务放入任务队列。
-
任务接口
- 每个任务必须实现的接口,主要用来规定任务的入口、任务执行过完后的收尾工作、工作执行的状态等,工作线程通过该接口调度任务的执行。
简单实现:
package com.chinasofti.se.threads;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService service = Executors.newScheduledThreadPool(5);
Set<Callable<Integer>> tasks = new HashSet<>();
tasks.add(()->{
String name = Thread.currentThread().getName();
for (int i = 0; i <10; i++) {
System.out.println(name+(i+1)+"只绵羊...");
}
return 1;
});
tasks.add(()->{
String name = Thread.currentThread().getName();
for (int i = 0; i <10; i++) {
System.out.println(name+(i+1)+"颗星星...");
}
return 2;
});
tasks.add(()->{
String name = Thread.currentThread().getName();
for (int i = 0; i <10; i++) {
System.out.println(name+(i+1)+"颗沙子...");
}
return 3;
});
List<Future<Integer>> result = service.invokeAll(tasks);
for (Future<Integer> future : result) {
System.out.println("线程任务执行结果"+future.get());
}
service.shutdown();
}
private static void testPool() throws InterruptedException, ExecutionException {
ExecutorService service = Executors.newScheduledThreadPool(5);
for (int j = 0; j < 10; j++) {
Future<?> future = service.submit(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + " 执行任务");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("得到结果" + future.get());
}
service.shutdown();
}
}
(JDK5引入的Executor框架的最大优点是把任务的提交和执行解耦,要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。)
三、Lock接口及重入锁
Lock的由来:为了清晰的表达如何加锁和释放锁,JDK5中加入了Lock接口,提供了更清晰的语义。
Lock的默认实现:
- ReentrantLock:
- 可重入的独占锁。该对象与synchronized关键字有着相同的表现和更清晰的语义,而且还具有一些扩展的功能。可重入锁被最近的一个成功lock的线程占有(unlock后释放)。该类有一个重要特性体现在构造器上,构造器接受一个可选参数,是否是公平锁,默认是非公平锁。
- 公平锁:先来一定先排队,一定先获取锁。
- 非公平锁:不保证上述条件。非公平锁的吞吐量更高。
- 可重入的独占锁。该对象与synchronized关键字有着相同的表现和更清晰的语义,而且还具有一些扩展的功能。可重入锁被最近的一个成功lock的线程占有(unlock后释放)。该类有一个重要特性体现在构造器上,构造器接受一个可选参数,是否是公平锁,默认是非公平锁。
四、ThreadLocal
ThreadLocal的由来:为了在同一个线程中共享一个变量(既不同线程的变量取值不同)。创建一个共享的散列表,用线程对象作为键,需要线程中共享的变量作为值保存起来,每次使用散列表.get(Thread.currentThread())的方式获取共享变量的本线程版本。 JDK优化了上述过程打造出了性能更加优秀的线程内变量共享工具ThreadLocal。
ThreadLocal的作用:为了解决多线程程序中的并发问题。
- ThreadLocal并不是一个Thread,而是Thread局部变量。当ThreadLocal维护变量时,它为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立的随意改变自己的副本,而不会影响到其他线程的变量副本。
- ThreadLocal的方法:
- void set(T value)
- 将此线程局部变量的当前线程副本中的值设置为指定值。
- void remove()
- 移除此线程局部变量当前线程的值。
- protected T initialValue()
- 返回此线程局部变量的当前线程的“初始值”。
- T get()
- 返回此线程局部变量的当前线程副本中的值
- void set(T value)
五、原子操作类
原子性:所谓原子性即指该操作无法分割,即便是在多线程环境下,进入该操作后只有完成操作的所有步骤才能被其他线程阻断,因此不会出现外部线程插入操作导致数据失效问题。
原子操作类:原子操作类相当于泛化的Volatile变量,能够支持原子读取-修改-写操作。
原子操作类在Java.util.concurrent.atomic包下,可以分为四种类型的原子更新类:
- 原子更新基本类型
- 原子更新数组类型
- 原子更新引用
- 原子更新属性
具体到每个类实现方法都不同,读者可以自行查Api。
六、生产者-消费者模式
此模型有如下几点:
- 生产者仅仅在仓储未满时候生产,仓满则停止生产
- 消费者仅仅在仓储有产品时候才能消费,仓空则等待
- 当消费者发现仓储没产品可消费时候会通知生产者生产
- 生产者在生产出可消费产品时候,应该通知等待的消费者去消费
(代码等笔者彻底掌握后会贴上来,读者请自行找相关文章了解。)
感谢您的阅读,如果有误欢迎指出。后续会找时间完善该文章。