文章目录
Stream流
Optional类
创建Optional类对象的方法:
Optional.of(T t)
创建一个Optional对象,t不可为空Optional.empty()
创建一个空的Optional对象Optional.ofNullable(T t)
创建一个Optional对象,t可为空判断Optional容器中是否包含对象:
boolean isPresent()
判断是否包含对象
void ifPresent(Consumer<? super T> con)
如果有值,将该值作为参数传给Consumer接口实现的代码获取Optional容器的数据对象:
T get()
若Optional对象包含值,返回值,否则抛异常
T orElse(T other)
若有值则返回值,否则返回other
T orElseGet(Supplier<? extends T> other)
若有值返回值,否则返回Supplier接口实现提供的对象
T orElseThrow(Supplier<? extends X> exception)
若有值返回值,否则抛出Supplier接口实现提供的异常
Java异常处理
多线程基础
线程与进程
线程的创建与使用
演示:
public class DrawCircle {
public static void main(String[] args) {
new Cat().start();//开始子线程
//若直接调用cat的run方法,就和调用普通方法一样,并未开启新线程
new Thread(new Dog()).start();
//main启动子线程,主线程不会阻塞,会继续执行
//主线程与子线程交替执行
Callable<Integer> c = new Callable() {//以匿名类的形式创建对象
@Override
public Integer call() throws Exception {
return 100;
}
};
FutureTask<Integer> task = new FutureTask(c);
new Thread(task).start();
}
}
class Cat extends Thread{
@Override
public void run() {
while (true) {
System.out.println("cat is running");
}
}
}
class Dog implements Runnable {//实现Runnable接口,建议使用
@Override
public void run() {
while (true) {
System.out.println("dog is running");
}
}
}
Thread类常用方法
setName(String name)
设置线程名称getName()
返回线程名称start()
线程执行(实际上是转为可运行状态),JVM底层调用start0()run()
调用线程对象的run方法setPriority(int newPriority)
设置线程优先级getPriority(int newPriority)
返回线程优先级sleep()
让当前线程休眠指定毫秒数interrupt()
中断线程,一般用于中断休眠线程yield()
线程礼让,让其他线程执行,礼让时间与成功不确定(运行->就绪)join()
线程插队,A线程中掉用B线程的join(),则表示A线程会等待B线程执行完后,A线程才会执行。join(long time)
线程插队,A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。
用户线程与守护线程
- 用户线程:工作线程,当线程的任务执行完或通知方式后结束
- 守护线程:为工作线程服务,当所有的用户线程结束,守护线程自动结束
- 常见守护线程:垃圾回收机制
- 设置线程为守护线程:
线程对象.setDaemon(true);
- 线程类型设置后方可启动线程为可运行状态
线程状态周期
线程同步机制
线程同步机制:
- 在多线程编程中,敏感数据不允许被多个线程同时访问。就需使用同步访问技术,保证数据在任何同一时刻,最多被一个线程访问,以确保数据完整
- 线程同步,即当一个线程对内存进行操作时,其他线程都不可对该内存地址进程操作,直至该线程完成操作
- Java中,引入对象互斥锁概念,来保证共享数据操作完整性
- 每个对象都对应一个可称为
互斥锁
的标记,该标记保证在任意同一时刻,只能有一个线程访问该对象synchronized
关键字与对象互斥锁联系,当某对象用该关键字修饰时,表明该对象在同一时刻只能被一个线程访问- 同步的局限性:程序的执行效率降低
- 对象互斥锁是非公平锁
同步代码块实现线程同步:
synchronized(同步锁对象){ // 需要同步操作的代码 }
同步方法实现线程同步:
public synchronized void m(String name){ //需要被同步的代码 }
注意:
- 对于非静态(普通成员)方法,同步锁就是this。
- 对于静态方法,我们使用当前方法所在类的字节码对象(类名.class)。
Lock锁对象实现线程同步:
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有!
Lock lock = new ReentrantLock(); lock.lock();// 上锁! //需要被同步的代码 lock.unlock();// 释放锁!
注意:Lock它是一个接口,我们使用的是它的子类ReentrantLock。
锁对象释放问题:
下列操作会释放锁(当前线程):
- 同步方法、同步代码块执行结束
- 同步方法、代码块中
break、return
- 同步方法、代码块出现未处理
Error/Exception
,异常结束- 同步方法、代码块中执行
wait()
,当前线程暂停下列操作不会释放锁(当前线程):
- 同步方法、代码块中执行
sleep()/yield()
暂停当前线程- 线程执行时,其他线程调用该线程
suspend()
,将其挂起
线程的死锁
==概述:==多个线程都占用对方的锁资源,但都不肯相让,导致死锁
演示:
@Override public void run() { synchronized (o1) {//A行线程获取o1对象锁 System.out.println(Thread.currentThread().getName() + "线程执行 任务1"); synchronized (o2) {//o2锁被B线程占用,A线程被阻塞 System.out.println(Thread.currentThread().getName() + "线程执行 任务2"); } } } @Override public void run() { synchronized (o2) {//B线程获取o2对象锁 System.out.println(Thread.currentThread().getName()+"线程执行任务3"); synchronized (o1) {//o1锁被A线程占用,B线程被阻塞 System.out.println(Thread.currentThread().getName()+"线程执行任务4"); } }//A、B线程占用对方需要的资源不肯互让,所以两个线程同时被阻塞,陷入死锁 }
线程池
==概述:==其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
原理:
- 线程池里面有很多线程对象,这些线程对象都是由线程池容器负责维护。
- 有很多任务(task)处于一个队列(Queue)状态。
- 线程池容器分配一个线程对象(thread),负责处理线程任务task,此时这个线程就处于被占用状态。有其它任务又进来了,线程池容器分配其它线程对象来处理。
- 一旦处理任务(task)的线程执行任务结束,它就空闲了(并不释放掉),又有新的任务抵达 ,那么它可能又会被线程池容器分配去处理任务。
- 重复利用线程池里面的线程来处理任务,没有将里面的线程对象销毁释放。
使用线程池的优点:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用:
//Java里线程池顶级接口java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService。 //java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工具类来创建线程池对象。 //Executors类中有个创建线程池的方法如下: public static ExecutorService newFixedThreadPool(int nThreads);// 返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量) public Future<?> submit(Runnable task) : // 获取线程池中的某一个线程对象,并执行。Future接口子类对象用来记录线程任务执行完毕后产生的结果。 <T> Future<T> submit(Callable<T> task) : // 获取线程池中的某一个线程对象,并执行。Future : 表示计算的结果. V get() : // 获取计算完成的结果。 /* *1. 创建线程池对象。 *2. 创建Runnable接口子类对象。(task) *3. 提交Runnable接口子类对象。(take task) *4. 关闭线程池(一般不做)。 */ ExecutorService service = Executors.newFixedThreadPool(3); Runnable r = new Runnable(){ @Override public void run() { //任务逻辑 } } service.submit(r);//submit方法结束后,程序并不终止,是因为线程池控制线程的关闭。 service.shutdown(); /* *1. 创建线程池对象。 *2. 创建Callable接口子类对象。(task) *3. 提交Callable接口子类对象。(take task) *4. 关闭线程池(一般不做)。 */ ExecutorService service = Executors.newFixedThreadPool(3); Callable<Integer> r = new Callable<Integer>(){ @Override public Integer call() { return 100; } } Future<Integer> ft = service.submit(r); Integer i = ft.get(); service.shutdown();
==自定义线程池:==自定义线程池(可以指定核心线程数量、最大线程数、空闲线程最大存活时间(数值)、任务队列数、任务拒绝的策略等)
/* 自定义线程池!【核心是构造方法的7个参数】 ExecutorService:默认的线程池 ThreadPoolExecutor: 自定义的线程池 构造方法:有7个参数 参数1:核心线程数量 参数2:最大线程数 参数3:空闲线程最大存活时间(数值) 参数4:时间的单位(java.util.concurrent包的下TimeUnit枚举) 参数5:任务队列(让任务在队列中等待,一旦线程有空闲,那么任务队列的任务就会被安排执行)【任务数量超过最大线程数的线程就会放到任务队列中】 参数6:创建线程的工厂(默认按照new Thread) 参数7:任务拒绝的策略 【AbortPolicy是ThreadPoolExecutor类的一个内部类】 * 何时拒绝?提交的任务数量超过 最大线程数和任务队列的总和,那些超过的部分的任务会被拒绝! * 如何拒绝?有四种拒绝策略(默认的策略 AbortPolicy) */ public class MyThreadPool { public static void main(String[] args) { // 创建自定义线程池对象! ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 5, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); // 提交线程任务 for (int i = 1; i <= 11; i++) { threadPoolExecutor.submit(() -> { System.out.println(Thread.currentThread().getName()); }); } threadPoolExecutor.shutdown(); } }
定时器的使用
概述:定时器Timer类,在java.util包下!线程调度任务以供将来在后台线程中执行的功能。任务可以安排一次执行,或者定期重复执行。
Timer timer = new Timer();//创建定时器对象 /* * Timer类中void schedule(TimerTask task, Date firstTime, long period) 从指定的时间开始,对指定的任务执行重复的固定延迟执行 。 * 参数1:定时器要执行的任务功能,传入TimerTask抽象类继承类的实例对象(重写run方法) * 参数2:延迟的时间 [毫秒值为单位] * 参数3:周期(每间隔多久重复执行一次task任务)[毫秒值为单位] */ timer.schedule(new TimerTask() { // 任务方法 @Override public void run() { //定时任务 } },3000,2000);
violate关键字
JVM多线程共享变量内存机制:
Java内存模型描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。多个线程同时对主内存的一个共享变量进行读取和修改时,首先会读取这个变量到自己的工作线程成为一个副本中,改动之后,再更新回主线程中变量内存。
多线程间共享变量可见性问题解决方案:
第一种方案:使用synchronized代码块包括访问共享变量的代码。
某一个线程进入synchronized代码块前后,执行过程入如下:
a.线程获得互斥锁。
b.清空工作内存。
c.从主内存拷贝共享变量最新的值到工作内存成为副本。
d.执行代码。
e.将修改后的副本的值刷新回主内存中。
f.线程释放锁。
随后,其他线程在进入synchronized代码块的时候,所读取到的工作内存上共享变量的值都是上一个线程修改后的最新值。
第二种方案:使用violate关键字修饰该共享变量。
violate关键字:
volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不同的线程都能及时的看到该变量的最新值。
关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。
volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
适用场景:
- 开关控制:利用可见性特点,控制某一段代码执行或者关闭。
- 多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读。
原子性
概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
例子:
public class VolatileAtomicThread implements Runnable { private int count = 0 ;// 定义一个int类型的遍历 @Override public void run() { for(int x = 0 ; x < 100 ; x++) {// 对该变量进行++操作,100次 count++ ; System.out.println("count =========>>>> " + count); } } } public class VolatileAtomicThreadDemo { public static void main(String[] args) { // 创建VolatileAtomicThread对象 VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ; for(int x = 0 ; x < 100 ; x++) {// 开启100个线程对count进行++操作 new Thread(volatileAtomicThread).start(); } }//最后输count的值出不一定是10000 } /* * count++操作包含3个步骤:【这3个操作若作为一个最小单元,那么程序就铁定出现10000】 * ①从主内存中读取数据到工作内存 * ②对工作内存中的数据进行++操作 * ③将工作内存中的数据写回到主内存 * * count++操作不是原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。 * 1)假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态 * 2)线程B也需要从主内存读取x变量的值,由于线程A没有对x值做任何修改因此B读取到的数据还是100 * 3)线程B工作内存中x执行了+1操作,但是未刷新之主内存中 * 4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作 * 5)线程B将101写入到主内存 * 6)线程A将101写入到主内存 * 虽然计算了2次,但是只对A进行了1次修改。 * * 在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。 * */ @Override public void run() { for(int x = 0 ; x < 100 ; x++) { synchronized(""){//使用锁机制解决问题 count++ ; System.out.println("count =========>>>> " + count); } } }
原子类
概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
AtomicInteger类(原子性Integer类)的使用:
public AtomicInteger():// 初始化一个默认值为0的原子型Integer [重要] public AtomicInteger(int initialValue):// 初始化一个指定值的原子型Intege int get(): // 获取值 int getAndIncrement(): // 以原子方式将当前值加1,注意,这里返回的是自增前的值。[重要] int incrementAndGet(): // 以原子方式将当前值加1,注意,这里返回的是自增后的值。 int addAndGet(int data): // 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。 int getAndSet(int value): // 以原子方式设置为newValue的值,并返回旧值。
对原子性章节代码进行优化:
public class VolatileAtomicThread implements Runnable { // 创建一个int类型对应的原子类(给定初始值为0) AtomicInteger i = new AtomicInteger(); @Override public void run() { for (int x = 0; x < 100; x++) { // 对该变量进行++操作,100次 // 先对原子类里的int类型的数值+1,后返回,+1动作是在一个原子内完成 int count = i.incrementAndGet(); System.out.println("count =========>>>> " + count); } } }
原子类CAS机制
概述:CAS的全成是: Compare And Swap(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-write转换为原子操作,这个原子操作直接由处理器保证。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B:
public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2);//获取对象当前值 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //CAS比较并交换,var1当前对象,var2当前值地址,var5当前值 //var5+var4预期修改后的值,compareAndSwapInt为native修饰的方法以确保原子性 return var5; } //若当前线程执行到7行被阻塞后重新执行,CAS机制会判断旧值var5与var2地址对应的数据进行比对。 //若不匹配值重新获取var5的值以确保var5是主存中最新的值,该过程称之为自旋。 //若匹配才将var5+var4的值替换到地址valueOffset中
乐观锁与悲观锁
悲观锁:
顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:
反之,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
并发集合
HashTable集合:
使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程A使用put进行元素添加,线程B不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
public synchronized V put(K key, V value) { ...} public synchronized V get(Object key) {...}
ConcurrentHashMap集合:
JDK1.7之前将大数组拆分成许多小数组,每个数组对应一把不同的锁,以此提高执行效率。
JDK1.8之后,进行了锁粒度的细化,又变成了一个大数组,假设两个线程同时对数组的第一个位置进行put操作,则会采取CAS策略,保证线程并发的安全性,同一时间只有一个线程成功执行CAS操作;在插入元素的过程中,会给插入的数组元素加上synchronized锁,再基于链表和红黑树插入数据,也就是数组的一个元素对应了一把锁**(CAS+局部(synchronized)锁定)**。
CopyOnWriteArrayList集合:
- 支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现,故CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景