多线程
Java语言是支持多线程的,多线程技术使程序的响应速度更快 ,可以在进行其它工作的同时一直处于活动状态。实现多线程的方式主要有三种:继承Thread类、实现Runable接口、继承Callable接口。
并发与并行
多线程共享数据
并发:说的是在一个时间段内,多件事情交替执行,宏观上的同时执行。
并行:说的是多个事情在同一时间段内同时执行,微观上的同时执行。
并发编程是在很多线程对共享资源进行访问时,需要通过控制,让多个线程并发的对共享数据进行访问。
多线程的本质问题
由于CPU、内存、硬盘三者之间的读写速度不一样。所以导致
- 多核CPu,每个内核中都有一层高速缓存。每个高速缓存中存储的数据不可见(可见性)
- 线程中有IO操作,耗时较长,操作系统需要切换线程执行(原子性)
- 操作系统对指令进行优化,打乱了指令的执行顺序(有序性)
volatile——解决可见性和有序性
volatile修饰变量,它保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变 量的值,这新值对其他线程来说是立即可见的。还禁止进行指令重排序。 但是volatile不能解决原子性问题。
volatile底层实现原理
使用 Memory Barrier 内存屏障 ,禁止在该条指令执行前插入其他指令,在工作内存修改后,结合缓存一致性协议,将工作内存数据更新到主内存,其他工作内存读取更新。
内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指 令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其 后面的指令移到内存屏障指令之前
有序性:
有序性实现:主要通过对 volatile 修饰的变量的读写操作前后加上各种特定 的内存屏障来禁止指令重排序来保障有序性的。
可见性:
可见性实现:主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现 的。对 volatiile 修饰的变量执行写操作时,JVM
会发送一个 Lock 前缀指令给 CPU,CPU 在执行完写操作后,会立即将新值刷新到内存,同时因为 MESI 缓 存一致性协议,其他各个
CPU 都会对总线嗅探,看自己本地缓存中的数据是否 被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个 CPU
里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见 性
锁和原子变量——解决原子性
原子变量——CAS
CAS 比较并交换 ,是一种乐观锁(没有加锁)的实现,采用自旋的思想比较。内部有3个值:V A B
VAB
- V:内存值, 操作前先将内存值读到工作内存
- A:预期值, 在工作内存修改了变量值后,将要将修改后的值向主内存写入的时候再次读取的主内存数据
- B: 内部操作后的变量值
当向主内存写入数据时,必须满足 A==V, 就V=B, 否则就再次读入主内存值
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private volatile static int num=0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
System.out.println(atomicInteger.incrementAndGet());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+(num++));
}
}.start();
}
}
CAS的缺点
首先CAS
是无锁的,采用自旋的方式,线程不会阻塞,如果有大量的线程不断的自旋去尝试,那么CPU消耗较大,所以CAS思想适合低并发情况。
ABA问题: 就内存值由A变为B, 再由B改为A, CAS不知道内存值已经发生过修改的。
如何解决?添加版本号
通过使用类添加版本号,来避免 ABA 问题。如原先 的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改 为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较, 只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。
Java中的锁
不完全是锁,有的是锁状态,有的是锁的特性
- 乐观锁 采用CAS机制,乐观认为不加锁是没有问题的. 原子类
- 悲观锁 就是真正的加锁实现,认为不加锁是会有问题的.
- 可重入锁 又名递归锁,当一个线程进行入外层方法获取锁时,如果内存调用另一个需要获得该锁修饰的方法,那么线程是可以进入的,synchronized就是一个可重入锁。
- 分段锁 不是具体的锁, 将锁的粒度分的更小,以提高并发效率.
- 自旋锁 不断重试去尝试获得锁,不会让线程进入阻塞状态,提高效率,但是耗cpu.
- 读写锁 里面维护两个锁实现, 一个是
写锁
,一个读锁
。如果使用的是写锁,一次只能有一个线程获得锁,如果是读锁,那么可以允许多个线程获得。写锁的优先级高于读锁
。 - 独占锁 / 共享锁 类似于读写锁。
- **公平锁 ** 可以按照请求的顺序分配锁,
ReentrantLock
中有公平锁实现,里面维护一个队列,按顺序排队获取锁。 - 非公平锁 不按照请求顺序分配锁,synchroized就是非公的,ReentrantLock中默认使用非公平锁,非公平锁的线程进来后会先尝试获取锁的状态,如果是0,那么将其++,进入代码块中,其他线程进入阻塞队列中等待。
synchronized中锁的状态
锁的状态是通过对象监视器在对象头中的字段来表明的。
- 无锁状态: 没有加锁
- 偏向锁: 只有一段线程访问同步代码,此时会将线程的id存入对象头中,下次线程来获取锁时直接分配即可。
- 轻量级锁: 当锁的状态为偏向锁时,又有线程访问,那么锁的状态会升级为轻量级锁,不会让线程进入阻塞状态,而是自旋尝试获取锁,以提高效率。
- 重量级锁: 当锁的状态为轻量级锁时,如果线程数量太多,线程自旋数达到一定数量,锁会升级为重量级锁,线程进入阻塞状态,有操作系统调度分配。
对象结构(记录锁)
对象在内存中的布局分为三块区域:对象头、实例 数据和对齐填充
synchronized需要一个对象,在对象头中记录有没有使用锁
锁的实现
AQS(代码层面)
AQS是juc
( java.util.concurrent java并发包)中实现线程安全的核心组件,是从java代码级别实现
.
内部维护了一个锁状态(0没有,>0有锁)volatile state
。 内部还维护一个队列,保存未获取到锁的线程
.
多个线程来访问,如果有一个线程访问到了state,就将其改为1,其他线程获取失败后,就会添加到队列中 Node(Thread)
ReentrantLock锁的实现
AQS是JUC实现线程安全的核心组件,是Java代码级别中的核心组件,内部维护了一个锁状态(由volatile修饰的state,所以他是可见和有序的) 还维护了一个阻塞队列 保存未获取到锁的线程。多个线程访问,如果有一个线程访问到state,就将其改为1(可见性),其他线程就会进入队列中等待(Node(Thread))。
ReentrantLock它实现了Lock接口。
public class ReentrantLock implements Lock, java.io.Serializable{ }
ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来 对共享资源进行同步。支持可重入锁。其源码有3个内部类,
Sync extends AQS
FairSync
extends Sync 公平实现
NonFairSync
extends Sync 非公平实现
分别是Sync
、NoFairSync
、FairSync
。NoFairSync继承了Sync,采用非公平的策略获取锁。FairSync 类也继承了 Sync 类,表示采用公平策略获取锁。
NoFairSync 非公平锁(默认的锁)
NonFairSync extends Sync 非公平实现
final void lock() {
if (compareAndSetState(0, 1))没有排队,直接尝试去获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//获取锁,表示一定能获取锁,获取不到就继续
}
点进acquire()
方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这是对源码的翻译
FairSync公平锁
而公平锁就比较老实,线程进来先排队,不然不予受理。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
synchronized锁的实现(指令级)
synchronized锁主要依靠底层指令实现
,它是关键字,可以修饰方法。代码块,一次只允许一个线程进入。
指令级别实现: 如果synchronized修饰方法,在编译后的指令中添加ACC_SYNCHRONIZED
表示此方法是同步方法,有线程进入后其他线程不能进入,在对象头中锁标志+1
,方法运行结束,或者出现异常,锁标志-1
。 synchroniezd如果修饰代码块: 在进入代码前加入monitorenter
指令 ,对象锁标志+1
, 同步代码块结束/或者出现异常执行monitorexit
,锁标志-1
。
JUC常用类(线程安全且高效的集合)
ConcurrentHashMap (线程安全且高效的HashMap)
首先,HashMap是线程不安全的 在JDK1.8之前
,HashMap在进行扩容时采用的是头插法,链表转移后,前后链表顺序倒置(头插法导致),在转移过程中修改了原来链表中节点的引用关系,导致链表结点互相引用,即形成了环, 这种情况下,当我们使用get曹操获取到环形链表处的数据,就会发生死循环。
在JDK1.8之后
,高并发的情况下,比如现在有两个线程都要调用put方法,都进行了判断,且都满足条件可以直接插入,这时线程1先插入,线程2在执行的时候就不会再次进行判断,也是直接插入,这就出现了元素覆盖,也就是说线程1做了无用功。这时候就是报错!
在JDK1.5没有引入ConcurrentHashMap的时候,HashTable是线程安全的hashmap,但是HashTable的线程安全非常的存粹。他给每个HashMap的方法都加入了synchronized锁 但是hahsmap通过扰动函数后n-1&hash
计算的索引值,其hash碰撞的概率已经非常小的,所以他们在同一索引位置的情况并不高,但是由于synchronized在高并发情况下会阻塞其他所有线程,所有效率会变得很低很低。
所以在JDK1.5后,java引入了ConcurrentHashMap的线程安全且高效的hashmap,使用cas+synchronized
的机制,实现高效率。源码:
public V put(K key, V value) {
return putVal(key, value, false);
}
可以看出,ConcurrentHashMap不是在put方法加synchronized,而是吧每个数组的位置看做独立区间
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
在putVal()
中先进行判断,如果计算出的索引位置没有没有任何元素,它会采用CAS
机制将改元素添加到第一个位置。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
如果计算的索引位置有元素,那么将会使用当前链表(红黑树)的头结点作为锁的标记对象,在经过hash碰撞确认不是重复元素后,这些线程进入阻塞状态,然后依次添加。
ConcurrentHashMap 不支持存储 null 键和 null 值。为消除歧义,是因为无法区分是拿到的索引位置为null,还是拿不到值返回的为null
CopyOnWriteArrayList / CopyOnWriteSet
同HashMap一样,ArrayList是一样线程不安全的,但是Vector也是给每一个方法都加入了synchronized,包括add()、get()。这就导致很多有线程在写值的时候,其他线程不能读,而且一般情况下,读操作比写操作多,这就导致效率低下。
CopyOnWriteArrayList给add加入了锁,但是在写数据时,先会创建一个新的副本,将新元素写入到新数组中,写完后再将新的数组赋给底层原来数组的引用。读没有做任何控制。
CopyOnWriteArraySet底层就是CopyOnWriteArrayList,不同的就是在添加是判断元素是否重复。
线程池
池? 线程池 & 数据库池?
为什么要有数据库池: 每次链接数据库都要创建数据库;链接对象,用完后销毁,太麻烦。但如果事先创建出一些链接对象放入池子中,每次使用时从池子获取,用完后返回到池子中,这样效率就会大大提升。
线程池: JDK5后提供ThreadPoolExecutor类事先线程池的创建(推荐)。
线程池的优点:
- 重复利用线程。
- 统一管理。
- 提高响应速度。
ThreadPoolExecutor
七个参数
corePoolSize 核心线程池大小
maximumPoolSize 线程池最大线程数
keepAliveTime 非核心线程池无任务时存活时间
unit 存活时间的单位
workQueue 阻塞队列 核心线程池满了后先进入阻塞队列
thredFactory 线程工厂,用于创建线程
hander 拒绝策略
流程
线程池中的阻塞队列
SynchronousQueue:同步队列是一个容量只有 1 的队列,这个队列比较特殊, 它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务,每个 put 必须等待一个 take。
ArrayBlockingQueue:有界队列,是一个用数组实现的有界阻塞队列,按 FIFO 排序量。
LinkedBlockingQueue:可设置容量队列,基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列, 最大长度为 Integer.MAX_VALUE;
线程池中的拒绝策略
构造方法的中最后的参数 RejectedExecutionHandler 用于指定线程池的拒绝 策略。当请求任务不断的过来,而系统此时又处理不过来的时候,我们就需要采 取对应的策略是拒绝服务。
- AbortPolicy 策略:该策略会直接抛出异常,阻止系统正常工作。
- CallerRunsPolicy 策略:只要线程池未关闭,该策略在调用者线程中运行当前的
任务(如果任务被拒绝了,则由提交任务的线程(例如:main)直接执行此任务)。 - DiscardOleddestPolicy 策略:该策略将丢弃最老的一个请求,也就是即将被执 行的任务,并尝试再次提交当前任务。
- DiscardPolicy 策略:该策略丢弃无法处理的任务,不予任何处理。
public static void main(String[] args) {
//创建线程池 7
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
5, 200,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
for(int i=1;i<=10;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);//添加任务到线程池
//Future<?> submit = executor.submit(myTask);
}
executor.shutdown();
}
提交与关闭任务
- 提交任务
exccute()不需要返回值
submit()需要返回值 - 关闭线程池:
shutdownNow是全部interrupt,停止执行,包括未执行的线程,全部取消。
shutdown:没有执行完的线程全部执行玩,然后停止,期间不接受新任务。