目录
1.2 synchronized的wait和notify,实现线程间协作
4. ThreadPoolExecutor创建自定义线程池:七大参数、四大策略
4.2 通过ThreadPoolExecutor创建线程池的方式:
4.4 拒绝策略实现接口:RejectedExecutionHandler
5. spring中的线程池类:ThreadPoolTaskExecutor
JUC是JAVA中util包下的一些类。主要是实现多线程和锁的。
一. JUC线程锁LOCKS包
LOCK接口有三个实现类:
- 公平锁:按照先来后到的原则依次执行线程。非公平锁则可以插队。
- 手动锁:需要手动创建LOCK的实现类,并手动执行.lock加锁,然后再try catch 执行业务代码,finally手动释放锁.unlock
1. 使用LOCK锁
方法锁,锁的是方法的调用者this;
对象锁,所得是对象;
类锁,静态方法锁,锁的是class类对象
public class TestSynchronized1 {
public static void main(String[] args) {
Stck stck = new Stck();
//创建三个线程同时操作一个对象执行卖票
new Thread(() -> {
for (int i = 0; i < 150; i++) {stck.sale();}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 150; i++) {stck.sale();}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 150; i++) {stck.sale();}
}, "C").start();
}
}
//模拟卖票过程
class Stck{
//一共有100张票
int num = 100;
//开始卖票方法
public void sale() {
Lock lock = new ReentrantLock();
lock.lock();
try {
if(num > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (num--) + "张票,还剩余:" + num + "张票");
}
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
1.1 lock与synchronized的区别
- synchronized是JAVA中的关键字,lock是一个java接口
- synchronized是自动加锁释放锁、lock需要手动加锁释放锁
- lock可以获取锁的状态
- synchronized线程阻塞时,其他线程会一直等待,lock可以设置超市等待后自动释放锁
- synchronized适合锁少量的代码同步问题,lock适合锁大量的同步锁
1.2 synchronized的wait和notify,实现线程间协作
synchronized在使用notify时,唤醒的是哪个wait状态的线程呢?
唤醒的是操作该同一个对象的某个线程,如果有多个线程在wait,则只会唤醒其中一个(随机一个线程,与等待的优先级有关)
synchronize的虚假唤醒
在使用wait和notify时,尽量将判断条件放在while循环里来执行等待和唤醒操作。if判断可能会存在虚假唤醒
1.3 JUC实现等待和唤醒,线程间协作
JUC中通过LOCK实现与synchronize一样的同步锁。
通过condition的实现类,实现wait和notify一样的功能:await()、signal()、signalAll()
* 需要注意的是:如果被唤醒的线程与执行唤醒的线程,不是同一把锁(lock对象)或者监视器(condition)对象。则不会执行协作。
因为每个condition1被await后,如果调用的是condition2.signal的话,是不会唤醒condition1的监视器的
public void printWait() throws InterruptedException {
lock.lock();
try {
condition.await();
System.out.println("我被唤醒了:" + Thread.currentThread().getName());
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printNotify() throws InterruptedException {
lock.lock();
try {
condition.signal();
System.out.println("我执行唤醒了:" + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
1.4 通过condition精准唤醒执行线程监视器
condition比自带得唤醒机制要灵活很多,每一个condition去监视一个线程
例:
执行三个线程,每个线程依次打印123。一致打印到90结束
package com.example.demo.JUC;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author hengtao.wu
* 执行三个线程,每个线程依次打印123。一致打印到90结束
* 思路:通过三个不同的condition控制三个线程,A执行点唤醒B,B唤醒C,C唤醒A,并设置判断条件的结束方式。
* @Date 2020/9/22 11:16
**/
public class TestThreadSignal {
public static void main(String[] args) throws InterruptedException {
Print print = new Print();
new Thread(() -> {print.printA();}, "线程A").start();
Thread.sleep(100);
new Thread(() -> {print.printB();}, "线程B").start();
Thread.sleep(100);
new Thread(() -> {print.printC();}, "线程C").start();
}
}
class Print {
private Lock lock = new ReentrantLock();
//每个监视器控制一个线程
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
private int num;
public void printA() {
lock.lock();
int i = 0;
try {
while (num < 90) {
if(i < 3) {
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
i++;
}else {
conditionB.signal();
conditionA.await();
i = 0;
}
}
conditionB.signal();
conditionC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
int i = 0;
try {
while (num < 90) {
if(i < 3) {
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
i++;
}else {
conditionC.signal();
conditionB.await();
i = 0;
}
}
conditionA.signal();
conditionC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
int i = 0;
try {
while (num < 90) {
if(i < 3) {
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
i++;
}else {
conditionA.signal();
conditionC.await();
i = 0;
}
}
conditionB.signal();
conditionA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
2. ReadWriteLock,读写锁
ReadWriteLock接口是JUC.locks包下的一个控制读写锁的接口,实现类只有一个:ReentrantReadWriteLock
作用:主要用来控制读写锁的。读的时候可以有多个线程同时读取,但是写操作只能有一个线程写入。经常使用与缓存当中。
原理:
①当某个线程在执行写入操作时,会获取到写入锁。此时除了线程本身外,其他线程的读写操作都会被进入队列等待不允许访问资源。直到写入锁释放。独占锁
②当某个线程准备执行写入操作时,需要等待当前所有的读取锁线程释放,才能获取写入锁。
③当前有读取线程时,再执行一个读取线程,可以获取读取锁。共享锁
参考博客:https://blog.youkuaiyun.com/yanyan19880509/article/details/52435135
public class ReentrantReadWriteLockTest {
public static void main(String[] args) {
TestMamch testMamch = new TestMamch();
for (int i = 0; i < 10; i++) {
final int temp = i;
new Thread(()->testMamch.get(temp+""), "线程" + temp).start();
}
for (int i = 0; i < 2; i++) {
final int temp = i;
new Thread(()->testMamch.set(temp+"", temp+""), "线程" + temp).start();
}
}
}
class TestMamch {
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Map<String, String> map = new HashMap<>();
public void set(String key, String value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始执行put" + key);
map.put(key, value);
System.out.println(key + "put完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(key + "开始读取");
map.get(key);
System.out.println(key + "读取完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
3. 可重入锁
可重入锁的理解:
在某个线程或者加了锁的方法中。如果该方法中还存在锁操作。例如A()中加了锁,调用B()也加了锁。此时对于A的锁来说,就是一把可重入锁,它会等B()的锁释放后释放A()的锁。
synchronized与Lock锁都是可重入锁
4. 自旋锁
SpinLock:
自旋锁在加锁时,就是只要不符合条件,就会一直循环,此时就是阻塞的。直到符合条件,获取到了锁,然后通过CAS设置原子引用。此时获取到了锁,其他线程获取锁时,就会一直自旋循环。直到锁被释放
自旋锁在释放时,将原子引用置空即可(符合while条件即可)。
自定义自旋锁:这样就能基于CAS实现与LOCK类一样的加锁功能了。
AtomicReference<Thread> reference = new AtomicReference<Thread>();
//加锁
public void Mylock() {
Thread thread = Thread.currentThread();
/*
加锁时,判断原子引用是否为空,如果是空的,则更新为当前值,相当于加了锁,
因为其他的线程就不能更新成功了,因为不是空的会一直再循环,知道原子引用为空。
*/
while (reference.compareAndSet(null, thread)) {
System.out.println("等待加锁");
}
System.out.println("加锁成功");
}
//加锁
public void MyUnlock() {
Thread thread = Thread.currentThread();
/*
释放锁时,将当前的原子引用设置为空,
就可以让其他线程在while循环中再次更新原子引用了
*/
reference.compareAndSet(thread, null);
System.out.println("解锁成功");
}
5. 死锁
查看死锁可以通过JDK自带的工具进行查询:
首先通过:jps -l 查询卡主了的class文件进程号
然后通过: jstack 进程号 查看该进程的堆栈信息
二. 队列
阻塞队列:BlockingQueue接口
非阻塞队列:AbstractQueue
双端队列:Deque接口
1. 阻塞队列BlockingQueue
阻塞队列:BlockingQueue实现类:ArrayBlockingQueue(数组实现)、LinkedTransferQueue(链表实现)、SynchronousQueue(同步队列)
本质都是collection接口的子接口BlockingQueue的实现类
阻塞队列:当队列已经存满后继续存入队列时、当队列为空,等待读取数据时。此时队列就是阻塞的。
1.1 ArrayBlockingQueue:API
方式 | 抛出异常 | 不抛出异常 | 阻塞,一直等待 | 阻塞,超时等待 |
添加 | add() | offer() | put() | offer(E,2,TimeUnit) |
移除 | remove() | poll() | take() | poll(2,TimeUnit) |
获取列首元素 | element() | peek() |
/**
* ①队列存满、空时读取会抛出异常
*/
public static void exceptionQueue() {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
System.out.println(queue.add(1)); //插入成功,返回true
System.out.println(queue.add(2));
System.out.println(queue.add(3));
System.out.println(queue.add(4)); //当队列已经存满时,直接抛出:IllegalStateException: Queue full
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove()); //当队列为空时,直接抛出:NoSuchElementException
}
/**
* ②队列存满、空时读取不会抛出异常,而是返回false/null
*/
public static void noExceptionQueue() {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
System.out.println(queue.offer(1)); //插入成功,返回true
System.out.println(queue.offer(2));
System.out.println(queue.offer(3));
System.out.println(queue.offer(4)); //当队列已经存满时,返回false
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll()); //当队列为空时,返回null
}
/**
* ③队列存满、空时读取线程一直阻塞
*/
public static void synQueue() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
queue.put(1); //无返回值
queue.put(2);
queue.put(3);
queue.put(4); //当队列存满时,线程会一直阻塞,知道队列存在空位置
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take()); //当队列为空时,会一直阻塞,知道队列有值可取
}
/**
* ④队列存满、空时读取线程,会超时阻塞
*/
public static void synTimeOutQueue() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//初始化队列长度为3
System.out.println(queue.offer(1)); //插入成功,返回true
System.out.println(queue.offer(2));
System.out.println(queue.offer(3));
System.out.println(queue.offer(4, 2, TimeUnit.SECONDS)); //当队列已经存满时,线程会超时等待阻塞,当超过两秒后无法插入,结束线程插入失败返回false
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll(2, TimeUnit.SECONDS)); //当队列为空时,线程等待2秒后无法获取,返回null,获取失败
}
1.2 SynchronousQueue同步队列
SynchronousQueue是同步队列,并且其中是不会存储元素的,只能一存一取。存入一个,必须要取出一个才可以继续存入。此时其他的存入线程就会阻塞,等待取出后被唤醒。
三. 线程与线程池
1. Callable
callbale是JUC包下的一个创建线程的接口,存在返回值,并且能够抛出异常。指定的业务是call方法。
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
1. new Thread(new runnable).start的参数是runnable的实现类。要想通过thread运行callable,就需要把callable转换为runnable的实现类
2. FutureTask实现了RunnableFuture,RunnableFuture继承自Runnable接口。相当于FutureTask也实现了Runnable接口。
3. FutureTask可以将callable做了参数构造。从而实现了,将callable转换为runnable的实现类的功能。
4. 直接将FutureTask传入thread,就可以运行线程了
5. FutureTask存放了线程执行的返回值
总结: 将callable通过构造创建FutureTask对象,FutureTask本质就是runnable的实现类
*/
TestThrea test = new TestThrea();
FutureTask task = new FutureTask(test);
new Thread(task,"线程A").start();
System.out.println(task.get());
}
}
class TestThrea implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + "线程被调用");
return "OK";
}
}
2. 线程池
线程池,通过池化技术,将线程实现初始化放在池子中。减少了线程的创建和销毁,提升了效率。并且能够完成线程的统一管理。
从而实现,线程可以服用,控制并发数,管理线程
常用的方式有:
①JUC下的Executors创建线程池(不推荐)
②ThreadPoolExecutor创建自定义线程池
③spring中的线程池类:ThreadPoolTaskExecutor
3. JUC下的Executors创建线程池 :三大方法
不推荐的用法,一共有三种创建线程池的方式。是一个创建线程池的工具类。底层还是通过new ThreadPoolExecutor()实现
例:newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
所以为了避免资源的浪费和资源耗尽的风险,需要通过ThreadPoolExecutor传入指定参数的方式来创建线程池:
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 20, 0L, TimeUnit.SECONDS, new SynchronousQueue<>());
核心线程数5,最大线程数20,超时时间0,单位是秒,创建一个阻塞队列。默认的factory是executors的默认工厂,以及默认拒绝策略
/*
创建只有一个线程的线程池
多线程情况下容易使请求阻塞队列长度一致扩大
源码中,创建该线程时,new的阻塞队列的长度为0x7fffffff,大约是21亿
*/
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
/*
创建固定5个线程的线程池
源码中,创建该线程时,new的阻塞队列的长度为0x7fffffff,大约是21亿
*/
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
/*
创建动态线程池,可随时扩大线程数,具体根据CPU的能力和业务逻辑量决定
容易造成线数量非常多的线程,甚至OOM
源码中,最大核心线程数为0x7fffffff,大约是21亿
*/
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); //
//执行
fixedThreadPool.execute(()-> System.out.println("线程执行中"));
fixedThreadPool.shutdown();
4. ThreadPoolExecutor创建自定义线程池:七大参数、四大策略
4.1 this方法,实例化线程池的七大参数源码:
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;
}
详解:
corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小 (当阻塞队列存满后,启动所有的最大线程池大小)
long keepAliveTime, //超时了没有调用会被释放, 超过改时间,线程没有使用就会被释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //存放阻塞队列对象(线程处理不过来的任务,存放在阻塞队列中。FIFO依次处理), BlockingQueue的实现类,当阻塞队列存满后,会调用最大核心线程池大小、拒绝策略处 理
ThreadFactory threadFactory, //线程工厂,创建线程用的,可以指定线程名称,默认是Executors.defaultThreadFactory()
RejectedExecutionHandler handler //拒绝策略,当线程数满了,并且阻塞队列也已经存满任务时,就需要执行拒绝策略决定如何处理
4.2 通过ThreadPoolExecutor创建线程池的方式:
可以通过自己自定义threadFactory的实现类,传入threadPoolExecutor构造方法实现自定义线程名称
//①:默认会传入Executors.defaultThreadFactory()创建工厂对象。 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 20, 0L, TimeUnit.SECONDS, new SynchronousQueue<>()); for (int i = 0; i < 20; i++) { poolExecutor.execute(() -> System.out.println(Thread.currentThread().getName() + "线程执行完成")); } //②:->改进为使用自定义ThreadFactory创建线程池,给线程指定名字 ThreadFactory factory = new ThreadFactoryBuild("test-pool-thread"); ThreadPoolExecutor poolExecutor1 = new ThreadPoolExecutor(5, 20, 0L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), factory); for (int i = 0; i < 20; i++) { poolExecutor1.execute(() -> System.out.println(Thread.currentThread().getName() + "线程执行完成")); } poolExecutor.shutdown(); poolExecutor1.shutdown(); //自定义实现ThreadFactory接口,手写规定线程名字以及规则 class ThreadFactoryBuild implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; private String nameFormat; ThreadFactoryBuild(String nameFormat) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = nameFormat + "-"; } @Override public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
4.3 理解触发拒绝策略
如下例:创建一个核心线程为5,最大线程为10,阻塞队列为9的线程池
同时创建20个线程,每个线程运行1秒。此时就会触发拒绝策略。
①5个线程开始执行,将15个其他线程放入阻塞队列(长度为9)
②当阻塞队列存满后,此时还有6个线程没有存,会触发最大核心线程数,即启动最大线程数10处理。在5的基础上又开了5个线程=10
③新来的五个线程处理这6个任务,还有一个任务无法处理,也就是:线程数 > 最大核心线程数+阻塞队列长度
此时拒绝策略生效:
Running, pool size = 10, active threads = 10, queued tasks = 9, completed tasks = 0。默认是抛出异常,可以自定义实现。
④如果将阻塞队列长度设置为10。则刚好可以处理这20个线程。
ThreadPoolExecutor poolExecutor1 = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(9));
for (int i = 0; i < 20; i++) {
poolExecutor1.execute(() -> {
System.out.println(Thread.currentThread().getName() + "线程执行完成");
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
poolExecutor1.shutdown();
4.4 拒绝策略实现接口:RejectedExecutionHandler
默认的四种拒绝策略:
AbortPolicy :默认的拒绝策略,不处理,并且抛出异常
CallerRunsPolicy :谁创建的线程,谁去处理。踢皮球踢回给了创建该线程者,一般是指main线程
DiscardOldestPolicy :尝试与最早的线程竞争,如果能够获取到线程数,则会执行,否则不处理,并且不抛出异常
DiscardPolicy:不处理任务,并且不抛出异常
5. spring中的线程池类:ThreadPoolTaskExecutor
在spring中使用线程池,一般通过ThreadPoolTaskExecutor来完成bean创建,从而使用线程池,代码如下:
/**
* 异步线程池的配置类
*/
@Configuration
@EnableAsync
public class ExecutorConfig1 {
// 核心线程数
private static final int CORE_POOL_SIZE = 5;
//最大核心线程数
private static final int MAX_POOL_SIZE = 20;
//线程空闲时间
private static final int KEEP_ALIVE_SECONDS = 60;
//队列长度
private static final int QUEUE_CAPACITY = 100;
//线程名称前缀
private static final String THREAD_NAME_PREFIX = "TEST-thread-";
@Bean(name = "asyncService")
public Executor asyncServiceExecutor() {
GwsLogger.info("异步 线程池启动成功!asyncServiceExecutor is start");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
executor.setQueueCapacity(QUEUE_CAPACITY);
executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
//设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}
}
6. 线程池自定义参数如何设置
最大线程池该如何设置:
CPU密集型:将最大的核心线程数设置为服务器的核数,可以通过
int maxThread = Runtime.getRuntime().availableProcessors();获取当前服务器的CPU核数
IO密集型:如果系统中需要处理大量的耗时IO操作,可以将最大线程数设置为大于该IO的任务数
三. JUC的集合类、辅助类
集合类:
①CopyOnWriteArrayList:
是一种线程安全的list集合,实现了list接口。叫做写入时复制集合。也就是说,在多线程下执行写入时,会先将list中的值复制一份出来,然后再修改后更新到list中去,保证不会覆盖别的线程修改的值。比vocket效率高,因为他是用LOCK锁实现的。
②CopyOnWriteArraySet
是一种线程安全的set集合,继承自AbstractSet类。同样是一种写入时复制的LOCK锁实现的安全的高效率的set集合。
③ConcurrentHashMap
是一个线程安全的map集合类,与HashMap一样,是Map接口的实现类。并且是通过synchronized修饰,实现线程安全。并且采用分段存储哈希表的方式,实现高效率。虽然使用了synchronized,但是比hashTable效率高很多。JDK8底层做了新改动。
辅助类:
①CountDownLatch : 减法计数器
可以用来作为计数器,每一个线程执行后,计数器减一,当所有线程执行完成后。也就是当CountDownLatch归零后,可以调用await方法执行归零后的逻辑。
public static void main(String[] args) throws InterruptedException { CountDownLatch count = new CountDownLatch(6); for (int i = 1; i < 7; i++) { new Thread(()-> { System.out.println(Thread.currentThread().getName() + "执行完成"); count.countDown(); //执行--操作 }, String.valueOf(i)).start(); } //此处会阻塞,直到count的值为0时,才会往下执行 count.await(); System.out.println("所有线程执行完毕"); }
②CyclicBarrier:加法计数器
通过加法,执行累加,加到规定值后,可以触发一个线程,执行相应的逻辑,例:所有线程执行完成后,打印执行完成
例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。
此例中,当所有线程后打印了执行完成后,才会执行await()后的操作。
详情可参考博客:https://blog.youkuaiyun.com/qq_39241239/article/details/87030142
public static void main(String[] args){ CyclicBarrier count = new CyclicBarrier(6, ()-> System.out.println("线程全部执行完毕")); for (int i = 1; i < 7; i++) { new Thread(()-> { System.out.println(Thread.currentThread().getName() + "执行完成"); try { count.await(); //执行++操作,并且让当前线程处于等待状态,当计数器达到了规定值后,才会唤醒执行往下的操作 System.out.println(Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } }
③Semaphore
是JUC下的一个控制计数容量的辅助类,意思是,这是一个容器开关,执行占用方法后+1,当存满指定值后就不可以再存放,需等待释放。执行完逻辑后,可释放-1。可以用在控制线程数场景
acquire():占用资源数+1
release():占用资源数-1
例:该网站只能同时允许6个用户访问,否则需要等待。限流
public static void main(String[] args){ //规定这个计数信号量,也就是说,同时这个计数器最大只能承载6个人。 Semaphore count = new Semaphore(6); for (int i = 1; i < 12; i++) { new Thread(()-> { try { count.acquire(); //表示,当前线程已经占据了一个访问位,如果当前count已经满了,则线程会阻塞等待释放位置后再执行 System.out.println(Thread.currentThread().getName() + "进行了访问"); TimeUnit.SECONDS.sleep(2); count.release(); //表示当前用户访问了2秒后,就离开了,同时释放这个访问位置 System.out.println(Thread.currentThread().getName() + "离开了访问"); } catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } }
四. 函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
有且只有一个方式,并且通过@FunctionalInterface注解修饰的接口,就叫做函数式接口
可以设置传参类型与返回类型。
可以使用lambda表达式编写
在JUF包下,since:JDK1.8
1. Function接口
功能型函数式接口:自定义参数类型,以及返回类型。
public static void main(String[] args) {
/**这就实现了一个function接口的内部类,并且重写了apply方法。参数类型是string,返回类型也是string,等价于:
*new Function<String, String>() {
* @Override
* public String apply(String str) {
* return str+str;
* }
* };
*/
Function<String, String> function = str -> str+str;
System.out.println(function.apply("123"));
}
2. Predicate 接口
predicate 接口也是JDK自带的函数式断定型接口,可以用来判断过滤等业务场景
参数类型是泛型,返回值为Boolean
Predicate<String> predicate = (str) -> {
return "123".equals(str);
};
System.out.println(predicate.test("1233"));
3. Consumer消费性接口
消费型接口:只有输入参数,没有返回值void
4. Supplier供给型接口
供给型接口:没有参数,只有返回值
5. 自定义函数式接口
符合只有一个接口方法,并且使用了@FunctionalInterface注解的接口,都可以作为函数式接口来使用。
6. stream流操作
流操作可以简化代码,并且提供了非常多的函数式接口API,提高了数据操作的效率,运行效率很高:
Stream、IntStream、LongStream等,操作数值效率都非常之高
public static void main(String[] args) {
User user1 = new User("小明", 13);
User user2 = new User("小红", 34);
User user3 = new User("小蓝", 24);
User user4 = new User("小白", 19);
User user5 = new User("小黑", 27);
List<User> list = Arrays.asList(user1, user2,user3, user4, user5);
//过滤年龄是20岁以上的,并且将名称都加个“子”,然后输出两个用户
System.out.println(list.stream()
.filter(u -> u.getAge() > 20)
.map(u -> u.getName() + "子")
.limit(2)
.collect(Collectors.toList()));
}
五. 扩展内容
1. forkjoin
forkjoin的含义是将任务拆分,然后合并,意思是对于耗时比较久的任务来说,可以拆分为多个子任务,然后子任务执行完成后,将结果合并后返回。利用多线的原理,实现任务的高效完成。
在大数据量时应用比较多,数据量小则不需要使用。通过双端队列实现。
- 使用
①forkjoin是JUC包下的一种多线执行任务的实现类。像线程池一样,通过ForkJoinPool池来执行ForkJoinTask任务:
ForkJoinPool.execute(ForkJoinTask task)
ForkJoinPool() 有三种构造方法 ,通常直接使用ForkJoinPool() pool = new ForkJoinPool()来创建pool
②创建ForkJoinTask
官网解释:在
ForkJoinPool
内运行的任务的抽象基类。 AForkJoinTask
是一个线程实体,其重量比普通线程轻得多。 大量任务和子任务可能由ForkJoinPool中的少量实际线程托管,价格为某些使用限制。也就是说,ForkJoinTask是pool执行任务的基类,我们自定义的forkTask 需要继承ForkJoinTask的子类,重写方法后,执行我们想要的分布计算。
demo: 计算1-10亿的和
当需要计算的个数是10000个时,就走循环,否则,执行递归拆分任务,最终返回结果。
public class MyForkJoinTask extends RecursiveTask<Long> {
private Long start = 0L;
private Long end = 10_0000_0000L;
private Long temp = 10000L;
private MyForkJoinTask(Long start, Long end) {
this.start = start;
this.end = end;
}
//重写计算方法,即通过pool执行的方法
@Override
protected Long compute() {
if((end - start) < temp) {
Long sum=0L;
for (Long i = start; i < end; i++) {
sum += i;
}
return sum;
}else {
Long middle = (end + start) / 2;
MyForkJoinTask task1 = new MyForkJoinTask(start, middle);
task1.fork(); //将该任务再次执行compute方法
MyForkJoinTask task2 = new MyForkJoinTask(middle+1, end);
task2.fork();
return task1.join() + task2.join(); //递归结束后获取返回结果
}
}
}
图解:
③:执行
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建pool池
ForkJoinPool pool = new ForkJoinPool();
//创建自定义任务
ForkJoinTask<Long> task = new MyForkJoinTask(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = pool.submit(task);//用返回值
Long sum = submit.get(); //该方式是阻塞的,会一直等待返回值返回
pool.execute(task); //无返回值
}
- 通过并行流实现: LongStream 这种方式是最高效的
System.out.println(LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum));
2. 异步回调
JUC包下提供了一个Future的接口,用来实现异步回调获取返回结果的
如果线程执行成功,获取成功返回结果,执行失败,获取失败返回结果
主要是以下实现类:
例:CompletableFuture的使用
无返回值的使用:
//无返回值的异步回调
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("线程执行成功");
});
future.get(); //此方法是阻塞的,会等待线程执行完成后执行。
有返回值的使用:
//有返回值的异步回调
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("线程正在执行中...");
int m = 10/0;
return 123;
});
Integer result = future.whenComplete((t, u) -> {
System.out.println("t=" + t); //T参数是正常执行成功后返回值
System.out.println("u=" + u); //U是执行失败后返回的异常信息
}).exceptionally((t) -> { //exceptionally是当执行失败时,也就是当线程被捕获到异常后
System.out.println("执行失败,失败返回信息为:" + t.getMessage());
return 500;
}).get();
System.out.println("执行结果是:" + result);
六. 原子性CAS理解
1. JMM
JAVA的内存模型,是一种概念,表示JAVA在运行时的内存模型。线程之间内存相互独立,互不可见。
8种操作:
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
2. volatile
是java中的关键字,为了让共享变量线程间互相可见实现同步性的轻量级操作
- 保证可见性:线程之间保证可见性,主内存的变量被更新后,线程中工作内存的值会被更新
- 不保证原子性:不保证数据安全,可以通过加锁,或者使用JUC下的原子类实现
- 避免指令重排:意思是我们编写的代码,计算机在执行的时候并不一定会按照我们写的逻辑去执行,而是根据性能优化方案重 排后执行,指令重排可能会导致程序异常执行。出现问题的概率几乎为0。但是通过volatile可以避免指令重排
2.1 原子类atomic
原子类是保证原子性的一些类。在JUC.atomic包下。底层是通过CAS实现的。比加锁的效率高非常多。
例如:int类型的变量,可以定义为:
AtomicInteger integer = new AtomicInteger();
integer.getAndIncrement();
就实现了int类型的变量实现原子性线程安全的++操作。
2.2 单例模式
所有的单例模式必须要保证无论如何,在程序中有且只能存在一个该类型的对象实例,一般都是通过构造方法私有化+锁实现单例
- 饿汉式单例
饿汉式单例模式,是在类加载的时候,就将该类完成实例化。并且不提供构造方法,只提供该对象实例的获取方法
但是如果该实例在实际中并没有使用,那么类加载就被创建,则会造成浪费内存空间。
public class HungryInstance {
private final static HungryInstance HUNGRYINSTANCE = new HungryInstance();
private HungryInstance() {}
public static HungryInstance getInstance() {
return HUNGRYINSTANCE;
}
}
- 懒汉式单例
懒汉式单例,类加载时并不会被创建,而是在真正被调用时,才会被创建。
但是要保证全局实例只有一个,就需要加锁
①. 将获取实例对象的方法加双重非空锁判断
public class LazyInstance {
private static LazyInstance lazyInstance;
private LazyInstance() {}
public static LazyInstance getInstance() {
if(null == lazyInstance) {
synchronized (LazyInstance.class) {
if(null == lazyInstance) {
lazyInstance = new LazyInstance();
}
}
}
return lazyInstance;
}
}
②. 通过volatile将实例对象防止指令重排
如果以上的方法发生了指令重排,即:
A线程通过getInstance方法获取到了实例对象。在还没有实例化前,CPU 先分配了内存空间,则此时该变量已经不是空的了,缺实际上没有实例化(没有执行到new)
B线程同步进来了,发现该变量已经不是null了,则直接返回,返回时A线程还没有NEW,则此时返回的实例对象就是个空实例对象
所以要通过volatile保证防止被指令重排。
public class LazyInstance {
private static volatile LazyInstance lazyInstance;
private LazyInstance() {}
public static LazyInstance getInstance() {
if(null == lazyInstance) {
synchronized (LazyInstance.class) {
if(null == lazyInstance) {
lazyInstance = new LazyInstance();
}
}
}
return lazyInstance;
}
}
③. 构造方法防止重复创建实例
但是,单例模式这样设计就真的安全了吗?如果通过反射,是可以创建不同的实例对象的
//反射获取实例化对象
LazyInstance instance1 = LazyInstance.getInstance();
Constructor<LazyInstance> constructor = LazyInstance.class.getDeclaredConstructor(null); //获取该class的无参构造器
constructor.setAccessible(true); //取消私有化的限制
LazyInstance instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
此时创建了两个不同的实例化对象。单例模式被破坏了。
此时可以通过在构造器中再次加锁,并且判断当前该class的私有化变量lazyInstance是否是空。来决定要不要new
如果该变量已经被new了,则会跑出异常。
private LazyInstance() {
synchronized (LazyInstance.class) {
if(null != lazyInstance) {
throw new RuntimeException("请不要通过反射创建单例模式对象");
}
}
}
④. 通过加密私有化变量防止反射创建实例
但是通过控制判断变量是否为空,单例模式就安全了吗?请看如下代码:
如果两个实例对象,都是通过反射获取,则不会走getInstance方法,就不会实例化变量lazyInstance
Constructor<LazyInstance> constructor = LazyInstance.class.getDeclaredConstructor(null); //获取该class的无参构造器
constructor.setAccessible(true); //取消私有化的限制
LazyInstance instance1 = constructor.newInstance();
LazyInstance instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
此时可以通过加密特殊字段来处理:instance字段可以设置为加密后的字段名。
这样就能保证:该class的构造器,在加锁的情况下,只能被执行一次,修改了instance的值,再次执行后,直接跑出异常。
private static boolean instance = false;
private static volatile LazyInstance lazyInstance;
private LazyInstance() {
synchronized (LazyInstance.class) {
if(!instance) {
instance = true;
}else {
throw new RuntimeException("请不要通过反射创建单例模式对象");
}
}
}
但是这样还是不能保证单例模式最终安全性,如下:
通过反射,执行一次初始化后,将该class的instance变量手动设置为false。 此时又破坏了单例
//通过反射获取属性instance
Field instance = LazyInstance.class.getDeclaredField("instance");
instance.setAccessible(true);
//反射获取实例化对象
Constructor<LazyInstance> constructor = LazyInstance.class.getDeclaredConstructor(null); //获取该class的无参构造器
constructor.setAccessible(true); //取消私有化的限制
LazyInstance instance1 = constructor.newInstance();
//执行完一次实例化后,该class中的instance为true,通过反射,手动将instance重新设置为false。又破坏了单例
instance.set(instance1, false);
LazyInstance instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
- 单例模式的安全性
JDK提供的枚举类,是安全的单例模式,不会因为通过反射创建对象时,导致多例产生。
如果是实现了序列化的单例模式,也可以通过反向破解序列化完成破解单例。
- 反编译工具:JDA.exe
3. CAS与atomic原子引用类包
CAS是java语言利用内存CPU的原子性操作保证同步性的一种算法:Compare and Swap(比较并替换),本质是通过CPU的同步性,保证在进行CAS时,指向的引用地址必须相同时,才会被更新。
大致可以理解为:在操作某个变量时,会将三个参数:该对象内存地址,当前对象的值,预更新的值。进行比较
如果在执行时,该CAS对象内存地址所对应的值与当前对象的值,一致,则将新值更新。如果不一致,则false不更新。
换句话说,如果在进行更新时,发现要更新的引用地址与原来的引用地址不对,则更新失败。
java用native修改的本地方法通过C++来完成操作内存,使同步效率变高。
常用类:unSafe类
ABA问题
ABA问题是指:CAS在更新数据时,是通过判断内存值与当前值是否一致,一致则更新
如果:A线程将a从1修改为了3,然后再从3修改成了1,此时B线程会认为a变量,没有修改过,从而进行了更新。
如果要避免这种情况发生的话,就需要将数据通过乐观锁的方式进行控制
JUC.atomic原子类包中有一个AtomicStampedReference类,就是用来原子操作时,控制乐观锁版本号的类
原理还是通过版本号来控制数据的更新的,
简单demo:
该类的本质是通过构造方法,初始化一个引用对象地址和版本号。
更新时,是更新了该对象了引用内存地址。
//构造方法, 传入引用和戳 public AtomicStampedReference(V initialRef, int initialStamp) //返回引用 public V getReference() //返回版本戳 public int getStamp() //如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存 public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) //如果当前引用 等于 预期引用, 将更新新的版本戳到内存 public boolean attemptStamp(V expectedReference, int newStamp) //设置当前引用的新引用和版本戳 public void set(V newReference, int newStamp)
public static void main(String[] args) {
Integer num = 1000;
//①定义原子引用变量reference,并指向引用num的内存地址,然后设置初始版本号为1
AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(num, 1);
//②将reference引用的内存地址更新为新的变量地址
Integer num2 = num + 1;
System.out.println(reference.compareAndSet(num, num2, reference.getStamp(), reference.getStamp() + 1));
System.out.println("新的stamp为:" + reference.getStamp());
//③获取当前reference的引用地址对象的值
System.out.println(reference.getReference());
//④判断当前num2是否是reference的当前引用地址,如果是则更新成功并且修改版本号,如果不是改地址,则失败false
System.out.println(reference.attemptStamp(num2, reference.getStamp()+1));
//⑤重新设置当前reference的引用对象,并设置版本号
reference.set(num2 + 2, 10);
System.out.println(reference.getReference());
//⑥测试版本号不一致时,是否更新成功
System.out.println(reference.compareAndSet(num2, num2 + 10, 5, reference.getStamp() + 1));
}
实战demo:
public class CASTest1 {
/*
AtomicStampedReference 是为了解决ABA问题,如果不考虑ABA问题可以直接使用AtomicReference
*/
public static void main(String[] args) throws InterruptedException {
MyUser u1 = new MyUser();
u1.setUserName("u1");
MyUser u2 = new MyUser();
u2.setUserName("u2");
AtomicStampedReference<MyUser> reference = new AtomicStampedReference<>(u1, 1);
int stamp = reference.getStamp();
//该线程已经将该reference的引用地址修改为了u2,然后又从u2修改为了u1,版本号已经被修改
new Thread(() -> {
System.out.println("线程开始工作");
if(reference.compareAndSet(u1, u2, reference.getStamp(), reference.getStamp() + 1)) {
System.out.println("线程已经将reference指向修改为:" + reference.getReference().getUserName() + ",此时的stamp是" + reference.getStamp());
}else {
System.out.println("线程第一次修改失败");
}
if(reference.compareAndSet(u2, u1, reference.getStamp(), reference.getStamp() + 1)) {
System.out.println("线程第二次已经将reference指向修改为:" + reference.getReference().getUserName() + ",此时的stamp是" + reference.getStamp());
}else {
System.out.println("线程第二次修改失败");
}
System.out.println("线程结束,当前线程指向对象是:" + reference.getReference().getUserName());
}).start();
TimeUnit.SECONDS.sleep(2);
//此时试图将reference的引用地址从u1修改为u2,stamp变量是线程前获取的,线程可能已经修改了
if(reference.compareAndSet(u1, u2, stamp, stamp+1)) {
System.out.println("主线程已经将reference指向修改为:" + reference.getReference().getUserName() + ",此时的stamp是" + reference.getStamp());
}else {
//结果是修改失败,因为此时虽然引用指向的地址是一致的,但是版本号不一致,所以更新失败了。解决了ABA问题
System.out.println("主线程修改失败");
}
}
}
@Data
class MyUser {
private String userName;
private Integer age;
}
运行结果: