进程和线程
内存中运行的程序就是进程;进程中执行的任务就是线程。
进程是操作系统资源分配的基本单位;线程是处理器调度资源的基本单位。
每个进程都有各自独立的数据空间,因此程序间的切换开销很大;线程共享进程的堆和方法区,相互切换的开销比较小。
一个进程崩溃之后,在保护模式下不会对其他进程产生影响;但是一个线程崩溃会导致整个进程的崩溃。因此多进程比多线程更健壮。
Volatile
定义:
volatile是java虚拟机提供的轻量级同步机制,能够保证可见性和有序性,volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性(保证原子性可以使用AtomicInteger)。而JMM(java内存模型)要求保证可见性、原子性和有序性,volatile能够做到其中的两点
原理:
可见性:即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。原理是使用内存屏障。
有序性:使用内存屏障,在执行操作的时候不允许其他操作。
CAS
定义:
即Compare And Swap,比较并交换,查看主内存中的值是否和自己期望的值一样,如果一样那么就执行交换操作,如果不一致则证明这个值被修改过,取消操作,他能够保证数据的原子性。
原理:
由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。
使用Unsafe类,Unsafe类直接使用本地方法native进行访问,即使用计算机原语,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
存在的问题:
一直循环比较造成CPU的开销比较大,并且存在ABA问题
ABA问题
是什么?
我查看的时候他的值为A,但是当我去替换的时候可能中间已经被其他的线程操作成B又改为A了
对于数值类型无所谓,但是如果是引用类型,那么对象里面的属性有可能被修改了。
解决方法
添加版本号
Lock
Lock锁底层是用的是CAS比较并交换,创建一个AtomicReference owner变量作为锁
lock方法使用owner.compareAndSet(null,Thread.currentThread())让一个线程获得锁,除了获得锁的其他线程使用LockSupport.park()进行阻塞
unlock方法会owner.compareAndSet(Thread.currentThread(),null)解除锁,其他线程使用LockSupport.unpark()进行唤醒
import sun.misc.Queue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
public class KingLock implements Lock {
//原子锁 房卡
AtomicReference<Thread> owner = new AtomicReference<>();
//队列,放那些阻塞的线程
LinkedBlockingDeque<Thread> waiters = new LinkedBlockingDeque<>();
@Override
public void lock() {
while(!owner.compareAndSet(null,Thread.currentThread())){
//抢不到锁的线程让他们阻塞
waiters.add(Thread.currentThread());
LockSupport.park();
}
}
@Override
public void unlock() {
//持有锁的线程解锁
if(owner.compareAndSet(Thread.currentThread(),null)){
//唤醒其他等待的线程
for(Object object : waiters.toArray()){
Thread next = (Thread) object;
LockSupport.unpark(next);
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
Sychornized
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。
偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较ThreadID。特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存货,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为
轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
轻量级锁:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word。若拷贝失败:若当前只有一个等待线程,则可通过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
重量级锁:指向互斥量(mutex),底层通过操作系统的mutex lock实现。等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。
Synchronized总共有三种用法:
1.当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
2.当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁,作用的对象是这个类的所有对象;
3.当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;
4.修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
线程池
定义
创建好一定数量的线程,需要的时候就来线程池里面取
底层使用阻塞队列,通过executor框架来实现,该框架下主要是ThreadPoolExecutor类
优点:
1、降低资源的消耗,免去了线程的创建和销毁的过程
2、提高响应速度,任务到达时不需要等待线程的创建就可以立即执行
3、提高线程的可管理性,使用线程池进行统一的分配、调优和监控
线程池的重要参数:
corePoolSize:线程池中的常驻线程数(今日当值)
maximumPoolSize:可容纳的最大线程数(最多窗口)
workQueue:任务被提交但是未执行的任务(候客区)
keepAliveTime:多余的空闲线程的存活时间(加班窗口等待的时间)
unit:keepAliveTime的单位
threadFactory:线程工厂
handler:拒绝策略
四种拒绝策略
AbortPolicy:直接抛异常(默认)
CallerRunsPolicy:将任务回退到调用者
DiscardOldestPolicy:抛弃队列中最久的任务
DiscardPolicy:丢弃当前任务
线程池的底层原理:
三种线程池:
单一的:SingleThreadExecutor(请求队列长度为最大INT值)
定长的:FixedThreadPool(请求队列长度为最大INT值)
可变的:CachedThreadPool(允许创建的线程数为最大INT值)
工作中我们都不用,我们用自定义的
配置线程池的参数:
CPU密集型:CPU核数+1个线程
IO密集型:CPU核数*2
各种锁
乐观锁(CAS):
线程比较乐观,默认不会产生并发,因此更新数据的时候不会加锁,而是在更新数据的时候确认下预期的数据和当前的数据是否一样,是的话进行操作,不是的话重来
悲观锁:
线程比较悲观,默认会产生并发,因此更新数据的时候会先加锁再进行操作,缺点是效率低下,会产生额外的资源消耗
公平锁:
先来后到,类似于排队打饭,线程谁先来谁先操作
非公平锁:
线程上来先尝试占有名额,如果失败,再改为公平锁模式,优点在于吞吐量比公平锁大,synchronized和Reentrantlock默认都是非公平锁
可重入锁(递归锁):
在同一个线程外层函数获得锁之后,在进入内层方法时会自动获得锁。synchronized和Reentrantlock默认都是可重入锁,目的是防止死锁。
自旋锁:
线程一直循环尝试获取锁,优点是减少了线程上下文切换的消耗,缺点是会产生额外的CPU消耗
独占锁(写锁)/共享锁(读锁)/互斥锁:
独占锁指的是一个锁一次只能被一个线程持有,用于写数据。
共享锁指的是多个线程可以共享一个锁,用于读数据。
读写锁是指比如签名时,允许多人同时观看表上的名字,但是只能有一个人去签名。
CopyOnWriteLock、AtomicInteger
CopyOnWriteLock:
和单词描述的一样,他的实现就是写时复制, 在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。
AtomicInteger:
底层使用的是unsafe的相关方法,在Unsafe的上面会发现,有一行注释叫做Unsafe.compareAndSwapInt,用的是CAS。
对于jdk1.8的并发包来说,底层基本上就是通过Usafe和CAS机制来实现的。有好处也肯定有一个坏处。从好的方面来讲,就是上面AtomicInteger类可以保持其原子性。但是从坏的方面来看,Usafe因为直接操作的底层地址,肯定不是那么安全,而且CAS机制也伴随着大量的问题,比如说有名的ABA问题等等。
CountDownLanch、CyclicBarrier、Semphore
CountDownLatch(火箭发射倒计时):
它允许一个或者多个线程一直等待,直到其他线程的操作执行完成后再执行,例如应用程序的主线程希望在负责框架服务的线程已经启动所有的服务之后再执行。
原理:
当一个或多个线程调用await()方法时,调用线程会被阻塞,其他线程调用countDown()方法会将计数器减一,当计数器的值变为0时,因调用await()方法被阻塞的线程才会被唤醒,继续执行。
CyclicBarrier(集齐七颗龙珠召唤神龙)
可循环屏障,让一组线程到达一个屏障,直到最后一个线程到达时,屏障才会开门,大家才一起开始运行
Semphore(信号量)(抢车位):
信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个是用于并发线程数的控制。
阻塞队列
种类:
ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
LinkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue
SynchronousQueue是一个不存储元素的阻塞队列,灭个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于前面二者
作用:
用于生产者消费者模式,类似于面包店卖面包
用于线程池
用于消息中间件
死锁
定义:
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
死锁的条件
互斥、循环等待、不可剥夺、请求保持
死锁的避免
死锁避免的基本思想:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。
死锁的预防
1、破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时,便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
2、破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
3、破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
示例
package com.jian8.juc.thread;
import java.util.concurrent.TimeUnit;
/**
* 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,
*/
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldThread(lockA,lockB),"Thread-AAA").start();
new Thread(new HoldThread(lockB,lockA),"Thread-BBB").start();
}
}
class HoldThread implements Runnable {
private String lockA;
private String lockB;
public HoldThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockA + "\t尝试获得:" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockB + "\t尝试获得:" + lockA);
}
}
}
}
解决方法:
1、使用jps -l定位进程号
2、jstack 进程号找到死锁查看