- 多线程
- 并发与并行
- 多线程的本质问题
- volatile——解决可见性和有序性
- volatile底层实现原理
- 锁和原子变量——解决原子性
- 原子变量——CAS
- VAB
- CAS的缺点
- Java中的锁
- synchronized中锁的状态
- 对象结构(记录锁)
- 锁的实现
- AQS(代码层面)
- ReentrantLock锁的实现
- NoFairSync 非公平锁(默认的锁)
- FairSync公平锁
- synchronized锁的实现(指令级)
- JUC常用类(线程安全且高效的集合)
- ConcurrentHashMap (线程安全且高效的HashMap)
- CopyOnWriteArrayList / CopyOnWriteSet
- 线程池
- ThreadPoolExecutor
- corePoolSize 核心线程池大小
- maximumPoolSize 线程池最大线程数
- keepAliveTime 非核心线程池无任务时存活时间
- unit 存活时间的单位
- workQueue 阻塞队列 核心线程池满了后先进入阻塞队列
- thredFactory 线程工厂,用于创建线程
- hander 拒绝策略
- 流程
- 线程池中的阻塞队列
- 线程池中的拒绝策略
- 提交与关闭任务
Java语言是支持多线程的,多线程技术使程序的响应速度更快 ,可以在进行其它工作的同时一直处于活动状态。实现多线程的方式主要有三种:继承Thread类、实现Runable接口、继承Callable接口。
并发与并行 多线程共享数据
并发:说的是在一个时间段内,多件事情交替执行,宏观上的同时执行。
并行:说的是多个事情在同一时间段内同时执行,微观上的同时执行。
并发编程是在很多线程对共享资源进行访问时,需要通过控制,让多个线程并发的对共享数据进行访问。
多线程的本质问题由于CPU、内存、硬盘三者之间的读写速度不一样。所以导致
- 多核CPu,每个内核中都有一层高速缓存。每个高速缓存中存储的数据不可见(可见性)
- 线程中有IO操作,耗时较长,操作系统需要切换线程执行(原子性)
- 操作系统对指令进行优化,打乱了指令的执行顺序(有序性)
volatile修饰变量,它保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变 量的值,这新值对其他线程来说是立即可见的。还禁止进行指令重排序。 但是volatile不能解决原子性问题。
volatile底层实现原理使用 Memory Barrier 内存屏障 ,禁止在该条指令执行前插入其他指令,在工作内存修改后,结合缓存一致性协议,将工作内存数据更新到主内存,其他工作内存读取更新。
内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指 令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其 后面的指令移到内存屏障指令之前
有序性:
有序性实现:主要通过对 volatile 修饰的变量的读写操作前后加上各种特定 的内存屏障来禁止指令重排序来保障有序性的。
可见性:
锁和原子变量——解决原子性 原子变量——CAS可见性实现:主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现 的。对 volatiile 修饰的变量执行写操作时,JVM
会发送一个 Lock 前缀指令给 CPU,CPU 在执行完写操作后,会立即将新值刷新到内存,同时因为 MESI 缓 存一致性协议,其他各个
CPU 都会对总线嗅探,看自己本地缓存中的数据是否 被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个 CPU
里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见 性
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不知道内存值已经发生过修改的。
如何解决?添加版本号
Java中的锁通过使用类添加版本号,来避免 ABA 问题。如原先 的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改 为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较, 只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。
不完全是锁,有的是锁状态,有的是锁的特性
- 乐观锁 采用CAS机制,乐观认为不加锁是没有问题的. 原子类
- 悲观锁 就是真正的加锁实现,认为不加锁是会有问题的.
- 可重入锁 又名递归锁,当一个线程进行入外层方法获取锁时,如果内存调用另一个需要获得该锁修饰的方法,那么线程是可以进入的,synchronized就是一个可重入锁。
- 分段锁 不是具体的锁, 将锁的粒度分的更小,以提高并发效率.
- 自旋锁 不断重试去尝试获得锁,不会让线程进入阻塞状态,提高效率,但是耗cpu.
- 读写锁 里面维护两个锁实现, 一个是写锁,一个读锁。如果使用的是写锁,一次只能有一个线程获得锁,如果是读锁,那么可以允许多个线程获得。写锁的优先级高于读锁。
- 独占锁 / 共享锁 类似于读写锁。
- **公平锁 ** 可以按照请求的顺序分配锁, ReentrantLock中有公平锁实现,里面维护一个队列,按顺序排队获取锁。
- 非公平锁 不按照请求顺序分配锁,synchroized就是非公的,ReentrantLock中默认使用非公平锁,非公平锁的线程进来后会先尝试获取锁的状态,如果是0,那么将其++,进入代码块中,其他线程进入阻塞队列中等待。
锁的状态是通过对象监视器在对象头中的字段来表明的。
- 无锁状态: 没有加锁
- 偏向锁: 只有一段线程访问同步代码,此时会将线程的id存入对象头中,下次线程来获取锁时直接分配即可。
- 轻量级锁: 当锁的状态为偏向锁时,又有线程访问,那么锁的状态会升级为轻量级锁,不会让线程进入阻塞状态,而是自旋尝试获取锁,以提高效率。
- 重量级锁: 当锁的状态为轻量级锁时,如果线程数量太多,线程自旋数达到一定数量,锁会升级为重量级锁,线程进入阻塞状态,有操作系统调度分配。
对象在内存中的布局分为三块区域:对象头、实例 数据和对齐填充
synchronized需要一个对象,在对象头中记录有没有使用锁
锁的实现 AQS(代码层面) AQS是juc( java.util.concurrent java并发包)中实现线程安全的核心组件,是从java代码级别实现.
内部维护了一个锁状态(0没有,>0有锁)volatile state。 内部还维护一个队列,保存未获取到锁的线程.
ReentrantLock锁的实现多个线程来访问,如果有一个线程访问到了state,就将其改为1,其他线程获取失败后,就会添加到队列中 Node(Thread)
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();
}
这是对源码的翻译
而公平锁就比较老实,线程进来先排队,不然不予受理。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
synchronized锁的实现(指令级)
synchronized锁主要依靠底层指令实现,它是关键字,可以修饰方法。代码块,一次只允许一个线程进入。
指令级别实现: 如果synchronized修饰方法,在编译后的指令中添加ACC_SYNCHRONIZED表示此方法是同步方法,有线程进入后其他线程不能进入,在对象头中锁标志+1,方法运行结束,或者出现异常,锁标志-1。 synchroniezd如果修饰代码块: 在进入代码前加入monitorenter指令 ,对象锁标志+1, 同步代码块结束/或者出现异常执行monitorexit,锁标志-1。
首先,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(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 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 pred = e;
if ((e = e.next) == null) {
pred.next = new Node(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类事先线程池的创建(推荐)。
线程池的优点:
- 重复利用线程。
- 统一管理。
- 提高响应速度。
七个参数
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:没有执行完的线程全部执行玩,然后停止,期间不接受新任务。



