Java增强之并发编程
1 多线程
1.1 进程及线程
程序启动的时候,电脑会把这个程序加载到内存,在内存中需要给当前的程序分配一段的独立运行的空间,这个空间就专门负责这个程序的运行。每个应用程序运行都需要在内存中有自己独立的运行空间,互不影响。进程就是应用程序在内存中的独立空间,负责当前应用程序的运行,负责调度当前程序中的所有运行细节。
线程:位于进程中,负责当前进程中的某个具备独立运行资质的空间,负责程序中具体的某个独功能的运行。一个进程中至少有一个线程,可以并发运行多个线程。多线程之间共享同一个堆空间,每个线程都有自己独立的栈空间
1.2 多线程
在一个进程中,同时开启多个线程,同时去完成某些任务。
一个进程可以并发运行多个线程,提高运行效率。
1.3 多线程运行的原理
CPU在线程中做时间片的切换。程序的运行时CPU负责的,其实CPU在运行程序时,某个时刻只能运行一个程序,CPU在多个程序之间进行高速的切换,因为切换的频率和速度太快了,所以我们是看不出来的。从而得知多线程虽然可以提高程序的运行效率,但是不能无限制的开线程。
1.4 实现线程的方式
(1)继承Thread类
(2)实现Runnable接口
(3)Callable和FutureTask创建线程
(4)通过线程池创建线程
代码演示见之前的文章:https://blog.csdn.net/weixin_43786255/article/details/92065717
1.5 线程状态图解
详细讲解见:https://blog.csdn.net/weixin_43786255/article/details/92065717
2 java同步关键字
在多线程编程中,为了达到线程安全的目的,往往通过加锁的方式来实现。
2.1 synchronized
是JVM级别的锁,在编译过程中,在指令级别加入一些标识来实现。无法中断正在阻塞队列或者等待队列的线程。synchronize是java语言的内置特性,锁的释放是由jvm决定的,人工无法干预 。
常与wait(),notify(),notifyAll()方法联用,调用wait()方法线程让出CPU,释放锁,进入等待状态waitting,进入等待队列。当其他线程调用notify()(随机唤醒等待队列中的一个线程)或notifyAll()(唤醒等待队列中的全部线程)时没会将队列中的线程对象,放入第二个阻塞队列,状态是blocked,等待锁被释放后,开始竞争锁。
synchronize提供了偏向锁,轻量级锁,重量级锁。
锁的释放时机有两种情况:
①获取锁的线程操作完成,该线程会自动释放锁 ;②)获取锁的线程出现异常,jvm会自动释放。
synchronized存在的问题 :
①如果获取锁标记的线程不主动释放锁,则未获取标记的只能等待,而且人工无法干预;②当多个线程读写文件时,读读操作互相不影响,但是synchronized仍然无法同时执行。
2.2 Lock
Lock锁是Java代码级别的,用户可以主动添加锁,但是必须手动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生。提供了公平锁,轮询锁,定时锁,可中断锁等,还增加了多路通知机制(Condition),可以用一个锁来管理多个同步块。
与synchronized的区别:
①synchronized 是 Java 语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
②synchronized 由系统自动释放,lock必须手动释放,否则可能产生死锁。
常见方法如下:
void lock() | 获取锁(阻塞,如果其他线程获取到锁,需要等待) |
boolean tryLock() | 获取锁(非阻塞,如果其他线程获取到锁,则返回false) |
boolean tryLock(long time, TimeUnit unit) | 获取锁(在指定的时间范围内获取,超时返回false) |
void lockInterruptibly() | 可中断锁 |
void unlock() | 释放锁 |
ReentrantLock 类是唯一实现了Lock 接口的类,并且 ReentrantLock 提供了更多的方法。
区别对待读、写的操作用ReadWriteLock接口的实现类ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁。
ReadWriteLock将读写操作分离处理:
①一个线程获取读锁,另外的线程可以获取读锁。但是不能获取写锁(必须等待读锁释放)。
②一个线程获取写锁,那么一定要等待该线程释放写锁,其他线程才能执行读写操作。
2.3 死锁
死锁:两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,会让程序挂起无法完成任务。
产生死锁的条件:
①互斥条件:一个资源每次只能被一个进程使用;
②请求与保持条件:一个进程因请求资源而阻塞式,对已获得的资源保持不放;
③不剥夺条件:进程已获得的资源,在未使用完成之前,不能强制剥夺;
④循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免死锁:避免相互等待,设置标记位
死锁处理:破坏条件
2.4 Volatile特殊域变量
首先了解在多线程编程中,要解决的问题主要有以下三方面:
①原子性:作为一个整体运行
②可见性:多个线程修改的内容是可见的。CPU不是直接和系统内存通信,而是把变量读到内部的缓冲,也叫私 有的数据工作栈,修改也是在内部缓存中,但是何时修改到系统内存不能确定,这个时间差就可能导致读到的值不是最新 值。
③指令重排:虚拟机把代码编译成指令后,出于优化,保证代码不变的情况下,会调整指令的执行顺序
valotile能够满足可见性和有序性,但无法保证原子性。
①保证可见性:在修改后强行把对变量的修改同步到系统内存中,当其他CPu在读取自己内部缓存中的值发现是 volatile修饰的时候会把内部缓存中的值置为无效,从系统内存中读取。
②保证有序性:在某些指令中插入屏障指令,用于确保在向屏障指令后面继续执行的时候,前面的所有指令已经 输入完毕。
原子性通过原子量:Atomicxxx:原子量保证数据的原子性
2.5 ThreadLocal线程变量副本
ThreadLocal提供了线程本地变量,访问本地变量的每个线程都会拷贝一个变量到自己的本地内存,多个线程操作这个变量的时候,实际上是操作自己本地内存里面的变量,这样就不会对其他线程产生影响。
ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题,ThreadLocal采用以时间换空间的方式,同步机制采用以空间换时间的方式
作用:保证线程的独立变量。
应用场景:有变量或对象实例需要在线程中多个地方被重复使用,不希望线程之间共享,又不希望每次使用是都重新创建,加大内存开销。
与Synchronized的区别:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
使用:可以将可以将 ThreadLoad<T>视为 Map<Thread ,T>,把需要隔离的数据放入ThreadLocal,通过threadLocal.set(val)赋值,threadLocal.get()获取值,最好不要放在线程池中,避免复用。
3 并发包
3.1 java并发包
jdk1.5版本以后,大多数的特性在 java.util.concurrent 包中,是专门用于多线程发编程的,充分利用了现代多处理器和多核心系统的功能以编写大规模并发应用程序。主要包含原子量、并发集合、同步器、可重入锁,并对线程池的构造提供了强力
的支持。
3.2 线程池
通过重用现有的线程池,而不是创建新的线程,可以在处理多个请求的时候分摊在线程创建和销毁过程中产生的巨大开销,当请求到达的时候工作线程已经存在,不会由于等待创建线程而延迟执行,从而提高系统的响应性。
public ThreadPoolExecutor(int corePoolSize, corePoolSize: 核心池的大小(常驻线程数) |
ThreadPoolExecutor的执行顺序:
①当线程数小于核心线程数时,创建线程
②当线程数大于核心线程数,且队列未满时,将任务放入任务队列
③当线程数大于核心线程数时,且队列任务已满,若线程数小于最大线程数(核心线程数+有界队列的数目),则创建线程;若线程数大于最大线程数,则抛出异常,拒绝任务。
更多详细讲解见:https://blog.csdn.net/weixin_43786255/article/details/92065717