并发编程
1、术语解析 线程进程、临界资源
1.1进程Process和线程Thread定义
1.1.1 逻辑层 线程和进程
- 进程包含线程,一个进程中包含多个线程.
- 线程是cpu调度和分配的基本单位,进程是操作系统进行资源分配(cpu,内存,硬盘io等)的最小单位.
- 1、进程Process:OS资源分配的基本单位,切换耗费资源多,操作系统含多个进程,进程含多个线程。分配不同内存空间。
- 2、线程Thread:调度执行的基本单位,切换快速,共享地址空间,通信方便。资源利用率好,需要考虑互斥与同步;同一类线程共享代码和数据空间,但是有独立运行栈和程序计数器。不给分配空间,除了cpu,共享资源。
- JAVA是单线程编程模型,主程序名是Main。但是JVM是多线程,GC是一个专门的线程。
1.1.2 物理层、CPU逻辑层 核心数和线程(CPU 4C8T 四核心八线程)
- 首先,关于计算机系统的很多概念,都有“逻辑层” 和 “物理层”的区分,这个是前提。
- 然后再看,“核心”这个概念是“物理层”的概念,指的就是 CPU硬件的物理核心数量。而“线程” 这个概念,是“逻辑层”的概念,而且这个“逻辑层”的概念,还要区分是 “CPU逻辑层” 还是 “操作系统OS逻辑层”。先说 “CPU逻辑层” 的 线程。Intel 在CPU上搞出了HT技术(Hyper Threading),也叫超线程技术。这个技术简单来说,就Intel 把一个CPU核心上,搞出了两个处理的流水线,在使用的时候可以当成两个来用。而他们把这每一个核心分出来的两个流水线,叫做“线程”。这也就是 4核心8线程的意思。
- 从上层逻辑上来看,完全可以把它当作是个8核心的CPU。再说 “操作系统OS逻辑层”的线程。操作系统把把处理单元称为“进程”,然后在每一个进程里面开辟了粒度更细的“线程”,这个“线程”是运行在某个进程中的处理调度单元,是由操作系统提供的虚拟的概念。因为是虚拟出来的,所以操作系统层面来说,“线程”可以创建很多个,而不局限于CPU层面的那个“8个线程”。
1.2、临界资源
1.2.1 临界资源是一次仅允许一个进程使用的共享资源。
各进程采取互斥的方式,实现共享的资源称作临界资源。
属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。
诸进程间采取互斥方式,实现对这种资源的共享。
1.2.2 临界区
每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。
不论是硬件临界资源还是软件临界资源,多个进程必须互斥的对它进行访问。
多个进程涉及到同一个临界资源的的临界区称为相关临界区。
使用临界区时,一般不允许其运行时间过长,只要运行在临界区的线程还没有离开,其他所有进入此临界区的线程都会被挂起而进入等待状态,并在一定程度上影响程序的运行性能。
1.3 线程状态
···
java.lang.Thread
···
- New 新建状态
- Runnable 可运行
- Waiting 无限期等待:不分配CPU Object.wait() thread.join() LockSupport.part() 需要notify唤醒
- Timed waiting 计时等待:不分配CPU
- Blocked:阻塞 等待获取排它锁(某个线程获取了排它锁synchronize修饰的代码块)
- Terminated被终止 (java.lang.IllegalThreadStateException)
- run正常结束,main终止
- 异常终止了run方法
1.4 线程方法:start和run区别:
1.5 Thread和Runnable的区别 :
1.6 处理线程的返回值
- 1、主线程等待法 2、join()
package com.icbc.thread;
/**
* 获取线程返回值:1、主线程等待法 2、阻塞主线程join(缺点:多个子任务的情况下不够精准) 3、callable接口实现3、
*
* @Auther: XDragon
* @Date: 2021/2/13/013 14:02
* @Email:cnxielong@gmail.com
*/
public class CycleWait implements Runnable {
private String value;
public CycleWait() {
}
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "we have data now";
}
public static void main(String[] args) throws InterruptedException {
CycleWait wait = new CycleWait();
Thread t = new Thread(wait);
t.start();
// System.out.println("value:"+wait.value);//输出 value:null 默认可能不走到多线程
// if (null == wait.value) { //主线程等待法
// Thread.currentThread().sleep(1000);
// }
t.join();//阻塞当前主线程 让子线程走完
System.out.println("value:" + wait.value);
}
}
- 3、Callable接口实现类和FutureTaskAPI实现
/**
* Callable接口实现类
* @Auther: XDragon
* @Date: 2021/2/13/013 15:25
* @Email:cnxielong@gmail.com
*/
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyCallable ready to work");
Thread.currentThread().sleep(3000);
System.out.println("MyCallable work done");
return "MyCallable work done";
}
}
/**
* FutureTask构造方法 isDone、get 方法
*
* @Auther: XDragon
* @Date: 2021/2/13/013 15:37
* @Email:cnxielong@gmail.com
*/
public class FutureTaskTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
if (!futureTask.isDone()) {//任务未完成
System.out.println("MyCallable has not finished");
}
System.out.println("task return:" + futureTask.get());//futureTask.get() 返回MyCallable call()方法的返回值
}
}
- 4、线程池
/**
* 线程池实现任务管理 获取线程池,submit方法获取Future
* @Auther: XDragon
* @Date: 2021/2/13/013 15:49
* @Email:cnxielong@gmail.com
*/
public class ThreadPoolDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
Future<String> future = newCachedThreadPool.submit(new MyCallable());
if (!future.isDone()) {//任务未完成
System.out.println("MyCallable has not finished");
}
System.out.println("task return:" + future.get());
}
}
1.7 sleep和wait区别
-
sleep(有参数)方法是Thread类里面的,主要的意义就是让当前线程停止执行,让出cpu给其他的线程,但是不会释放对象锁资源以及监控的状态,当指定的时间到了之后又会自动恢复运行状态。
-
wait()方法是Object类里面的,主要的意义就是让出cpu给其他的线程,让线程放弃当前的对象的锁,进入等待此对象的等待锁定池,只有针对此对象调动notify方法或者wait时间结束,本线程才能够进入对象锁定池准备获取对象锁进入运行状态。
-
区别是:
(a)sleep()有参数:毫秒 wait()可以无参数
(b)sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。
© wait要写在 synchronize里面
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
static void yield() - 让出CPU的执行权,转而去执行其他的线程(了解)。
static void sleep(long millis) - 让当前正在执行的线程休眠参数指定的毫秒数。static void sleep(long millis, int nanos) - 让线程休眠参数指定的毫秒 + 纳秒。
- 1秒 = 1000毫秒 1毫秒 = 1000微秒 1微秒 = 1000纳秒
void interrupt() - 用于中断线程,通常用于睡眠的打断(了解)。
int getPriority() - 用于获取当前线程的优先级。void setPriority(int newPriority) - 用于修改线程的优先级为参数指定的数值。
- 优先级高的线程表示获取时间片的机会越多,但不保证一定先执行。
boolean isDaemon() - 用于判断当前线程是否为守护线程。
void setDaemon(boolean on) - 用于设置该线程为守护线程。
void join() - 用于等待调用对象所描述的线程终止。
void join(long millis) - 等待调用对象终止的最长时间为参数指定的毫秒。
void join(long millis, int nanos) - 用于等待参数指定的毫秒 + 纳秒。
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
1.8 notify和notifyAll区别 锁池、等待池
参考链接:https://blog.csdn.net/weixin_42504145/article/details/85329386?ops_request_misc=%25257B%252522request%25255Fid%252522%25253A%252522161322908616780255294196%252522%25252C%252522scm%252522%25253A%25252220140713.130102334…%252522%25257D&request_id=161322908616780255294196&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-1-85329386.first_rank_v2_pc_rank_v29_10&utm_term=JAVA+%25E9%2594%2581%25E6%25B1%25A0
1.8.1 锁池和等待池
在java中,每个对象都有两个池,锁(monitor)池和等待池
- 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程B和C想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程(B和C)在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以B和C这些线程就进入了该对象的锁池中。
- 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。
1.8.2 notify和notifyAll:唤醒线程,线程会由等待池进入锁池
- notifyAll()方法:唤醒所有 wait 线程,会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
- notify()方法:只随机唤醒一个 wait 线程,只有一个线程会由等待池进入锁池。
- 被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
1.9 yield方法:让出CPU的执行权
-
static void yield() - 让出CPU的执行权,转而去执行其他的线程(但是线程调度器可能仍然会选择这个线程)。
-
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
-
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。暂停当前正在执行的线程对象,并执行其他线程。
-
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
1.10 中断线程 interrupt
2、锁分类
2.1互斥锁
2.1.1 synchronized是互斥锁
2.1.1.1 synchronized对象锁和类锁
2.1.2 synchronized实现
- 主要是由JAVA对象头和monitor来实现的
2.1.2.1 JAVA对象头
- 对象在内存中分配:对象头、实例数据、对齐填充
- 对象头:由 Mark Word:存放锁信息 和 Class Metadata Address实现
MarkWord:32bit
2.1.2.2 monitor锁
hotSpot虚拟机 ObjectMonitor.app文件来实现 monitor是娘胎里带来的
_EntryList::存放要请求数据的线程
_WaitList:存放等待的线程
2.1.3 synchronized四种状态
2.1.3.1 偏向锁:
2.1.3.2 轻量级锁:
2.1.3.4 重量级锁:
- 多个线程竞争锁,轻量级锁要膨胀成重量级锁。
2.2 解锁和锁本质
- 锁本质:工作内存和主内存的数据交互控制
- 释放锁:线程对应的本地内存变量–>刷新到–>主内存
- 获取锁:主内存–>刷新到–>线程对应的本地内存变量
2.1.2.4 可重入:一个线程可以再次请求自己持有的对象锁的临界资源
synchronize:是可重入锁
2.1.2.4 synchronize缺点:
2.2 自旋锁和自适应自旋锁
- 自旋锁:线程等一会不释放CPU。
2.3锁消除、锁粗化
2.3.1 锁消除
2.3.2 锁粗化
2.4 synchronized和ReentrantLock(再入锁)
java.util.concurrent.locks.ReentrantLock
2.4.1 公平锁、非公平锁(fairness指定)
3、AQS、CAS、ABA问题 悲观锁乐观锁
3.1 悲观锁乐观锁
添加链接描述:https://www.cnblogs.com/renhui/p/9755789.html
3.1.1 悲观锁
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
- 这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
3.1.2 乐观锁
- 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
3.2 CAS:compare And Swap,比较并交换
3.2.1 CAS定义
- CAS:compareAndSwamp,比较并交换
java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。 - CAS有3个操作数,内存值V,预期值A,更新值B。只有当V=A时,才把V更新为B。
用途:
- java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的(eg. AtomicInteger.java,AtomicBoolean,AtomicLong)
3.2.2 CAS缺点
如果出现ABA问题:用互斥锁
3.3 AQS
- AQS:是抽象同步队列AbstractQueued Synchronizer的简称,是实现同步机制的基础,并发包中的各种所谓的锁就是通过AQS实现的。
参考链接:
https://blog.csdn.net/u010862794/article/details/72892300?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162609772116780357215496%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162609772116780357215496&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-72892300.first_rank_v2_pc_rank_v29&utm_term=CAS%E5%92%8CAQS&spm=1018.2226.3001.4187
3.4 ThreadLocal、ThreadLocalMap 和 volatile关键字
3.4.1 ThreadLocal
- 意义:ThreadLocal会为每个使用该变量的线程提供自己的变量副本,所以每一个线程都可以独立地改变自己的副本,将对象的可见范围限制在同一个线程内,而不会影响其它线程所对应的变量副本。
这样做其实就是以空间换时间的方式(与synchronized相反),以耗费内存为代价,单大大减少了线程同步(如synchronized)所带来性能消耗以及减少了线程并发控制的复杂度。ThreadLocal 是一个线程的局部变量(其实就是一个Map)。 - 数据结构:ThreadLocal里面有ThreadLocalMap,ThreadLocalMap的KEY是ThreadLocal的引用,Value是具体的值
- 方法:
- get:获取当前线程中保存的变量副本
- set:设置当前线程中变量副本
- remove:删除 使用完threadLocal之后手动删除一下
- Thread中包含:threadLocal,threadLocal内部类是ThreadLocalMap
ThreadLocal的内存泄漏问题:使用完threadLocal之后手动删除一下。 - ThreadLocalMap的生命周期跟Thread一样长,Spring使用线程池,线程使用完成不会被销毁,会放入线程池。
- 如果ThreadLocal被设置为null后,并且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将被回收。这样的话,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value就会形成内存泄露。
用途:
- 1)存储用户Session
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
- 2)解决线程安全的问题
- 比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String formatDate(Date date) {
return format1.get().format(date);
}
}
3.4.2 ThreadLocal和ThreadLocalMap的关系
- ThreadLocalMap是ThreadLocal的一个内部类,ThreadLocalMap内部有个Entry (K,V结构的)类数组的集合。
- ThreadLocalMap的Key是ThreadLocal的弱引用,value是线程变量副本。ThreadLocal不储存值但是能储存KEY,但是能根据Key找到value。
3.4.2 volatile
- volatile主要是用来在多线程中同步变量。
- 在一般情况下,为了提升性能,每个线程在运行时都会将主内存中的变量保存一份在自己的内存中作为变量副本,但是这样就很容易出现多个线程中保存的副本变量不一致,或与主内存的中的变量值不一致的情况。
- 而当一个变量被volatile修饰后,该变量就不能被缓存到线程的内存中。
注意:
- volatile无法保证对变量的任何操作都是原子性的。
- 使用volatile关键字时必须具备两个条件:
- 1、对变量的写操作不依赖于当前值。
- 2、该变量没有包含在具有其他变量的不变式中。
- 即保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
4、JMM(JAVA内存模型)
- 本地内存:JAVA线程私有,拷贝主内存的数据到工作内存,修改完成本地内存后线程通信,放到主内存。
- Java内存模型规定所有的变量都存储在主内存中(JVM内存的一部分),每个线程有自己独立的工作内存,它保存来被该线程使用的变量的主内存复制。线程对这些变量的操作都在自己的工作内存中进行,不能直接操作主内存和其他工作内存中的变量或者变量副本。
4.1 主内存:
4.2 工作内存:
4.3 JMM可见性:指令重排序
4.3.1 JMM可见性:Happens-before原则:判断是否存在竞争
4.3.2 volatile:轻量级同步关键字 共享变量线程可见
4.3.2.1 缺点 volatile 修饰变量的操作可能线程不安全:
i++ 不是原子性,线程不安全:
线程安全:
4.3.3 线程安全单例:
4.3.4 区分volatile和synchronize:
5、线程池
5.1意义:
- 提前创建好一定量的线程,避免频繁创建线程和销毁线程带来的系统开销。
5.2:构造函数 参数
包路径: java.util.concurrent.ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
- corePoolSize(5):核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize(10):线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- workQueue(1000):一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响。
- keepAliveTime(5S):表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性(天、小时、分钟、秒、ms、μs、ns)
- threadFactory:线程工厂,主要用来创建线程;
- handler:线程池饱和(阻塞队列满了)策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 。
5.3:线程池状态、生命周期
5.4:线程池大小
5.5:线程池实现方式:
1、CahcedThreadPool
2、FixedThreadPool
3、SingleThreadPool
4、ScheduledThreadPool 开发中基本没用到过
x、通信方式
1、线程共享地址
1)锁机制,互斥锁(排他,防止并发修改)、条件变量(以原子方式阻塞进程,直到满足条件为止,与互斥锁共用)、读写锁(多个线程同时读共享数据,写操作互斥);
2)信号量机制semaphore,无名信号量和命名信号量;
3)信号机制signal,计数器,控制共享资源的同步。
线程通信主要用于线程同步,无数据交换机制
2、进程通信:
1)管道pipe,半双工,数据单向流动,只能在父子进程之间,会发生阻塞,存满则写阻塞,写会读阻塞;
2)有名管道namedpipe,半双工,允许非父子通信;
3)信号量semophore,计数器,控制多个进程共享资源,作为锁,同一进程内不同线程之间同步。
4)消息队列messagequeue,消息链表,存放在内核由消息队列标识符标识,克服信号传递信息少,管道只承载吴格式字节流等。
5)信号sinal,通知接收进程,某个事件已发生。
6)共享内存sharememory,映射一段能在其他进程访问的内存,多个进程共享。
7)套接字socket,不同设备及其间的进程通信。