并发
-
进程和线程
- 进程:资源分配的基本单位。可以理解为在内存中运行的程序。每个进程都有独立的内存空间,一个进程包含多个线程
- 线程:任务执行的基本单位。负责进程中任务的执行。每个线程共享进程的内存空间,一个线程使用时,其他线程必须等待。
- 用户 (User) 线程:
运行在前台,执行具体的任务
,如程序的主线程、连接网络的子线程等都是用户线程。 - 守护 (Daemon) 线程:
运行在后台,为其他前台线程服务
。也可以说守护线程是 JVM 中用户线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。守护线程拥有自动结束自己生命周期的特性。如垃圾回收线程,程序或JVM退出时,垃圾回收线程能够自动关闭。
-
线程实现方式
-
继承Thread类
- 继承Thread类
- 重写核心执行逻辑run方法
- 调用start方法开启线程。调用run方法只是一个普通方法
-
实现Runnable接口【因为Java是单继承的,所以一般选择Runnable创建线程】
- 实现Runnable接口
- 重写run方法
- 创建Thread对象,构造参数为当前Runnable实现类的对象
- 调用start开启线程
-
实现Callable接口【需要返回值时,选择Callable创建线程】
- 实现Callable接口
- 重写call()方法,有返回值,返回类型就是Callable接口中泛型对应的类型
- 创建FutureTask对象,构造参数为当前Callable接口的实现类对象
- 将FutureTask作为参数创建Thread对象
- 调用start开启线程
- futureTask.get获取call方法返回值
-
线程池
-
为什么使用线程池
- 使用线程池可以复用池中的线程,不需要每次都创建新线程,减少创建和销毁线程的开销;
- 同时,线程池具有队列缓冲策略、拒绝机制,并能动态管理线程个数,特定的线程池还具有定时执行、周期执行功能
-
Executors类创建线程池【不用】
-
FixedThreadPool(固定线程数量)和SingleThreadPool(单例线程池):
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
-
CachedThreadPool(缓存线程池)和ScheduledThreadPool(定时周期性线程池):
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
-
-
ThreadPoolExecutor创建线程池
-
主要参数
- corePoolSize(int):核心线程数量
- workQueue:阻塞队列,存放等待执行的任务,使用时指定大小
- maximumPoolSize(int):线程池内的最大线程数量
- keepAliveTime(long):线程存活时间
- TimeUnit unit:线程存活时间的单位
- [RejectedExecutionHandlerxedThreadPool(固定线程数量)和Sigln./xiewenfeng520/article/details/106954167/):拒绝策略,当任务队列存满并且线程池个数达到maximunPoolSize后采取的策略
- threadFactory:创建线程的工厂,可以重写newThread方法自定义线程工厂
-
- ArrayBlockingQueue:数组结构的有界阻塞队列
- LinkedBlockingQueue:链表结构的有界阻塞队列,默认大小为Integer.MAX_VALUE
- SynchronousQueue:单个元素阻塞队列,最多只存储一个元素
阻塞队列核心方法
方法 抛出异常 返回true/false 发生阻塞 设置时间 插入 add(e) offer(e) put(e) offer(e,time,unit) 移除 remove() poll() take() poll(time,unit) 取队首 element() peek() 无 无 -
拒绝策略
- 抛RejectedExecutionException异常的AbortPolicy(如果不指定的默认策略)
- 使用调用者所在线程来运行任务CallerRunsPolicy
- 丢弃一个等待执行的任务,然后尝试执行当前任务DiscardOldestPolicy
- 不动声色的丢弃并且不抛异常DiscardPolicy
-
实现原理
- 首先通过构造方法创建线程池threadPoolExecutor
- 编写实现Runnable接口、重写run方法的任务类Task
- 调用线程池的execute方法创建线程并执行任务threadPoolExecutor.execute(new Task());
- 关闭线程池
- shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。
- shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。
- awaitTermination:等待线程池中线程运行完毕,或子线程中断,或超过设置的时限,主线程继续执行。
- 使用shutdownNow容易报错,要进行异常捕获
- 使用shutdown配合awaitTermination关闭线程池
-
如何设置线程数和队列大小
-
最大线程数,根据性能最高线程数设置
-
阻塞队列长度,根据估计的最大流量设置
-
设置核心线程数,根据平时的流量所需要线程数设置
-
-
-
执行原理
1.通过execute方法提交任务时,当线程池中的线程数小于corePoolSize时,新提交的任务将通过创建一个新线程来执行,即使此时线程池中存在空闲线程。
2.通过execute方法提交任务时,当线程池中线程数量达到corePoolSize时,新提交的任务将被放入workQueue中,等待线程池中线程调度执行。
3.通过execute方法提交任务时,当workQueue已存满,且maximumPoolSize大于corePoolSize时,新提交的任务将通过创建新线程执行。
4.当线程池中的线程执行完任务空闲时,会尝试从workQueue中取头结点任务执行。
5.通过execute方法提交任务,当workQueue存满,并且线程池中线程数也达到maxmumPoolSize时,新提交的任务由RejectedExecutionHandler执行拒绝操作。
6.当线程池中线程数超过corePoolSize,并且未配置allowCoreThreadTimeOut=true,空闲时间超过keepAliveTime的线程会被销毁,保持线程池中线程数为corePoolSize。
注意:上图表达的是销毁空闲线程,保持线程数为corePoolSize,不是销毁corePoolSize中的线程。
7.当设置allowCoreThreadTimeOut=true时,任何空闲时间超过keepAliveTime的线程都会被销毁。
-
-
Lambda表达式创建线程
//实现runable接口并重写run()方法 new Thread(() -> { System.out.println(); },"T1").start(); //线程结束判断方法 while (Thread.activeCount() > 2){ //默认有main线程和gc线程 Thread.yield(); }
Lambda代替匿名内部类,对某些接口进行简单的实现, 规定接口中只能有一个需要被实现的方法。
-
start方法和run方法
start方法用于启动线程,run方法称为线程体,start方法启动线程后自动执行run方法中的内容。
直接调用run方法会被当作主线程下的一个普通方法去执行。
-
-
sleep和wait
- sleep()方法是Thread的静态本地方法;wait()方法是Object类的实例本地方法。
- wait()和notify()因为会对对象的“锁标志”进行操作,所以wait(),notify(),notifyAll()方法必须在同步方法或同步代码块中调用,也就是必须获取到对象锁;而sleep()方法可以在任何地方使用。
- 当一个线程执行到wait()方法时,它会进入到当前对象的线程等待池,同时会释放对象的锁,使其他线程能够访问;可以通过notify随机唤醒一个线程,notifyAll唤醒所有线程来唤醒等待的线程。而sleep()只是会让调用线程进入睡眠状态,让出CPU资源给其他线程,并不会释放掉对象锁。等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。
-
Volatile的作用
volatile修饰的变量能保证每个线程获取该变量的最新值。
volatile通过内存屏障防止了指令的重排序。
结合缓存一致性协议,保证了可见性。
不保证原子性:
根据Java内存模型,多线程并发时,线程缓存还未来的及将内存中已变更的数据重新更新到缓存,就将缓存中的旧数据提交到了内存中。
解决办法:
- 加synchronized,为每个线程加锁;
- 使用JUC下的原子类
- 如atomicInteger.getAndIncrement()代替i++;
- 原子类通过CAS+volatile+返回原始值内存地址的native方法实现原子性。
-
volatile可见性实现原理
- 生成汇编代码时,volatile修饰的共享变量在进行写操作时会多一个Lock前缀的指令。
- Lock前缀的指令会引起处理器缓存将共享变量写回内存。
- 此时其他处理器缓存该内存地址的数据将会无效。
- 处理器缓存发现失效后,就会从内存重新读取最新的当前变量。
-
内存屏障
为了性能优化,JMM会在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,会保证程序最终执行结果不变。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。如果不想重排序,就要使用内存屏障。
加volatile关键字通过插入内存屏障禁止内存屏障前后的指令执行重排序。
-
缓存一致性协议
根据JMM(Java内存模型)概念,为了提高处理速度,处理器不直接和内存进行通信。线程在运行的过程中会把主内存的数据拷贝一份到线程内部缓存中,此时就会出现各个线程之间数据不同步的情况。缓存一致性协议就是指线程通过探测总线上传播的数据来检查自己缓存的值是不是过期了,如果缓存的值对应的内存地址被修改,就会重新从内存中读取此数据到缓存。来保证可见性与一致性。
-
什么时候用到过volatile
单例模式:
-
DCL(Double Check Lock双端检锁机制)
在用synchronized加锁的前后,加对象非空判断。但因为有指令重排,在并发下仍有可能出错。要加volatile。
-
-
CAS
Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。是乐观锁的主要实现方式 。
CAS的操作过程
CAS包括三个参数,V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值 ,当实际值和预期值相同时,代表没有被其他线程修改过,直接将新值赋值给内存地址。反之,就要重新尝试或挂起线程。
CAS的三个问题
-
ABA问题
-
类似MySQL乐观锁处理方式,添加版本号。
-
原子引用:
通过原子引用
AtomicReference<User> userAtomic = new AtomicReference<>()
将User类定义为原子类。通过
AtomicStampedReference<User> userAtomicStamp = new AtomicStampedReference<>(user,100)
声明原子引用时间戳,通过AtomicStampedReference的compareAndSet()方法比较值和时间戳,比较成功则同时修改值和时间戳。
-
-
自旋时间过长
CAS操作如果长时间不成功,会导致其一直自旋,浪费性能。
-
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。atomic中提供了AtomicReference原子引用来保证对象的原子性,可以把多个变量放在一个对象里来进行CAS操作。
-
-
并发异常的集合
会出现java.util.ConcurrentModificationException并发修改异常的集合
-
并发时因为add方法没有加synchronized锁住。
解决办法:
- 使用方法上加了synchronized的并发安全集合Vector,性能降低。
- 使用工具类Collections的线程安全方法。比如Collections.synchronizedList(new ArrayList<>())
- 使用CopyOnWriteArrayList,利用读写分离的原理,写时加锁,读不加锁。写时拷贝一个新的数组,写结束时数组指针指向新数组。
-
HashSet
HashSet底层是HashMap,value为空.
- Collections.synchronizedSet
- CopyOnWriteArraySet
-
HashMap
- 使用ConcurrentHashMap代替
-
-
synchronized
synchronized关键字通过修饰代码块或方法,使方法或代码块在多线程环境下不被多个线程同时执行。
synchronized实现原理:
- synchronized修饰静态方法锁住类对象,修饰实例方法锁住实例对象。当某个线程访问某个静态方法或实例方法时,会检查方法常量池中的方法表结构是否有ACC_SYNCHRONIZED标志,有就是同步方法,线程会先获取监视器锁,再执行方法,执行结束释放监视器锁。线程获取不到监视器锁则被阻塞。
- synchronized修饰同步代码块,采用monitorenter和monitorexit两个指令实现同步。执行monitorenter指令,获取对象的锁,并将锁的计数器加1,执行monitorexit时将锁的计数器减1,计数器为0时,锁被释放。
synchronized和Lock区别:
- synchronized是关键字。Lock是类。
- synchronized不需要手动获取锁、释放锁,发生异常会自动monitorexit释放锁。Lock需要使用Lock和unLock加锁释放锁。
- synchronized不可中断。ReentrantLock可以通过
tryLock(Long timeout, TimeUnit unit)
设置超时方法。 - synchronized加非公平锁。ReentrantLock默认非公平锁。
- synchronized只能随即唤醒一个或唤醒全部线程。ReentrantLock可以精确唤醒。
- synchronized关联一个条件队列。ReentrantLock可关联多个条件队列。
-
并发工具类
-
CountDownLatch减一计数器【秦逐一灭六国后才统一】
让一些线程阻塞直到另一些线程完成一系列操作。
调用await方法的线程将被阻塞;其他线程调用countDown方法将计数器减一,当计数器值为零时,被await阻塞的方法被唤醒。
-
CyclicBarrier【集齐七颗龙珠一起召唤神龙】
CyclicBarrier cyclicBarrier = new CyclicBarrier(10, () -> {System.out.println("屏障线程")})
让一组线程互相等待至某个状态再一起执行。
让一组线程到达一个屏障时通过CyclicBarrier.await方法被阻塞,直到屏障线程到达屏障时,所有被屏障拦截的线程才继续执行。
-
Semaphore信号量【6车抢3车位】
信号量伸缩使用。
Semaphore.acquire方法抢到信号,Semaphore信号减一
Semaphore.release方法释放信号,Semaphore信号加一
-