多线程编程步骤
- 创建资源类(属性 + 方法 )
- 操作方法的时候注意(线程的先后执行问题–通信)
- 判断,判断是否是自己应该操作的业务 , 如果不是 就在while中睡眠,等他其他进程业务完成后唤醒 ,此进程
- 进入自己的业务 ,
- 唤醒其他进程
- 多个线程调用资源类的方法
创建线程的方法:
- 继承Thread类
- 实现Runnable接口
- 使用Callable接口
- 重写的是call方法,call()方法可以引发异常,并且有返回值!
- 当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可 以知道该线程返回的结果。为此,可以使用 Future 对象。下面的异步回调中 说明 ,Future 这个对象有缺陷
- 使用线程池
- 多线程的异步任务都应该交给线程池
LOCK接口与Synchronized
Synchronized关键字 的作用范围 与 充当锁的对象
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{} 括起来的代码,作用的对象是调用这个代码块的对象;
- 这样要你传入 如果不传入默认this
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法 充当锁的 是 new出来的对象,即 synchronized(this);
- 修饰一个静态的方法,其作用的范围是整个静态方法,充当锁的 是 Class 字节码对象。 synchronized(this.class)
- 修饰一个类,用主 的对象是这个类的所有对象
- 注意
- 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定 义的一部分,不可以被继承!,当然如果不重写父类方法,直接调用父类方法,也是同步的了
具体表现为以下 3 种形式。
进程通信
可以使用 wait() 与 notifyAll;方法来操作 进行的先后执行
while (是否是自己的业务) {
this.wait();
}
//业务。。。。
...
//通知
this.notifyAll();
LOCK接口
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允 许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对 象。Lock 提供了比 synchronized 更多的功能。
可重入锁ReentrantLock
Lock lock = new ReentrantLock(); //注意这个地方
lock.lock(); //上锁 开锁
try {
//...业务
}catch (Exception e) {
// TODO: handle exception
//一定要要 try catch 以保证锁一定被被释放,防止死锁的发生。
}finally {
//解锁
lock.unlock();
}
Lock 与的 Synchronized 区别
- Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内 置特性。Lock 是一个类,通过这个类可以实现同步访问;
- Lock 和 synchronized 有一点非常大的不同,**采用 synchronized 不需要用户 去手动释放锁,**当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如 果没有主动释放锁,就有可能导致出现死锁现象。
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;
- 当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率。 (读写问题,Lock可以做到Synchronized 不可以 )
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源 非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。(我也不知道为什么 。有人说 是硬件与软件的实现,但是测试后的确是 Lock性能高)
进程通信
可以使用 Condition来操作线程的先后执行
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
try {
//判断
while (是否是自己的业务) {
condition.await();
}
//业务。。。。
//通知
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
集合线程安全问题
java.util.ConcurrentModificationException
-
ArrayList集合线程不安全演示
-
解决方案-Vector(利用synchronized 关键字实现,效率比较低)
-
解决方案-Collections
-
List<String> list = Collections.synchronizedList(new ArrayList<>());
-
-
解决方案-CopyOnWriteArrayList
-
(写时复制技术) 并发读,写的时候 ,先复制一个相同内容的集合,在复制的对象中写,写完后然后合并
- 可以多个线程一起读、写线程获取到锁,其他写线程阻塞
-
List<String> list = new CopyOnWriteArrayList<>(); ----------------------------------- public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
-
-
它是线程安全的。
-
因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
-
迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
-
使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代 器时,迭代器依赖于不变的数组快照。
-
-
HashSet线程不安全
-
解决方案 CopyOnWriteArraySet (写时复制技术)
-
Set<String> set = new CopyOnWriteArraySet<>();
-
-
HashMap线程不安全
-
解决方案 ConcurrentHashMap
-
Map<String,String> map = new ConcurrentHashMap<>();
-
多线程锁
公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
可重入锁
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。synchronized 与 lock都是可重入锁。
Object o = new Object();
new Thread(()->{
synchronized(o) {
System.out.println(Thread.currentThread().getName()+" 外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 内层");
}
}
}
},"t1").start();
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外层");
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 内层");
}finally {
//释放锁 甚至这里可以不释放锁,但是下面那个 线程就拿不到锁了 。因为你还没师范
//lock.unlock();
}
}finally {
//释放做
lock.unlock();
}
},"t1").start();
//创建新线程
new Thread(()->{
lock.lock();
System.out.println("aaaa");
lock.unlock();
},"aa").start();
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直
到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),即不支持并发每次只有一个此线程操
作,效率比较低
乐观锁
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去
更新这个数据(一般是用版本号来控制的 ,比如说 v1.0我更新一次 应该是v1.1 如果在v1.0的时候并发,更新的时候 就会发现 版本号对
不上了,那么 就有一个线程进行回滚 )。这就就能提高并发量
读写锁
JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁,共享读,独立写。
- 进入读锁的前提是
- 没有写进程,可以有多个读进程
- 进入写锁的前提是
- 读写进程都不可以有
三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
非公平的情况下,会出现线程饥饿的现象,因为可能一直有读,就一直不能写。
(2)重进入:读锁和写锁都支持线程重进入。(上面有些 可重入锁)
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
读书不能升级为写锁
//可重入读写锁对象
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁
//锁降级 -------- 以下代码的输出 是可以正常执行的 输出 write /n ----read 因为写锁降级了----------------
//1 获取写锁
writeLock.lock();
System.out.println("write");
//2 获取读锁
readLock.lock();
System.out.println("---read");
//3 释放写锁
writeLock.unlock();
//4 释放读锁
readLock.unlock();
//不能反过来 ,有意思 如果先读锁 ,在写锁 ,那么写肯定是进行不了 他要等 都锁释放
JUC强大的辅助类
减少计数CountDownLatch
CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法 之后的语句。
CountDownLatch countDownLatch = new CountDownLatch(6);
//6个同学陆续离开教室之后
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
//计数 -1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
循环栅栏CyclicBarrier
循环阻塞,每次执行 CyclicBarrier 一 次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作
//创建CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("*****集齐7颗龙珠就可以召唤神龙");
});
//集齐七颗龙珠过程
for (int i = 1; i <=7; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
//等待
cyclicBarrier.await();
//等 await()7 次后执行 下面的代码
//注意这里!!
// await()7 次 这里七个线程 还是开始抢占资源
} catch (Exception e) {
e.printStackTrace();
},String.valueOf(i)).start();
}
信号量Semaphore
//操作系统那本书上的pv操作
//创建Semaphore,设置许可数量
Semaphore semaphore = new Semaphore(3);
//模拟6辆汽车
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
//抢占 如果没抢到 那就等着 等着 信号量释放 可以理解为-1 单当等于 0 的时候等待
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" 抢到了车位");
//设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+" ------离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放 理解为 +1
semaphore.release();
}
},String.valueOf(i)).start();
}
阻塞队列BlockingQueue
阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据 由队列的一端输入,从另外一端输出,
为什么需要 BlockingQueue
- 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件 满足,被挂起的线程又会自动被唤起
- 我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了 ,在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细 节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
常见的阻塞队列
接口操作方法:
暂未研究
- ArrayBlockingQueue(最常使用)
- LinkedBlockingQueue
- DelayQueue
- PriorityBlockingQueue
- SynchronousQueue
- LinkedTransferQueue
- LinkedBlockingDeque
线程池
一种线程使用模式。线程过多会带来调度开销, 进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理 者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代 价。线程池不仅能够保证内核的充分利用,还能防止过分调度。即现拿现用,用完 放回去,不进行销毁。免去了销毁和创建的开销。很多xx池都是这个理论
线程池的五种创建方法:
前三种常用类型 ,前四个是 ThreadPoolExecutor 接口下的
newCachedThreadPool
-
创建一个可扩容的线程池,如果线程池长度超过处理需要,可灵活回收空 闲线程,若无可回收,则新建线程
-
ExecutorService threadPool = Executors.newCachedThreadPool(); //适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景 threadPool.execute(()->{ //业务 });
-
线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
-
线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
-
当线程池中,没有可用线程,会重新创建一个线程
newFixedThreadPool
-
创建一个固定的大小的线程池,如果没有可用线程那么就在队列中等待。
-
ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //创建五个线程的线程池 //适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景
-
线程池中的线程处于一定的量,可以很好的控制线程的并发量
-
线程可以重复被使用,在显示关闭之前,都将一直存在
-
超出一定量的线程被提交时候需在队列中等待
newSingleThreadExecutor
-
创建一个单个线程的Executor ,确保work都是单线程的,如果运行出异常那么终止当前运行的进程,给其它进程用
-
ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //创建单个线程的线程池
-
线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此 执行
newScheduledThreadPool
-
是一个可以周期性执行任务的线程池
-
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5); executor.scheduleAtFixedRate(()->{ System.out.println(Thread.currentThread().getName()+" HeartBeat..........."); },2,1, TimeUnit.SECONDS); //第一次2秒 后面每隔1秒 选择一个线程 执行一次任务
newWorkStealingPool
(ForkJoinPool接口) 核心是分发任务和收集结果 下面 有讲解
-
把一个任务拆分成多个“小任务”分发到不同的cpu核心上执行,执行完后再把结果收集到一起返回。
-
线程数量,则默认使用当前计算机中可用的cpu数量
-
以下的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。
-
public class ForkJoinTest { private double[] d; private class ForkJoinTask extends RecursiveTask<Integer> { private int first; private int last; public ForkJoinTask(int first, int last) { this.first = first; this.last = last; } protected Integer compute() { int subCount; if (last - first < 10) { subCount = 0; for (int i = first; i <= last; i++) { if (d[i] < 0.5) subCount++; } } else { int mid = (first + last) >>> 1; ForkJoinTask left = new ForkJoinTask(first, mid); left.fork(); ForkJoinTask right = new ForkJoinTask(mid + 1, last); right.fork(); subCount = left.join(); subCount += right.join(); } return subCount; } } public static void main(String[] args) { d = createArrayOfRandomDoubles(); int n = new ForkJoinPool().invoke(new ForkJoinTask(0, 9999999)); System.out.println("Found " + n + " values"); } }
线程池创建时候七个重要参数
- corePoolSize 线程池的核心线程数
- 核心线程一直存在 除非AllowcCrethreadTimeOut
- 在多线程业务中,核心业务应放到核心线程中,非核心线程应该做一些 不是特别重要的业务,当核心业务线程吃紧的时候,就可以吧非核心线程的任务先放一放,处理核心业务
- maximumPoolSize 能容纳的最大线程数
- keepAliveTime 空闲线程存活时间
- 当前线程数大雨core数量,就会释放掉非核心线程
- unit 存活的时间单位
- BlockingQueue workQueue 存放提交但未执行任务的队列 (阻塞队列)
- threadFactory 创建线程的工厂类
- RejectedExecutionHandler handler 等待队列满后的拒绝策略
(自定义线程池的时候 会用到以上七个参数)
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
面试题:
一个线程池 core 7; max 20 ,queue:50,100 并发进来怎么分配的;
先有 7 个能直接得到执行,接下来 50 个进入队列排队,在多开 13 个继续执行。现在 70 个 被安排上了。剩下 30 个默认拒绝策略。
流程
-
在创建了线程池后,线程池中的线程数为零
-
当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如 果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入 队列;
- 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,还有要进程处理任务,那么还是要创建非核心线程立刻优先运行这个任务;
- 如 果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程 池会启动饱和拒绝策略来执行。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行
-
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
- 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
- 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
Fork/Join 框架
Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情.
Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并
说白了就是一个递归的过程,不过他是用多个线程进行操作。
//创建MyTask对象
//一个递归的 操作 官网上给的是斐波那契数列
MyTask myTask = new MyTask(0,100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
//获取最终合并之后结果
Integer result = forkJoinTask.get();
System.out.println(result);
//关闭池对象
forkJoinPool.shutdown();
实现原理
- ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成
- ForkJoinTask 数组负责将存放以及将程序提交给 ForkJoinPool , 他的子类实现了不同的fork 与 join机制
- 当我们调用 ForkJoinTask 的 fork 方法时,程序会把 任务放在 ForkJoinWorkerThread 的 pushTask 的 workQueue 中,异步地 执行这个任务,然后立即返回结果
- ForkJoinTask 的 join 方法,主要是阻塞当前线程并且通过 doJoin()方法得到当前任务的状态来判断返回 什么结果
- 已完成(NORMAL)、被取消(CANCELLED)、信号(SIGNAL)和出 现异常(EXCEPTIONAL)
- 而 ForkJoinWorkerThread 负责执行这些任务。
单纯的学习笔记 ,有错请纠正