1、什么是JUC
java.util工具包
2、线程和进程
进程(Process)
程序是指令和数据的有序集合,本身没有运行的含义,是一个静态的概念。
进程是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。
线程(Thread)
通常在一个进程中可以包括若干个线程,一个进程中至少有一个线程,不然没有存在的意义,线程是CPU调度和执行的单位
真正的多线程是指有多个CPU,即多核
线程就是独立的执行路径
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
java开不了线程,只能调用本地方法(c++)来开启线程
private native void start0();
并发和并行
并发(多线程操作同一个资源)
- 单核cpu,多个线程快速交替
并行(多个线程一起运行)
- 多核cpu,多个线程可以同时执行
//获取cpu核数
System.out.println(Runtime.getRuntime().availableProcessors());
并发编程的本质:充分利用CPU的资源
线程状态
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待(无限制时间)
WAITING,
// 超时等待(超过限制时间就不等待)
TIMED_WAITING,
// 终止
TERMINATED;
}
wait和sleep区别
1、来自不同的类
wait->Object
sleep->Thread
2、关于锁的释放
wait会释放锁
sleep不会释放锁
3、使用的范围不同
wait必须在同步代码块中
sleep可以在任何地方
3、Lock锁
传统
synchronized
锁方法,锁资源
class Ticket{
private int number = 30;
public synchronized void sale(){
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
}
}
JUC中的lock接口
公平锁:先来后到
非公平锁:可以插队
//lock
// 1.new ReentrantLock();
// 2.lock.lock();//加锁
// 3。lock.unlock();//解锁
class Ticket{
private int number = 30;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock();//加锁
try {
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();//解锁
}
}
}
synchronized和lock区别
1、synchronized是内置的java关键字,lock是一个java类
2、synchronized无法判断获取锁的状态,lock可以判断是否获取到了锁
3、synchronized会自动释放锁,lock锁必须要手动释放锁,如果不释放锁,会死锁
4、synchronized会一直等待下去,lock不一定会等待下去
5、synchronized 可重入锁,不可以中断,非公平;
lock 可重入锁,可以判断,自定义
6、synchronized 适合锁少量的代码同步问题,lock适合锁大量的同步代码
4、生产者和消费者问题
使用lock实现
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.decrement();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.decrement();
}
},"D").start();
}
}
class Data2{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment(){
lock.lock();
try {
while (number!=0){
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//唤醒其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decrement(){
lock.lock();
try {
while (number==0){
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//唤醒其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
结果:
但出现了随机的状态
使用Condition精准唤醒
// A执行完调用B,B执行完调用C,C执行完调用A
public class C {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printC();
}
},"C").start();
}
}
class Data3{
private int number = 1;//1A,2B,3C
private Lock lock=new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void printA(){
lock.lock();
try {
while (number !=1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAA");
// 唤醒指定的人
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (number !=2){
//等待
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBB");
// 唤醒指定的人
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (number !=3){
//等待
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>CCCCCCC");
// 唤醒指定的人
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
结果
A、B、C顺序执行
5、8锁现象
要明白synchronized锁的是什么?
// 静态的同步方法 锁的是class类模板
public static synchronized void sendSms(){
}
// 普通的同步方法 锁的是调用者
public synchronized void call(){
}
6、集合类不安全
List不安全
// java.util.ConcurrentModificationException:并发修改异常
public static void main(String[] args) {
// 并发下的ArrayList不安全
/*
解决方案:
1、List<String> list = new Vector<>(); 使用古老类Vector
2、List<String> list = Collections.synchronizedList(new ArrayList<>()); 使用Collections工具类
3、List<String> list = new CopyOnWriteArrayList<>();
*/
// CopyOnWrite 写入时复制
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
CopyOnWrite比Vector效率高
CopyOnWrite使用的lock锁,Vector使用的是synchronized
HashSet不安全
跟上面List同理,new一个CopyOnWriteArraySet解决并发问题
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet();
for (int i = 1; i <= 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
HashSet底层:就是使用了map
public HashSet() {
map = new HashMap<>();
}
// add:本质就是map,key无法重复
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object(); // 不变的值
HashMap不安全
public static void main(String[] args) {
// new HashMap<>()等价于 new HashMap<>(16,0.75)
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 1; i <= 30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
7、Callable
1、可以有返回值
2、可以抛出异常
3、方法不同 run() / call()
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread Thread = new MyThread();
FutureTask futureTask = new FutureTask(Thread);//适配类
new Thread(futureTask,"A").start();
System.out.println(futureTask.get());// 获取callable的返回结果
}
}
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("call()");
return 1024;
}
}
细节:
1、有缓存
2、get()返回结果可能需要等待,会阻塞
8、常用的辅助类
8.1、CountDownLatch(减法)
- A
CountDownLatch
是一种通用的同步工具,可用于多种用途。一个CountDownLatch
为一个计数的CountDownLatch用作一个简单的开/关锁存器,或者门:所有线程调用await
在门口等待,直到被调用countDown()
的线程打开。一个CountDownLatch
初始化N可以用来做一个线程等待,直到N个线程完成某项操作,或某些动作已经完成N次。
public static void main(String[] args) throws InterruptedException {
//总数是6
CountDownLatch latch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go");
latch.countDown(); // 数量-1
},String.valueOf(i)).start();
}
latch.await(); // 等待计数器归零,再向下执行
System.out.println("结束");
}
latch.countDown(); // 数量-1
latch.await(); // 等待计数器归零,再向下执行
8.2、CyclicBarrier(加法)
- 允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("到达了上限");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(temp);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
CyclicBarrier会记录开启的线程,就像上面代码,每开启7个线程就会执行CyclicBarrier初始化中定义的方法
在lambda表达式中使用的变量要为final修饰的变量
8.3、Semaphore(信号量)
Semaphore:信号量
- 信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
public static void main(String[] args) {
// 线程数量:限流3个
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
// acquire():得到
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"来了");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// release():释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
semaphore.acquire():获得,假如满了,等待
semaphore.release():释放,然后唤醒等待的线程
9、读写锁
ReadWriteLock:读的时候可以被多个线程同时读,写的时候只能有一个线程去写
// 独占锁(写锁):一次只能被一个线程占用
// 共享锁(读锁):多个线程可以同时占用
// ReadWriteLock
// 只有读和读的线程可以共存
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写入
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
//自定义缓存
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
// 读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//写,只有一个线程能写
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//读
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
readWriteLock里有writeLock()和readLock()两种方法,一个是写锁,一个是读锁。
10、阻塞队列
在队列满的时候写会阻塞,在队列空的时候取会阻塞
在多线程并发处理和线程池中会使用阻塞队列
BlockingQueue的四组API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(,) |
移除 | remove | poll | take | poll(,) |
检测队列首 | element | peek | - | - |
抛出异常
//抛出异常
public static void test1(){
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// Queue full:队列已满,抛出异常
//System.out.println(blockingQueue.add("d"));
System.out.println(blockingQueue.element());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//NoSuchElementException:队列中没有元素了
//System.out.println(blockingQueue.remove());
}
不抛出异常,有返回值
//不抛出异常,有返回值
public static void test2(){
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
//System.out.println(blockingQueue.offer("d"));//false,不抛出异常
System.out.println(blockingQueue.peek());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//System.out.println(blockingQueue.poll());//返回值为null,不抛出异常
}
等待,阻塞(一直阻塞)
// 等待,阻塞(一直阻塞)
public static void test3() throws InterruptedException {
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//blockingQueue.put("d");// 队列没有位置了,一直阻塞
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//System.out.println(blockingQueue.take());//没用元素了,一直阻塞
}
等待,阻塞(等待超时)
// 等待,阻塞(等待超时)
public static void test4() throws InterruptedException {
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
blockingQueue.offer("d",2, TimeUnit.SECONDS);//等待,超时2s就退出
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
}
同步队列
SynchronousQueue:其中每个插入操作必须等待另一个线程相应的删除操作,反之亦然。同步队列没有任何内部容量,甚至没有一个容量。 你不能peek
在同步队列,因为一个元素,当您尝试删除它才存在; 您无法插入元素(使用任何方法),除非另有线程正在尝试删除它; 你不能迭代,因为没有什么可以迭代。 队列的头部是第一个排队的插入线程尝试添加到队列中的元素; 如果没有这样排队的线程,那么没有元素可用于删除,并且poll()
将返回null
。 为了其他Collection
方法(例如contains
)的目的, SynchronousQueue
充当空集合。 此队列不允许null
元素。
11、线程池
线程池:三大方法、7大参数、4种拒绝策略
池化技术
程序的运行要占用系统的资源,为了优化资源的使用,就有了池化技术
池化技术:事先准备好一些资源,需要时就使用,使用完就放回池子
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理
线程复用、可以控制最大并发数、管理线程
三大方法
//Executors 工具类 3大方法
public class Demo01 {
public static void main(String[] args) {
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
//ExecutorService threadPool= Executors.newFixedThreadPool(5);// 创建一个固定的线程池大小
ExecutorService threadPool = Executors.newCachedThreadPool();// 可伸缩的
try {
for (int i = 0; i < 100; i++) {
// 使用了线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
7大参数
首先分析上面三大方法的源码,发现都是new了一个同样的对象ThreadPoolExecutor
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数一:int corePoolSize:核心线程池大小
参数二: int maximumPoolSize:最大核心线程池大小
参数三: long keepAliveTime:超时了没有人调用就会释放
参数四:TimeUnit unit:超时单位
参数五: BlockingQueue workQueue:阻塞队列
参数六:ThreadFactory threadFactory:线程工厂
参数七:RejectedExecutionHandler handler:拒绝策略
4种拒绝策略
/**
* new ThreadPoolExecutor.AbortPolicy() // 满了还有线程进来抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() // 让主线程调用
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会
抛出异常!
*/
手动创建线程池
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
try {
// 最大承载:deque+max
for (int i = 0; i < 9; i++) {
// 使用了线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
池的大小设置
了解IO密集型和CPU密集型
对于CPU密集型
- 一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。
- 但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。
- 因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。
对于IO密集型
- 如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比方说多线程网络传输,多线程往不同的目录写文件,等等。
- 所以通常就需要开cpu核数的两倍的线程, 当线程进行I/O操作cpu空暇时启用其他线程继续使用cpu,提高cpu使用率
总结:
CPU 密集型,几核,就是几,可以保持CPu的效率最高!
IO 密集型 一般线程数需要设置2倍CPU数以上,
public static void main(String[] args) {
//最大线程该如何定义
// 1、cpu密集型,看电脑是几核的
// 2、IO密集型
// 程序 15个大型任务
// 获取电脑的线程
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
try {
// 最大承载:deque+max
for (int i = 0; i < 9; i++) {
// 使用了线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
12、四大函数式接口
必会的:lambda表达式、链式编程、函数式接口、Stream流式计算
Function函数式接口
只有一个方法的接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
// function 函数式接口
// 只要是函数式接口 可以用lambda表达式简化
public static void main(String[] args) {
// Function<String, String> function = new Function<String, String>() {
// @Override
// public String apply(String s) {
// return s;
// }
// };
Function<String, String> function = (s)->{return s;};
System.out.println(function.apply("fdgs"));
}
Predicate断定型接口
有一个输入参数,返回值只能是 布尔值!
// 断定型接口:有一个输入参数,返回值是布尔值
public static void main(String[] args) {
// Predicate<String> predicate = new Predicate<String>() {
// @Override
// public boolean test(String s) {
// return s.isEmpty();
// }
// };
Predicate<String> predicate = (s)->{return s.isEmpty();};
System.out.println(predicate.test(""));
}
Consumer 消费型接口
只有输入,没有返回值
//只有输入,没有返回值
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String s) {
// System.out.println(s);
// }
// };
Consumer<String> consumer = (s)->{ System.out.println(s); };
consumer.accept("aa");
}
Supplier 供给型接口
没有参数,只有返回值
//没有参数,只有返回值
public static void main(String[] args) {
// Supplier<Integer> supplier = new Supplier<Integer>() {
// @Override
// public Integer get() {
// return 1024;
// }
// };
Supplier<Integer> supplier = ()->{return 1024;};
System.out.println(supplier.get());
}
13、Stream流式计算
什么是流?
流是Java8引入的全新概念,它用来处理集合中的数据,暂且可以把它理解为一种高级集合。
流的特点:
- 只能遍历一次
- 采用内部迭代方式
什么是Stream流式计算?
Stream(流)是一个来自数据源的元素队列并支持聚合操作
Stream流式计算的一些操作
filter
filter方法设置的条件过滤出元素,里面的参数是Predicate断定型接口
map
map方法用于映射每个元素的对应结果,里面的参数是Function函数式接口
sorted
sorted方法用于对流进行排序,里面可传入comparator比较器参数
limit
limit 方法用于获取指定数量的流
// 一行代码完成以下条件
// id为偶数
// 年龄大于23
// 用户名转为大写
// 用户名倒着排序
// 只输出一个用户
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
//计算交给stream流
// lambda表达式、链式编程、函数式接口、Stream流式计算
// 处理完后
list.stream()
.filter(u->{return u.getId()%2==0;})
.filter(u->{return u.getAge()>23;})
.map(u->{return u.getName().toUpperCase();})
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})
.limit(1)
.forEach(System.out::println);
}
14、ForkJoin
什么是ForkJoin?
并行执行任务,采用分治,分治分为两个阶段
第一个阶段分解任务,把任务分解为一个个小任务直至小任务可以简单的计算返回结果。
第二阶段合并结果,把每个小任务的结果合并返回得到最终结果。而Fork
就是分解任务,Join
就是合并结果。
ForkJoin特点:工作窃取
采用双端队列,因为ForkJoinPool
有一个机制,当某个工作线程对应消费的任务队列空闲的时候它会去别的忙的任务队列的尾部分担(stealing)任务过来执行
ForkJoin操作
**注意:**当有两个任务都fork的情况下,必须按照f1.fork(),f2.fork(), f2.join(),f1.join()
这样的顺序,不然有性能问题。
public class ForkJoinDemo extends RecursiveTask<Long> {
private long start;
private long end;
//临界值
private long temp = 10000L;
public ForkJoinDemo(long start, long end) {
this.start = start;
this.end = end;
}
//计算方法
@Override
protected Long compute() {
if ((end-start)<temp){
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else {
long mid = (start + end)/2;
ForkJoinDemo task1 = new ForkJoinDemo(start, mid);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(mid+1,end);
task2.fork(); // 拆分任务,把任务压入线程队列
return task2.join()+task1.join();
}
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();// 301
test2();// 184
test3();// 169
}
//普通的
public static void test1(){
long sum = 0L;
long start = System.currentTimeMillis();
for (long i = 1L; i <= 10_0000_0000; i++) {
sum+=i;
}
long end = System.currentTimeMillis();
System.out.println("sum="+sum+"时间: "+(end-start));
}
// forkjoin
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum="+sum+"时间: "+(end-start));
}
public static void test3(){
long start = System.currentTimeMillis();
// Stream并行流 ()
long sum = LongStream.rangeClosed(0L,
10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum="+sum+"时间: "+(end-start));
}
15、异步回调(Todo)
16、JMM
JMM:java内存模型,是一个概念,不存在的东西
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机 遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变 量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中, 以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内 存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须 write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量 实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解 锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前, 必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
17、Volatile
Volatile是java虚拟机提供的轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
当一个变量被 volatile
修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
volatile
修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。
1、保证可见性
// 不加volatile,程序就会死循环
// 加了volatile,可以保证可见性
private volatile static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
2、不保证原子性
原子性 : 不可分割
线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败
// volatile不保证原子性
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
如果不用volatile和synchronized ,怎么样保证原子性
使用原子类来解决原子性问题,底层使用了native,调用了c的方法
// volatile不保证原子性
// 原子类的Integer
private static AtomicInteger num = new AtomicInteger();
public static void add(){
//num++;
num.getAndIncrement();//AtomicInteger+1方法
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
这些类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!
3、禁止指令重排
指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
Volatile 是可以保持 可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
18、单例模式
饿汉式
//饿汉式单例
public class Hungry {
//可能会浪费空间
private byte[] data1= new byte[1024*1024];
private byte[] data2= new byte[1024*1024];
private byte[] data3= new byte[1024*1024];
private byte[] data4= new byte[1024*1024];
private Hungry(){
}
private final static Hungry HUNGRY=new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
DCL懒汉式:Double Check Lock
//懒汉式单例
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "ok");
}
private volatile static LazyMan lazyMan;//volatile:防止指令重排
//双重检测锁模式
public static LazyMan getInstance(){
if (lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
// 利用反射破坏单例
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazyMan instance1 = getInstance();
// 利用发射获取对象的无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//关闭私有权限
declaredConstructor.setAccessible(true);
// 创建对象
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
19、CAS
概念
CAS(Compare-And-Swap),它是一条CPU并发原语,用于判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
// CAS compareAndSet :比较并交换
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//public final boolean compareAndSet(int expect, int update)
// 如果期望的值达到了,那么就更新,否则不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
结果:
点开源码发现调用了unsafe类里的方法,再点开发现是native修饰的方法,调用了c++的方法
ABA问题
CAS会导致ABA问题
CAS算法实现一个重要的前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化.
比如说一个线程one从内存位置V中取出A,线程Two也执行,将A–>B–>A,这时one进行CAS操作发现内存仍是A,然后one操作成功.
one操作成功,但是在这个过程中线程two可能操作了其它数据,产生问题.
20、原子引用
在操作过程中加入版本号来避免ABA问题
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
// 正常在业务操作,这里面比较的都是一个个对象
static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1,1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println("a2=>"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("a3=>"+atomicStampedReference.getStamp());
},"a").start();
// 乐观锁的原理相同!
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("b2=>"+atomicStampedReference.getStamp());
},"b").start();
}
结果
21、不同的锁
1、公平锁、非公平锁
公平锁:先来后到,不能插队
非公平锁:可以插队(默认都是非公平的)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2、可重入锁
可重入锁(递归锁)
同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁,这就是可重入锁
java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的
可重入锁最大的作用是避免死锁
3、自旋锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
比如unsafe类中的方法
4、死锁
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
产生死锁的必要条件:
1、互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2、请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4、环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
预防死锁
破环必要条件中的任何一个就行了
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏互斥条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请求和保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)