文章目录
1. 进程和线程概述
用户线程:自定义线程 主线程结束了,用户线程还在运行,jvm存活
守护线程:如垃圾回收 没有用户线程了,都是守护线程,jvm结束
【*】主线程结束了,用户线程可能还没结束。
Synchronized关键字
修饰代码块,实例方法,静态方法
-
实例方法(非静态)
synchronized不属于方法定义的一部分,因此synchronized关键字不能被继承。若子类重写了父类带synchronized修饰的方法,子类方法默认是不同步的,需要显示的加上synchronized方法。或者调用父类的方法。
对于实例方法,如果多个线程对不同实例对象中的共享资源(类变量、静态变量)进行修改,加了synchronized修饰也会出错。而若是多个线程对同一个实例对象中的共享资源(静态变量,成员变量)进行修改,加synchronized修饰则是有效的。
-
静态方法
对静态方法加锁,锁是当前类的class对象锁
-
代码块
被synchronized修饰的代码块,当一个线程获取了锁并执行代码时,其它线程只能等待。等待已经获取锁的线程释放锁。
释放锁的情况:
1)代码块执行完了,释放锁
2)线程异常,JVM会让现场释放锁
如果获取锁的线程由于等待IO或者其它原因(如调用sleep方法)被阻塞,但是没有释放锁,其它线程只能等待。可以通过设置等待时间或者响应中断处理机制来解决。
多线程编程步骤
- 创建资源
类
,定义属性和操作方法。 - 创建多个
线程
,调用资源类的操作方法
package JUC.Guigu.chapter1;
public class SaleTickets {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// 2.创建多个线程
// 对同一个资源类对象进行操作
new Thread(() -> {
for (int i =0; i < 40; i++) {
ticket.sale();
}
}, "窗口1").start();
new Thread(() -> {
for (int i =0; i < 40; i++) {
ticket.sale();
}
}, "窗口2").start();
new Thread(() -> {
for (int i =0; i < 40; i++) {
ticket.sale();
}
}, "窗口3").start();
}
}
// 1. 创建资源类
class Ticket{
// 票数量
private int number = 100;
// 操作方法, 非静态同步方法。
// 只能保证一个实例对象在多个线程中的同步性。
// 不能保证多个实例对象在多个线程中的同步性。
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + ": 总票数: " + number-- + " 余票: " + number);
}
}
}
Lock接口:手动加锁解锁
可重入锁:ReentrantLock
可重入锁:ReentrantLock.lock()
, ReentrantLock.unlock()
import java.util.concurrent.locks.ReentrantLock;
// 1.创建资源类
class Ticket{
// 票数量
private int number = 100;
// 创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
// 买票方法
void sale() {
// 上锁
lock.lock();
try {
// 买票
if (number > 0) {
System.out.println(Thread.currentThread().getName() + ": 卖出! 余票: " + (--number));
}
} finally {
// 释放锁
lock.unlock();
}
}
}
public class SaleTicketByLock {
public static void main(String[] args) {
// 1.资源类与操作方法
Ticket ticket = new Ticket();
// 2.创建多个线程
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "窗口1").start();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "窗口2").start();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "窗口3").start();
}
}
关于Lock接口
由于Lock接口出现异常时不会释放锁,因此使用try-finally结构。
lock接口的等待-通知模式
-
synchronized。 使用wait()/notify() 实现线程的等待-通知
-
lock。使用Condition接口,返回一个condition实例,实现等待-通知。
2. 线程间通信
线程间通信模型:共享内存和消息传递
线程间的具体通信步骤:
- 创建资源类,定义属性和操作方法
- 资源类操作方法中,进行判断、操作、通知
- 创建多个资源,调用操作方法
- 防止虚拟唤醒问题
synchronized实现
this.wait(), this.notifyAll()
package JUC.Guigu.sync;
// 创建资源类
class Share {
// 定义初始值
private int number = 0;
// 操作方法
public synchronized void increase() throws InterruptedException {
// 不是0,等待
if (number != 0) {
this.wait();
}
// 是0,加1
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
this.notifyAll();
}
public synchronized void decrease() throws InterruptedException {
if (number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
this.notifyAll();
}
}
/**
* 两个线程,一个加1,一个减1.
*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 1.多个线程操作的对象及方法
Share share = new Share();
// 2.创建多个线程
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "a").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "b").start();
}
}
虚拟唤醒问题
主要把握:wait() 等待,被唤醒后,会直接往下执行。
唤醒后仍然需要进行条件判断,是否满足操作条件。
// 操作方法
public synchronized void increase() throws InterruptedException {
// 不是0,等待
while (number != 0) {
this.wait(); // 在哪里睡,在哪里醒。
// 如果用if判断,则唤醒后不会再进行条件判断,直接往下执行,导致错误。
// 所以需要用while
}
// 是0,加1
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
this.notifyAll();
}
reentrantlock实现
condition.await(), condition.signalAll()
package JUC.Guigu.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 资源类
class share{
// 定义锁
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 定义操作变量
private int number = 0;
// 操作方法
public void increase() throws InterruptedException {
// 上锁
lock.lock();
try {
while (number != 0) {
// 线程间通信方法
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrease() throws InterruptedException {
// 上锁
lock.lock();
try {
while (number != 1){
// 线程间通信方法
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
// 1.多线程操作实例对象及方法
share share = new share();
// 2.创建多个线程
new Thread(() -> {
try {
for (int i =0; i < 10; i++) {
share.increase();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "a").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
share.decrease();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "b").start();
new Thread(() -> {
try {
for (int i =0; i < 10; i++) {
share.increase();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "c").start();
new Thread(() -> {
try {
for (int i =0; i < 10; i++) {
share.decrease();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "d").start();
}
}
3. 定制化通信
使线程按定义的顺序工作
具体思路:
为每个线程定制标志位,为每个线程定制一个Condition对象。
AA打印5次,BB打印10次,CC打印15次。
为三个线程都安排一个condition对象,c1,c2,c3。按照次序实现精准唤醒。
【c1处理完,修改标志位,唤醒c2】 -->
【c2处理完,修改标志位,唤醒c3】 -->
【c3处理完,修改标志位,唤醒c1】
lock实现
package JUC.Guigu.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
// 资源类
class ShareResc{
private ReentrantLock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
// 线程标志位
private int flag = 0;
public void printMethod1(int times, int loop){
lock.lock();
try {
while(flag != 0){
try {
c1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < times; i++){
System.out.println(Thread.currentThread().getName() + " : " + i + " 轮次:" + loop);
}
flag = 1;
c2.signal(); // 精准唤醒
} finally {
lock.unlock();
}
}
public void printMethod2(int times, int loop){
lock.lock();
try {
while(flag != 1){
try {
c2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < times; i++){
System.out.println(Thread.currentThread().getName() + " : " + i + " 轮次:" + loop);
}
flag = 2;
c3.signal();
} finally {
lock.unlock();
}
}
public void printMethod3(int times, int loop){
lock.lock();
try {
while(flag != 2){
try {
c3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < times; i++){
System.out.println(Thread.currentThread().getName() + " : " + i + " 轮次:" + loop);
}
flag = 0;
c1.signal();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
ShareResc share = new ShareResc();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
share.printMethod1(5, i);
}
}, "AA").start();
new Thread(() -> {
for (int i =0; i < 10; i++) {
share.printMethod2(10, i);
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
share.printMethod3(15, i);
}
}, "CC").start();
}
}
sync实现
(不能称为定制化通信,无法实现精准唤醒)
package JUC.Guigu.sync;
// 共享资源类
class ShareResc{
private int num = 0;
// 同步代码块实现
private Object lock = new Object();
public void printMethod(int targetNum, int times, int loop) throws InterruptedException {
synchronized (lock) {
while (num % 3 != targetNum) {
lock.wait();
}
for (int i = 0; i < times; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i + " 轮次:" + loop);
}
num++;
lock.notifyAll();
}
}
// 同步方法实现
public synchronized void printMethod2(int targetNum, int times, int loop) throws InterruptedException {
while (num % 3 != targetNum) {
this.wait();
}
for (int i = 0; i < times; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i + " 轮次:" + loop);
}
num++;
this.notifyAll();
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// 多线程共同处理的资源类或方法
int loopTimes = 10;
ShareResc shareResc = new ShareResc();
// 创建多个线程
new Thread(() -> {
for (int i = 0; i < loopTimes; i++){
try {
shareResc.printMethod(0, 5, i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < loopTimes; i++){
try {
shareResc.printMethod(1, 10, i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < loopTimes; i++){
try {
shareResc.printMethod(2, 15, i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
}
}
4. 集合的线程安全问题
ArrayList的add()方法是线程不安全的。
多线程同时添加和获取元素时,会报出异常java.util.ConcurrentModificationException
解决方案
vector
Vector的add()方法 是线程安全的。add方法有synchronized修饰.
// 解决法1
List<String> list = new Vector<>();
此方法比较古老,不常用。
Collections
Collections类中很多方法是静态方法(锁时类的class对象锁,多线程在针对多个实例对象处理时,能保证线程安全)。
返回指定列表支持的同步(线程安全的)列表为synchronizedList(List <T> list)
// 解决方法2
List<String> list = Collections.synchronizedList(new ArrayList<>());
此方法比较古老,不常用。
CopyOnWriteArrayList
写时复制技术。
- 读:所线程并发读
- 写:独立写。
// 解决方法3
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList中的add方法
final transient ReentrantLock lock = new ReentrantLock();
...
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();
}
}
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
在需要对集合进行修改时,复制一份,修改后再替换原来的(array引用指向新数组)。
但也会因此引入 “弱一致性” 问题; 所谓 “弱一致性” 是指当一个线程正在读取数据时, 若此时有另一个线程同时在修改该区域的数据, 读取的线程将无法读取最新的数据, 即该读取线程只能读取到它读取时刻以前的最新数据;
“弱一致性” 的另一个体现是当使用迭代器的时候, 使用迭代器遍历集合时, 该迭代器只能遍历到创建该迭代器时的数据, 对于创建了迭代器后对集合进行的修改, 该迭代器无法感知; 这是因为创建迭代器时, 迭代器对原始数据创建了一份 “快照 (Snapshot)”; 因此
CopyOnWriteArrayList
和CopyOnWriteArraySet
只能适用于对数据实时性要求不高的场景;
CopyOnWriteArraySet
其具体实现是基于 CopyOnWriteArrayList
的, 其内部维护了一个 CopyOnWriteArrayList
实例 al
。
对set的操作都委托给了al。
public boolean add(E e) {
return al.addIfAbsent(e);
}
ConcurrentHashMap
5. 多线程锁
Class锁
和 对象锁
。
synchronized
修饰的非静态同步方法,锁的是当前的实例对象,多个对象的锁是不同的。
synchronized
修饰的 静态同步方法,锁的是当前类的Class对象,多个对象的锁是相同的。
同步方法块,锁的是同步Synchronized括号里配置的对象。
- 配置的为实例对象,锁的是实例对象。
- 配置的为static对象,锁的是Class对象。
公平锁和非公平锁
公平锁:排队,检查队列,效率相对低
非公平锁:效率高,但是线程容易饿死。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可重入锁
synchronized和lock都是可重入锁。(递归锁)
Synchronized(隐式):自动上锁和解锁。
Lock(显式):手动上锁和解锁。
由同一把锁修饰的方法,不需要等待解锁,就可再次调用加锁方法。可以嵌套获得锁(重入),解锁必须递归的解锁。
个人思考:
【在可重入锁修饰的同步方法中,调用自身方法或者其它由这把锁修饰的代码逻辑,仍然可以保证同步性】。
【在同一把锁修饰的所有方法中,只要一个线程抢到了该锁,该锁修饰的所有资源都为该线程所拥有,其它线程等待】
package JUC.Guigu.lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadDemo4 {
public static void main(String[] args) {
// 创建lock
ReentrantLock 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(); // 如果此处不释放锁,lock2将无法获得锁。
}
} finally {
lock.unlock();
}
}, "lock1").start();
new Thread(() -> {
try {
lock.lock();
System.out.println("lock2");
} finally {
lock.unlock();
}
}, "lock2").start();
}
}
死锁
两个或两个以上的线程因为争夺资源而造成互相等待资源的现象称为死锁。
package JUC.Guigu.sync;
import java.util.concurrent.TimeUnit;
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
new Thread(() -> {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "已获取o1, 等待o2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "已获取o2");
}
}
}, "Thread1").start();
new Thread(() -> {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "已获取o2, 等待o1");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "已获取o1");
}
}
}, "Thread1").start();
}
}
jps:类似linux中的ps -ef
jstack:jvm自带的堆栈跟踪工具
6. Callable接口
为了支持线程能够返回结果,Java中提供了Callable接口。
Runnable接口和Callable接口的区别 | Runnable | Callable |
---|---|---|
是否抛出异常 | 不抛出异常 | 抛出异常 |
是否有返回值 | 无返回值 | 有返回值 |
线程调用方法 | run() | call() |
由于Thread构造方法不支持Callable的实现,不能通过下面这种方式来实现Callable接口创建线程。
class MyThread2 implements Callable {
@Override
public Object call() throws Exception {
return 200;
}
}
// 错误写法,Thread构造方法中,没有Callable的实现
// 实现callable接口创建线程
new Thread(new MyThread2(), "BB").start();
FutureTask
解决方法:继承FutureTask类。
FutureTask来实现了Runnable接口和Callable接口。
// 解决方法
// 1.创建FutureTask对象
FutureTask futureTask1 = new FutureTask<>(new MyThread2());
// lambda接口,new 一个futuretask对象
// 1.创建FutureTask对象
FutureTask<Integer> futureTask2 = new FutureTask<>(() -> {
System.out.println("futuretask");
return 1024;
});
// 2. 创建一个线程
new Thread(futureTask2, "lucy").start();
// 4. 可调用FutureTask的 isDone() 方法获取是否处理完的状态
while(!futureTask2.isDone()) {
System.out.println("wait...");
}
// 3. 调用FutureTask的get方法获取结果
System.out.println(futureTask2.get());
System.out.println(Thread.currentThread().getName() + "over");
7. JUC辅助类
CountDownLatch:减一计数
CountDownLatch.countDown()
CountDownLatch.await()
CountDownLatch.getCount()
package JUC.Guigu.juc;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 新建一个CountDownLatch对象,设置初始值
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 号同学离开了!");
// 2. 计数器的值减一
countDownLatch.countDown();
// 4. 可以获取当前计数器的值
System.out.println(countDownLatch.getCount());
}, String.valueOf(i)).start();
}
// 3. 阻塞线程,直到计数器到0
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " 班长锁门走人了");
}
}
CyclicBarrier:循环阻塞
CyclicBarrier.await()
同步辅助类。允许一组线程互相等待,直到到达某一个公共屏障点。
循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
- A
CyclicBarrier
支持一个可选的Runnable
命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。在任何一方继续进行之前,此屏障操作对更新共享状态很有用。
package JUC.Guigu.juc;
import java.util.concurrent.CyclicBarrier;
public class CycliBarrierDemo {
private static final int NUMBER = 7;
public static void main(String[] args) {
// 1. 创建Barrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
// 3. 一个runnable接口的lambda实现。
System.out.println( " ********* 集齐7颗龙珠可以召唤神龙");
});
for (int i =1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 星龙珠被收集到来了");
// 2. 等待
cyclicBarrier.await();
} catch (Exception e){
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
Semaphore:计数信号量
Semaphore.acquire()
Semaphore.release()
一个计数信号量。在概念上,信号量维持一组许可证。如果有必要,每个
acquire()
都会阻塞,直到许可证可用,然后才能使用它。每个release()
添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象;Semaphore
只保留可用数量的计数,并相应地执行。信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
package JUC.Guigu.juc;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
// 1. 创建一组计数信号量
Semaphore semaphore = new Semaphore(6);
for (int i= 0; i < 10; i++) {
new Thread(() -> {
try {
// 2. 从信号量获得许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 抢到了车位");
// 4. 查看信号量中当前可用的许可数量
System.out.println(semaphore.availablePermits());
} catch (Exception e){
e.printStackTrace();
} finally {
// 3. 释放许可证,将其返回到信号量。
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
8. 读写锁
读锁 - 共享锁
写锁 - 排他锁
ReentrantReadWriteLock: 读写锁
ReentrantReadWriteLock.writeLock().lock()
ReentrantReadWriteLock.writeLock().unlock()
ReentrantReadWriteLock.readLock().lock()
ReentrantReadWriteLock.readLock().lock()
package JUC.Guigu.juc;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
// 资源类
class MyCache {
// 创建map集合
private volatile Map<String, Object> map = new HashMap<>();
// 1. 创建读写锁对象
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 存数据
public void put(String key, Object value) {
// 2. 获取写锁
rwLock.writeLock().lock();
// 暂停一会
try {
System.out.println(Thread.currentThread().getName() + " 正在写操作" + key);
TimeUnit.MICROSECONDS.sleep(300);
// 放数据
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 释放写锁
rwLock.writeLock().unlock();
}
}
// 取数据
public Object get(String key) {
// 4. 获取读锁
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName() + " 正在读取操作" + key);
TimeUnit.MICROSECONDS.sleep(300);
// 取数据
result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 取完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 5. 释放读锁
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 创建多个线程, 放数据
for (int i = 0; i <= 5; i++) {
final int num = 1;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
// 创建多个线程,取数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
锁降级
锁的演变:
- 无锁,多线程抢夺资源
- synchronized和Reentrantlock, 每次都是独占锁,每次只可以进行一个操作。不能共享。
- ReentrantReadWriteLock,读读可以共享,提升性能。但是不能多人写。缺点:造成锁饥饿。(一直读,不能写)
- 写锁降级为读锁(在等级上,写锁高于读锁)
将写锁降级为读锁
获取写锁 -> 获取读锁 -> 释放写锁 --> 释放读锁
9. 阻塞队列
- 队列为空时,获取元素的线程会被阻塞。
- 队列满时,添加元素的线程会被阻塞。
分类(BlockingQueue实现类)
- ArrayBlockingQueue
由数组结构组成的有界阻塞队列
- LinkedBoockingQueue
由链表结构组成的有界阻塞队列
package JUC.Guigu.queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// 创建阻塞队列
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
// 抛出异常
// System.out.println(blockingQueue.remove()); // 返回<E>, 队列为空,抛出异常
// System.out.println(blockingQueue.add("a")); // true
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.element());
// System.out.println(blockingQueue.add("c")); // 队列已满,抛出异常
//
// 特殊值
// System.out.println(blockingQueue.poll()); // 队列为空,返回false
// System.out.println(blockingQueue.offer("a")); // true
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("aaa")); // 队列已满,返回false
// 阻塞
// System.out.println(blockingQueue.take()); // 队列为空,被阻塞。
// blockingQueue.put("a");
// blockingQueue.put("b");
// blockingQueue.put("c");
// blockingQueue.put("aaa"); // 队列已满,被阻塞
// 超时
System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS)); // 阻塞时间2s
System.out.println(blockingQueue.offer("a")); // true
blockingQueue.offer("b");
blockingQueue.offer("c");
blockingQueue.stream().forEach(System.out::println);
blockingQueue.offer("w", 3L, TimeUnit.SECONDS); // 阻塞时间3s
}
}
10. 线程池
概述
数据库连接池:创建和管理一个数据库连接的缓冲池的技术,允许程序重复使用一个现有的数据库连接,而不是重新建立一个。(最小连接数,最大连接数;C3P0,DBCP,Druid)
线程池:Thread Pool
。线程过多会带来调度开销,影响缓存局部性和整体性能。线程池维护着多个线程,等待监督管理者分配可并发执行的任务。避免了频繁创建和销毁线程的代价。
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
- 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
架构
Java 中的线程池通过Executor
框架实现,该框架中用到了 Executor接口
,Executors类
,ExecutorService
,ThreadPoolExecutor
这几个类。
public class Executors {
/**
* 创建一个线程池,该线程池重用在共享无界队列上运行的固定数量的线程。
* 在任何时候,最多 nThreads 个 线程将是活动的处理任务。
* 如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待,直到有线程可用。
* 如果任何线程在关闭之前的执行过程中由于失败而终止,如果需要执行后续任务,新的线程将取代它。
* 池中的线程将一直存在,直到显式关闭。
*
* 参数:nThreads – 池中的线程数
* 返回值:新创建的线程池
* 抛异常:IllegalArgumentException – 如果 nThreads <= 0
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
}
public class ThreadPoolExecutor extends AbstractExecutorService {
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/**
* 分 3 个步骤进行:
*
* 1.如果运行的线程少于 corePoolSize,尝试使用给定命令启动一个新线程作为其第一个任务。
* 对 addWorker 的调用以原子方式检查 runState 和 workerCount,
* 因此通过返回 false 来防止在不应该添加线程时添加线程的错误警报。
*
* 2.如果一个任务可以成功排队,那么我们仍然需要仔细检查我们是否应该添加一个线程
*(因为现有的线程自上次检查后死亡)或者池在进入此方法后关闭。
* 因此,我们重新检查状态,如果有必要,如果停止排队,则回滚,
* 或者如果没有,则启动一个新线程。
*
* 3.如果我们不能排队任务,那么我们尝试添加一个新线程。
* 如果它失败了,我们知道我们已经关闭或饱和,因此拒绝任务。
*/
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
}
使用方式
package JUC.Guigu.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class ThreadPoolDemo1 {
public static void main(String[] args) {
// 1. 创建线程池
// ①多线程 线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// ②单线程 线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// ③可扩容线程池,根据需求创建线程
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService threadPool = cachedThreadPool;
try {
for (int i = 0; i < 10; i++) {
// 2.执行线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 3.关闭线程
threadPool.shutdown();
}
}
}
七个参数
corePoolSize
, 常驻线程数量(核心)maximumPoolSize
,最大线程数量keepAliveTime,TimeUnit unit
,线程存活时间BlockingQueue<Runnable> workQueue
,阻塞队列(排队的线程放入)。ThreadFactory threadFactory
,线程工厂,用于创建线程。RejectedExecutionHandler handler
,拒绝策略(线程满了)
public class ThreadPoolExecutor extends AbstractExecutorService {
/**
*使用给定的初始参数创建一个新的 ThreadPoolExecutor。
* 参数:
* corePoolSize - 保留在池中的线程数,即使它们是空闲的,除非设置了 allowCoreThreadTimeOut
* maximumPoolSize – 池中允许的最大线程数
* keepAliveTime – 当线程数大于核心时,这是多余的空闲线程在终止前等待新任务的最长时间。
* unit – keepAliveTime 参数的时间单位
* workQueue – 用于在执行任务之前保存任务的队列。此队列将仅保存由 execute 方法提交的 Runnable 任务。
* threadFactory – 执行器创建新线程时使用的工厂。
* handler – 由于达到线程边界和队列容量而阻塞执行时使用的处理程序。
* 抛出:
* IllegalArgumentException – 如果满足以下条件之一: corePoolSize < 0 keepAliveTime < 0 maximumPoolSize <= 0 maximumPoolSize < corePoolSize
* NullPointerException – 如果 workQueue 或 threadFactory 或 handler 为 null
*
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
}
底层工作流程
- 核心线程:核心线程数满了,后来的去排队。
- 阻塞队列:排队的位置也满来了,创建新线程。
- 阻绝策略:线程达到了最大数量(队列满了且线程最大了),拒绝策略。
拒绝策略
- AbortPoicy(默认):抛出异常。
- CallerRunsPolicy:将任务退回到调用者。
- DiscardOlderPolicy:丢弃队列中等待最久的任务。
- DiscardPolicy:丢弃无法处理额任务。
自定义线程池
public class ThreadPoolDemo2 {
public static void main(String[] args) {
// 1. 自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
2L, // 线程存活时间(超出核心线程数的线程最大空闲时间)
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(3), // 阻塞队列,队列容量
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略,(抛异常)
);
}
}
11. Fork 和 Join 框架
分支与合并
-
Fork,拆分:把大任务拆分成多个子任务进行并行处理。
-
Join,合并:把子任务的结果合并成最后的计算结果。
-
ForkJoinTask
:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接继承 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:RecursiveAction
:用于没有返回结果的任务RecursiveTask
:用于有返回结果的任务
-
ForkJoinPool
:ForkJoinTask 需要通过 ForkJoinPool 来执行 -
RecursiveTask
: 继承后可以实现递归(自己调自己)调用的任务
package JUC.Guigu.forkjoin;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
public class ForkJoinDemo1 {
public static void main(String[] args) throws Exception {
// 1.创建RecursiveTask对象
MyTask task = new MyTask(1, 100);
// 2.创建分支合并池ForkJoinPool对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(task);
// 3.获取最终合并后的结果
Integer result = forkJoinTask.get();
System.out.println(result);
// 4.关闭池对象
forkJoinPool.shutdown();
}
}
class MyTask extends RecursiveTask<Integer> {
private static final Integer VALUE = 10; // 拆分差值不能超过10
private int begin; // 拆分开始值
private int end; // 拆分结束值
private int result; // 返回结果
public MyTask (int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
// 判断差值
if ((end - begin) <= VALUE) {
// 相加操作
for (int i = begin; i <= end; i++){
result += i;
}
} else {
// 进一步拆分
int middle = (begin + end) / 2;
MyTask task1 = new MyTask(begin, middle);
MyTask task2 = new MyTask(middle + 1, end);
// 调用方法拆分
task1.fork();
task2.fork();
// 合并结果
result = task1.join() + task2.join();
}
return result;
}
}
12. 异步回调
CompletableFuture
在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。
CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类
- 没有返回值方法
runSync()
- 有返回值方法
supplyAsync()
package JUC.Guigu.completable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
// 异步调用 没有返回值 1. CompletableFuture.runAsync()
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "completableFuture1");
});
// 2. get()
completableFuture1.get();
// 异步调用 有返回值 1. CompletableFuture.supplyAsync()
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "completableFuture2");
int i = 1 / 0;
return 1024;
});
// 2. whenComplete()
completableFuture2.whenComplete((t, u) -> {
System.out.println("t:" + t); // 返回值
System.out.println("u:" + u); // 异常信息
});
}
}
Future与CompletableFuture
Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成。
Future的主要缺点:
(1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成
(2)不支持进一步的非阻塞调用
通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能
(3)不支持链式调用
对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
(4)不支持多个 Future 合并
比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理
Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的。