JUC并发简单学习小结
JDK API文档:在线文档-jdk-zh (oschina.net)
一、解决ArrayList集合线程不安全的方法:
1.使用vector来存储
2.使用Collection工具类中的synchroizedList方法,参数列表传入ArrayList类型的对象即可(new ArrayList)
3.使用CopyOnWriteArrayList写时复制技术,优点:读取时允许并发,写入数据时独立,每次写复制出一份一样的集合,完成后覆盖合并原来的集合内容。后面要做修改时读取的数据是前者修改后新的集合内容。底层源码采用锁ReentrantLock来实现。
List<String> list new CopyOnWriteArrayList<>();
二、解决HashSet和HashMap线程不安全的方法:
1.使用CopyOnWriteArraySet
Set<String> set new CopyOnWriteArraySet<>();
2.使用ConcurrentHashMap
Map<String,String> map = new ConcurrentHashMap<>();
三、Synchronized锁的几种情况
1.修饰普通方法时,锁的是调用该方法的对象。该对象在处理该方法结束前,其他线程(调用被Synchronized修饰的方法)需等待。
2.修饰静态方法时,锁的是整个类。涵盖所有对象,但仅对被static sychronized修饰的方法生效。
3.一个类中,没有Synchronized关键字修饰的方法不被对象锁影响,也就是说可以对该对象方法进行并发操作。
四、公平锁与非公平锁
Lock lock = new ReentrantLock(false);//非公平锁,会导致其他线程饿死现象,效率高
Lock lock = new ReentrantLock(true);//公平锁,保证所有线程都能拥有过资源,效率相对较低,原因是需要做判定其他线程是否拥有过资源
Synchronized(隐式)和Lock(显式)都是可重入锁
五、死锁
验证死锁:
1.jps 类似于linux ps -ef 查看进程
2.jstack jvm自带堆栈跟踪工具
第一步,命令行输入(需先配置环境变量)jps -l,找到进程号
第二步,输入jstack+进程号
六、Callable接口
线程创建方式
1.创建接口实现类方式
//创建实现类
Class Cb implements Callable{
@Override
public Interger call() throws Exception{
return 200;
}
}
//在主类中矿建FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(new Cb());
2.使用lamda表达式创建
FutureTask<Integer> futureTask = new FutureTask<>(()->{
return 200;
});
FutureTask未来任务原理
在不影响主线程工作前提下,可以单开另一个线程做其他任务,最后汇总,只汇总一次。
例子:调用futureTask.get()方法需要进行相应计算得到返回值200,再次调用该方法时,则直接拿到结果,这就是汇总一次的解释。
//线程创建
new Thread(futureTask,"lily").start();
七、辅助类
CountDownLatch:
创建对象,设置初始值。new CountDownLatch(6)
countDownLatch.countDown();计数器-1
countDownLatch.await()方法,可以判断计数器是否为0,
如果不为零,则不继续执行下面代码。
CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier (number,()->{
//重写run方法的内容
});
一般用于循环语句中
第一个参数为数值,第二个重写Runable接口的run方法。
调用CyclicBarrier的await()方法, 当循环次数未达到指定数时,处于等待状态,只有达到指定数时才会执行CyclicBarrier对象中的语句(指run()方法中的内容)。
Semaphore
创建Semaphore对象,传入许可数量number
Semaphore semaphore = new Semaphore(number);
每次创建新线程时,在线程run()方法中调用Semaphore的acquire()方法,实现抢占位置,当数量到达许可数量时,后面线程需要等待。正在进行的线程结束后会调用Semaphore的release()方法,释放资源,其他线程会被唤醒继续工作,直到许可数又达到时进入等待状态。
八、乐观锁与悲观锁
悲观锁:一个线程对资源进行操作时,其他线程不能获得该资源。等待操作结束后才能对其进行操作。保证了线程资源独立,但效率慢。
乐观锁:支持并发操作,资源会有一个默认版本号,处理快并先释放资源的线程会先比对资源版本号,若一致,则操作可以成功,并更新资源的版本号,其他处理慢的线程由于不能匹配版本号会导致操作失败。
九、表锁与行锁
表锁:对整张表进行操作时会获得该锁,其他人都不能对表进行修改。
行锁:对表中的某行进行操作时会获得该行的锁,其他人不能对该行进行操作,但可以操作其他行的内容。
注:这种锁会造成死锁现象。
十、读写锁
使用原因:当线程对资源进行写入时,其他线程能对资源内容进行读取,这时前面线程的写入操作可能没有完成,导致读取时无法读出修改后的内容。
创建对象
private ReadWriteLock rwLock = new ReentrantReanWriteLock();
写锁:对数据进行写入前,调用writeLock().lock()方法加锁
线程写入操作完成后,调用writeLock().unlock()解锁。
读锁:同样的对线程读取操作前调用readLock().lock()方法加锁
结束后,调用unlock()释放锁。
区别:写锁是独占锁,保证操作线程仅有一个。
读锁是共享锁,可以并发读。读写锁不能同时存在,读的时候不能有写,两者互斥效果。写操作时可以读,这里是锁降级的效果。
缺点:会造成锁饥饿,一直读,无法写入。
十一、锁的降级
锁只能降级不能升级。
写锁可以降级为读锁,读锁不能升级为写锁。
(自己的傻瓜解释:写数据时包含了读和写两个操作,所以比单纯的读高级,所以需要降级为读。)
理解:对资源进行写入操作时,需要先读取资源原来的数据,然后才能进行写,而对资源进行读取时,不能进行写的操作。
十二、阻塞队列
add和remove方法,失败时会报错,成功时add返回true,remove返回对应元素
public class BlockingQueueDemo {
public static void main(String[] args) {
//创建长度为3的阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
boolean a = blockingQueue.add("a");
System.out.println(a);
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
//检查队列头元素
System.out.println(blockingQueue.element());
//由于队列长度最大值为3,继续添加会报错
blockingQueue.add("w");
}
}
报错信息:
Exception in thread “main” java.lang.IllegalStateException: Queue full
//队列遵循先进先出原则,当队列中为空时,继续remove会报错
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
报错信息
Exception in thread “main” java.util.NoSuchElementException
offer和poll方法,失败时offer返回false,poll返回null,成功时offer返回true,poll返回对应元素值
这两个方法可以设置阻塞时间,超过规定时间没有完成对应操作就会结束。
blockingQueue.offer("aa",3, TimeUnit.SECONDS);
blockingQueue.poll(3, TimeUnit.SECONDS);
put和take方法,当队列满时,继续put添加元素会造成线程阻塞(测试时程序一直处于运行状态),直到队列中腾出空间。put方法无返回值(void)。当队列为空时,继续调用take取出元素也会造成线程阻塞,直到队列中添加了新的元素,take方法会返回对应元素值。
十三、线程池
事先创建好规定数量的线程,放在线程池中等待,需要处理业务时直接调用即可,省去了每次处理就要创建线程,关闭线程,销毁线程的时间,提高了运行效率。
一池多线程:newFixedThreadPool()
使用:
public class ExecutorsDemo {
public static void main(String[] args) {
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);//最大线程数
try {
for (int i = 0; i < 10; i++) {
//只有在线程池对象调用execute()方法时线程才会创建。
threadPool1.execute(()->{
//重写Runnable方法
System.out.println(Thread.currentThread().getName()+"办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//处理完关闭线程池,否则导致程序无法停止,占用资源
threadPool1.shutdown();
}
}
}
一池一线程:newSingleThreadExecutor()
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();//单线程
一池可扩容线程:newCachedThreadPool()
ExecutorService threadPool3 = Executors.newCachedThreadPool();//可扩容
三种方式创建线程池的底层原理都创建了ThreadPoolExecutor,并且有各自默认的7个参数。
注:实际开发中不采用这三种方式创建(可能会堆积大量的请求,从而导致oom),一般使用自定义的线程池。
十四、线程池工作流程与拒绝策略
发起处理请求调用execute()方法时,首先判断线程池中核心线程(corePool)是否被占用,若有闲置线程则使用该线程进行处理,没有则将请求推入阻塞队列(BlockingQueue)。当阻塞队列满时,后面的请求将直接为其创建新线程直至达到线程池中最大存在线程数量(maximumPool)。当前线程池中所有线程都在工作,阻塞队列又满的情况,这时会对后面请求采用拒绝策略。
自定义线程池
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//线程池允许的最大线程数
long keepAliveTime,//线程池存活时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//拒绝策略)
使用:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),//默认线程工厂(创建线程的作用)
new ThreadPoolExecutor.AbortPolicy()//直接抛出异常,阻止系统运行
);
try {
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(()->{
//重写Runnable方法
System.out.println(Thread.currentThread().getName()+"办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//处理完关闭线程池,否则导致程序无法停止,占用资源
threadPoolExecutor.shutdown();
}
十五、分支合并框架(fork/join)
以下是实现0到100累加的拆分合并例子
条件为累加的数字范围不能相差超过10
class ForkJoin extends RecursiveTask<Integer>{
private static final Integer value = 10;
private int begin;
private int end;
private int result;
//有参构造
public ForkJoin(int begin,int end){
this.begin = begin;
this.end = end;
}
//拆分和合并过程
@Override
protected Integer compute() {
//首先进行判断
if (end - begin>value){
//取中间值
int middle = (begin+end)/2;
//拆分左边
ForkJoin forkJoin1 = new ForkJoin(begin,middle);
//拆分右边
ForkJoin forkJoin2 = new ForkJoin(middle+1,end);
//调用fork拆分
forkJoin1.fork();
forkJoin2.fork();
//合并结果
result = forkJoin1.join()+forkJoin2.join();
}else {
for (int i = begin; i <= end; i++) {
result += i;
}
}
return result;
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建目标对象
ForkJoin forkJoin = new ForkJoin(0, 100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
//提交到分支合并池
ForkJoinTask<Integer> task = forkJoinPool.submit(forkJoin);
//获取最后结果
Integer integer = task.get();
System.out.println(integer);
//关闭池对象
forkJoinPool.shutdown();
}
}
十六、异步回调
同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去。
异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待。
——引用:[杉菜666]https://www.cnblogs.com/lgyxrk/
两种基础方法分为runAsync()和supplyAsync()
异步回调方法说明
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//异步调用,无返回值
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName()+"completableFuture1");
});
completableFuture1.get();
//异步调用,有返回值
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"completableFuture2");
int t = 1/0;//模拟异常
return 10;
});
completableFuture2.whenComplete((t,u)->{
System.out.println("t="+t);//保存的是返回值
System.out.println("u="+u);//保存的是异常信息
}).get();
}
}