java并发编程
进程:运行时程序,分配内存资源最小单位
线程:进程执行最小单元,CPU执行最小单元
🌵如何创建线程:
🍃1.继承Thread类,重写run()方法
public class TestThread extends Thread{
@Override
public void run() {
System.out.println(66666);
}
public static void main(String[] args) {
TestThread testThread=new TestThread();
testThread.start();
}
}
🍃2.实现Runnable接口,重写run()方法
public class TestThread implements Runnable{
@Override
public void run() {
System.out.println(66666);
}
public static void main(String[] args) {
TestThread testThread=new TestThread();
Thread s1=new Thread(testThread);
s1.start();
}
}
🍃3.实现Collable接口
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class TestThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(6666666);
return 3;
}
public static void main(String[] args) {
TestThread testThread=new TestThread();
FutureTask<Integer> futureTask=new FutureTask<>(testThread);
Thread thread=new Thread(futureTask);
thread.start();
}
}
🍃4.从线程池里取
🌵线程常用方法:
🌵线程状态:
🍃1.新建
一个Thread类或其子类的对象被声明并创建
🍃2.就绪
处于新建状态的线程被start()后,将进入线程队列等待CPU时间片
🍃3.运行
当就绪的线程被调度并获得CPU资源时,便进入运行状态
🍃4.阻塞
在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行
🍃5.死亡
线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
🌵多线程
🍃优点:
多线程技术使程序的响应速度更快 ,可以在进行其它工作的同时一直处于活动状态,程序性能得到提升。
注:压榨硬件剩余价值(硬件利用率)
🍃缺点:
多线程同时访问共享数据(安全性),可能出现资源争夺问题,(性能)
🍃并发与并行:
并发
执行说一个时间段内,多件事情在这个时间段交替执行
并行
执行多件事同一时刻同时发生
🌵并发编程问题:
JMM(java内存模型)
Java 内存模型中规定了所有的变量都存储在主内存
中,每条线程还有自己的工作内存
,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
注:这里的工作内存是 JMM 的一个抽象概念,也叫本地内存
,其存储了该线程读 写共享变量的副本
🍃不可见性:
由于JMM(java内存模型),一个线程对共享变量进行修改,其他线程不能立马看见
🍃乱序性:
指令在执行过程中,将指令顺序改变
🍃非原子性:
线程切换带来非原子性问题
注:加锁保持线程互斥可以解决非原子性问题
🍀Volatile关键字:
🍃作用:
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后
🍂1.线程对变量操作变成可见
🍂2.禁止指令重排
注:volatile不能保证解决线程切换带来非原子性问题
🌵如何保证原子性
🍀原子变量(扩展)
🍀 CAS机制:
CAS机制是一种不加锁的实现(乐观锁),采用自旋的思想,
🍂自旋:
即每次判断我的预期值和内存中的值是不是相同,如果不相同则说明该内存值已经被其他线程更新过了,因此需要拿到该最新值作为预期值,重新判断。而该线程不断的循环判断是否该内存值已经被其他线程更新过了
🍂实际操作:
CAS 包含了三个操作数:
①内存值 V
②预估值 A (比较时,从内存中再次读到的值)
③更新值 B (更新后的值)
当且仅当预期值 A==V,将内存值 V=B,否则什么都不做
🌿什么是原子类:
在java.util.concurrent 包下面提供一些类,可以在不加锁的情况下实现++操作的原子性. 这些类称为原子类 AtomicInteger.
🌿原理:volatile+CAS机制
注:原子类内部实现使用了不加锁的CAS机制,
线程不会被阻塞,所有的线程都会不不断的重试进行操作,在访问量大的情况下,回导致cpu消耗过高原子类适合低并发下使用
🌵ABA问题:
一个线程获取了内存值为A当线程修改后,要写回内存时,已经有其他的线程改变了内存值 B,又有线程将内存值改回到与当前线程预估值相同的值 A
解决方案:使用有版本号的原子类
🍀锁
注:Java 中很多锁的名词,这些并不是全指锁,有的指锁的特性,有的指锁的设计,有的指锁的状态
🌿乐观锁:
乐观认为不用加锁,并发操作不会出问题(原子类)
注:适用于读多写少情况
🌿悲观锁
必须加锁,不然一定出问题
注:适用于读少写多情况
🌿可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁(ReentrantLock,Synchronized),两个方法共用同一把锁
🌿读写锁
特点: 读读共享,读写互斥,写写互斥
注:加读锁是防止在另外的线程在此时写入数据,防止读取脏数据
🌿分段锁
注:jdk8之后,去除了真正的分段锁,现在的分段锁不是锁,是一种实现的思想.将锁的粒度更小化
例如ConcurrentHashMap 没有给方法加锁用hash表中的第一个节点当做锁的, 这样就可以有多把锁,提高了并发操作效率
🌿自旋锁
所谓自旋其实指的就是自己重试,当线程抢锁失败后,重试几次,要是抢到锁了就继续,要是抢不到就阻塞线程。
注:自旋锁是是比较消耗 CPU 的,因为要不断的循环重试,不会释放 CPU资源。另外,加锁时间普遍较短的场景非常适合自旋锁,可以极大提高锁的效率
🌿共享锁/独占锁
共享锁: 多个线程共享一把锁, 读写锁中的读锁,都是读操作时,多个线程可以共用
独占锁: synchronized ReentrantLock 互斥的 读写锁的写锁
注:ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
🌿公平锁/非公平锁
公平锁(Fair Lock)是指按照请求锁的顺序分配,拥有稳定获得锁的机会.
非公平锁(Nonfair Lock)是指不按照请求锁的顺序分配,不一定拥有获得锁的机会.(如 synchronized )
注:ReentrantLock 默认是非公平锁,但是底层可以通过 AQS 的来实现线程调度,所以可以使其变成公平锁
🌿偏向锁/轻量级锁/重量级锁
锁状态
🌱无锁:
没有任何线程使用锁对象
🌱偏向锁:
就是当前只有一个线程访问,在对象头Mark Word中记录线程id,下次此线程访问时,可以直接获取锁
🌱轻量级锁:
当锁的状态为偏向锁时,此时继续有线程来访问,升级为轻量级锁 会让线程以自旋的方式获取锁,先程不会阻塞
🌱重量级锁:
当锁的状态为轻量级锁时,线程自旋获取锁的次数到达一定数量时,锁的状态升级为重量级锁,会让自旋次数多的线程进入到阻塞状态,因为访问大时,线程都自旋获得锁,cpu消耗大.
注:轻量级(自旋),重量级(操作系统)
☘️对象结构
对象头,实例数据,对齐填充
对象头中有一块区域称为 Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID等等
🍀synchronized实现:
Java 提供的一种原子性性内置锁,Java 每个对象都可以把它当做是监视器锁,线程代码执行在进入 synchronized 代码块时候会自动获取内部锁,这个时候其他线程访问时候会被阻塞,直到进入 synchronized 中的代码执行完毕或者抛出异常或者调用了 wait 方法,都会释放锁资源。
注:synchronized 基于进入和退出监视器对象来实现方法同步和代码块同步
☘️同步方法:
使用 ACC_SYNCHRONIZED 标记是否为同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,该标记表明线程进入该方法时,需要 monitorenter,退出该方法时需要 monitorexit
☘️代码块的同步:
利用 monitorenter 和 monitorexit 这两个字节码指令。在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁。当前线程拥有了这个对象的锁,把锁的计数器+1;当执行 monitorexit 指令时将模计数器-1;当计数器为 0 时,锁就被释放了。
🍀AQS
AQS(AbstractQueuedSynchronizer)抽象同步队列是一个底层具体的同步实现者
☘️AQS实现原理:
在内部有一个 state 变量表示锁是否使用, 初始化 0,在多线程条件下,线程要执行临界区的代码,必须首先获取 state,某个线程获取成功之后, state加 1,其他线程再获取的话由于共享资源已被占用,所以会到 FIFO 队列去等待,等占有 state 的线程执行完临界区的代码释放资源( state 减 1)后,会唤醒FIFO 中的下一个等待线程(head 中的下一个结点)去获取 state
注:state 由于是多线程共享变量,所以必须定义成 volatile,以保证 state 的可见性, 同时虽然 volatile 能保证可见性,但不能保证原子性,所以 AQS 提供了对 state 的原子操作方法,保证了线程安全
🍀ReentrantLock锁实现
ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来对共享资源进行同步,同时和 synchronized 一样,ReentrantLock 支持可重入
注:ReentrantLock 总共有三个内部类,并且三个内部类是紧密相关的
☘️构造方法
☘️NonfairSync 类继承了 Sync 类,表示采用非公平策略获取锁,其实现了 Sync 类中抽象的 lock 方
☘️FairSync 类也继承了 Sync 类,表示采用公平策略获取锁,其实现了 Sync 类中的抽象 lock 方法.
🌵JUC常用类
🍀ConcurrentHashMap
ConcurrentHashMap 同步容器类是 Java 5 增加的一个线程安全的哈希表。对与多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用“锁分段”机制
.(用了 Node 锁,减低锁的粒度,提高性能,并使用 CAS操作来确保 Node 的一些操作的原子性,取代了锁)
☘️具体操作
put 时首先通过 hash 找到对应链表过后,查看是否是第一个 Node,如果是,直接用 cas 原则插入,无需加锁。然后, 如果不是链表第一个 Node, 则直接用链表第一个 Node 加锁,这里加的锁是 synchronized
注:读操作不加锁
注:Hashtable线程安全,Hashmap线程不安全
注:ConcurrentHashMap 不支持存储 null 键和 null 值. 为了消除歧义,ConcurrentHashMap 不能 put null 是因为 无法分辨是 key 没找到的 null 还是有 key 值为 null,这在多线程里面是模糊不清的,所以就不让 put null
🍀CopyOnWriteArrayList
注:ArraayList 是线程不安全的,在高并发情况下可能会出现问题, Vector 是线程安全的
☘️原理:
CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。
注:读取不加锁,写入不会阻塞读取操作,写入写出需要同步等待
☘️CopyOnWriteArraySet:
CopyOnWriteArraySet 的实现基于 CopyOnWriteArrayList,不能存储重复数据。
🍀辅助类 CountDownLatch
CountDownLatch 允许一个线程等待其他线程各自执行完毕后再执行
☘️底层实现:AQS
☘️操作:
创建 CountDownLatch 对象时指定一个初始值是线程的数量。每当一个线程执行完毕后,AQS 内部的 state 就-1,当 state 的值为0 时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
🌵线程池
☘️作用:
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
java从jdk5开始就有了线程池的实现 有两个类ThreadPoolExecutor,Executors实现线程池其中阿里巴巴开发规约中规定使用ThreadPoolExecutor实现
注: ThreadPoolExecutor中可以准确的控制创建的数量,最大等待数量,拒绝策略等
☘️ThreadPoolExecutor构造方法中的7个参数
1.corePoolSize 5 核心池子的数量(大小),默认是先不创建线程,有任务到达后, 再创建,之后就不销毁 了2.maximumPoolSize 10 最大池子数量
3.keepAliveTime: 非核心线程池中的线程,在多久之后没有任务执行时,销毁
4.TimeUnit 时间单位
5.workQueue 等待队列 设置队列数量 20
6.threadFactory 创建线程工厂
7.handler 拒绝策略
🍀拒绝策略:
1.AbortPolicy : 抛出异常
2.CallerRunsPolicy : 由提交任务的线程执行, 例如我们的main线程
3.DiscardOldestPolicy: 丢弃队列中等待时间最长的任务
4.DiscardPolicy: 直接丢弃任务 不予理睬
🍃线程执行
创建完成 ThreadPoolExecutor 之后,当向线程池提交任务时,通常使用 execute 方法。execute 方法的执行流程图如下:
🍃线程池中的队列:
ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,创建时必须设置长度,,按 FIFO 排序量。
LinkedBlockingQueue:基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置是一个最大长度为 Integer.MAX_VALUE
🍃execute 与 submit 的区别
执行任务除了可以使用 execute 方法还可以使用 submit 方法。它们的主要区别
是:execute 适用于不需要关注返回值的场景,submit 方法适用于需要关注返
回值的场景。
🍃关闭线程池
shutdownNow:
对正在执行的任务全部发出 interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:
当我们调用 shutdown 后,线程池将不再接受新的任务,但也不会
去强制终止已经提交或者正在执行中的任务
🌵ThreadLocal
ThreadLocal是用来为每个线程提供一个变量副本,每个线程中的变量是相互隔离的, 所以称为本地线程变量
🍃hredLocal底层结构:
ThreadLocal内部维护一个ThreadLocalMap的内部类 为每个线程创建一个ThreadLocalMap对象,用来保存此线程拥有的变量在每一个线程中的ThreadLocal对象中,用当前操作的ThreadLocal对象作为键每次为线程赋值时,首先要获取当前线程对象,拿到当前线程对象中的ThreadLocalMap
🍃ThreadLocal内存泄漏问题
当本地变量不再线程中继续使用时,但是value值还与外界保持引用关系,这样一来,垃圾回收器就不能回收ThreadLoaclMap对象,会造成内存泄漏问题
🍃解决方案:
用完之后删除 threadLocal.remove();下次垃圾回收时,就可以回收ThreadLoaclMap了.