什么是并发编程?
并行:在同一个时间节点上,多个线程同时执行(是真正意义上的同时执行)
并发:一个时间段内,多个线程依次执行。
并发编程:在例如买票、抢购、秒杀等等场景下,有大量的请求访问同一个资源。会出现线程安全的问题,所以需要通过编程来解决多个线程依次访问资源,称为并发编程。
并发编程的根本原因:
- 多核cpu的出现,真正意义上可以做到并行执行
- java内存模型(JMM)
java内存模型,规范了Java虚拟机与计算机内存是如何协同工作的。
将内存分为主内存和工作内存。两个线程同时操作,会导致出错,本质原因在于内存模型设计。
共享数据存储在主内存中,每个线程都有各自的工作内存。操作共享数据时,会将主内存中的数据复制一份到工作内存中操作,操作完成后,再写回到主内存中。
但是一旦两个线程同时进行操作,读取共享数据,两个线程各自在工作内存中修改后,同时又写到主内存,这样就会与预期的结果不同。(AB两个线程同时操作变量n)
一、并发编程核心问题
由于java内存模型的设计,多线程操作一些共享的数据时,出现以下3个问题:
(1)不可见性:A线程在工作内存中操作共享数据时,B线程不知道A线程已经修改了数据。
(2)无序性:为了优化性能,有时候会改变程序中语句的先后顺序,以提高速度。
int a = 10;
io.read();//从其他地方读数据
int b = 5;
int c=a+b;
但是为了优化,第2行需要从其他地方读数据 需要时间;系统可能将3行代码乱序执行,例如 1、3、2的顺序执行。
有时,看似没有关系的代码乱序执行,可能会对后面的代码产生影响。
(3)非原子性:
一个或多个操作在CPU执行的过程中不被中断的特性,我们称为原子性。 原子性是拒绝多线程交叉操作的,同一时刻只能有一个线程来对它进行操作。
高级语言里一条语句往往需要多条CPU指令完成。如 count++,至少需要三条CPU指令。
- 首先,需要把变量 count 从主内存加载到工作内存;
- 之后,在工作内存执行 +1 操作;
- 最后,将结果写入主内存;
解决办法:
- 让不可见变为可见
- 让无序变为有序
- 非原子执行变为原子(加锁),由于线程切换执行导致
缓存(工作内存) 带来了不可见性;指令重排优化带来了无序性;线程切换带来了非原子性。
volatile可以解决前两个问题,加锁可以解决所有问题。
二、volatile关键字
volatile修饰的共享变量(类的成员变量、类的静态成员变量),被一个线程修改后,可以同步更新到其他线程,让其他线程中立即可见。volatile修饰的共享变量,指令是有顺序的。
但是volatile不能解决原子性问题,原子性问题由于线程切换执行导致。
volatile底层实现原理:
使用内存屏障(指令)进行控制。
- 有序性实现:volatile修饰的变量,在操作前添加内存屏障,来禁止指令重排序。
- 可见性实现:volatile修饰的变量添加内存屏障之外,还通过缓存一致性协议(MESI)将数据写回到主内存,其他工作内存嗅探后,如果自己工作内存中的数据过期,重新从主内存读取最新的数据。
三、如何保证原子性
同一时刻只有一个线程执行,称之为互斥。如果我们能够保证对共享变量的修改是互斥的,那么就能保证原子性了。
1、锁
只有通过加锁的方式,让线程互斥执行,来保证一次只有一个线程对共享资源进行访问。
synchronized:关键字;修饰代码块、方法;自动获取锁,自动释放锁
ReentrantLock:类;只能对某段代码修饰;需要手动加锁,手动释放锁
2、原子变量
在java中还提供一些原子类,在低并发情况下使用,是一种无锁实现。
JUC(java.util.concurrent包)中,里面的locks包和atomic包,它们可以解决原子性问题。
1.原子类原理(AtomicInteger 为例)
原子类的原子性是通过volatile+CAS实现原子操作的。
低并发情况下:使用原子类 AtomicInteger,底层有一个变量通过volatile关键字修饰的,结合CAS机制实现。
2.CAS(重点)
采用CAS机制(Compare-And-Swap比较并交换),是一种无锁实现,在低并发情况下使用。CAS是乐观锁的方式,采用的是自旋的思想。
采用自旋思想:
(1)第一次从内存中读到内存值V
(2)对数据进行修改,将改变后的值写入到内存时,需要重新读取内存中最新的值,作为预期值A
(3)在写入前比较预期值与内存值,看是否一致:
- 如果一致,说明其他线程没有修改内存中的值,将更新后的值,写入到内存;
- 如果不一致,说明其他线程修改了主内存中的值,就需要重新计算变量值,反复这一过程。--->自旋
优点:
- 不加锁,所有的线程都可以对共享数据操作;
- 适合低并发使用,因为所有线程不会进入阻塞状态
缺点:
- 大并发时,不停自旋判断,导致cpu占用率高
3.ABA问题
ABA问题,即线程1读取到内存值,线程2将内存值由A改为了B,再由B改为了A。当线程1去判断时,预期值与内存值相同,无法分辨内存值是否发生过变化。
通过设置版本号,每次操作改变版本号,来避免ABA问题。如原先的内存值为(A,1),线程修改为(B,2),再修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号1和3,即可发现该主内存中的数据被更新过了。
四、java中的锁
一些锁的名称指的是锁的特性、设计、状态,并不是都是锁。
1、乐观锁/悲观锁
乐观锁:没有加锁,不加锁的方式是没有问题的。例如CAS机制
悲观锁:必须加锁。悲观的认为,不加锁的并发操作一定会出问题。
2、可重入锁
synchronized和ReentrantLock是可重入锁,可以避免死锁。
A方法和B方法是两个同步方法,在同一个类中,用同一把锁,先进入到同步方法A中,锁被使用,在方法A调用方法B依然可以进入到方法B。(此时方法A还没有释放锁)
如果不是可重入锁的话,方法B不会被当前线程执行。
3、读写锁
ReentrantReadWriteLock,里面有一个读锁和写锁。
- 读读不互斥:只有读没有写,可以多个线程同时读
- 读写互斥:一旦有写操作,读写不同同时进行。
- 写写互斥:多个写互斥
4、分段锁
不是锁,是一种锁实现思想:用于将数据分段,并在每个分段上都会单独加锁,以提高并发效率。
举例:Hashtable是将整合hash表格锁住了,一次只能有一个线程操作并发量低,效率低。
ConcurrentHashMap将每个哈希位置当做一个锁,可以有多个线程对map进行操作,一次只能有一个线程操作一个位置.
5、自旋锁
不是锁。是自己重试,当线程抢锁失败后,重试几次,如果抢到锁了就继续,如果抢不到就阻塞线程。
6、共享锁/独占锁
共享锁:一个锁可被多个线程共享,例如读写锁中的 读锁。
独占锁:一次只能有一个线程操作。例如:Synchronized、ReentrantLock,读写锁中的 写锁。
7、公平锁/非公平锁
公平锁:按照请求的顺序执行(排队,先来来执行)。
非公平锁:不按照请求顺序执行,谁先抢到谁先执行。
synchronized是一种非公平锁。ReentrantLock默认是非公平锁,但是底层可以通过AQS来实现线程调度,使其变成公平锁。
五、synchronized锁
1、锁的状态
在synchronized锁的底层实现中,提供4种锁的状态,又来区别对待。(锁的状态在同步锁对象的对象头中,有一个区域叫Mark Word中存储)
- 无锁状态:没有线程进入。
- 偏向锁:始终只有一个线程访问同步代码快,记录线程的编号,快速的获取锁。
- 轻量级锁:当锁状态为偏向锁时,还有其他线程访问,此时升级为轻量级锁。特点:当一个线程获取锁之后,其他线程不会阻塞,会通过自旋方式获取锁,提高效率。
- 重量级锁:当锁的状态为轻量级锁时,线程自旋达到一定的次数,还没有获取到锁,就会进入到阻塞状态,锁状态升级为重量级锁,等待操作系统调度。
2、对象结构
在Hotspot虚拟机中,对象在内存中分为三块区域:对象头、实例数据和对齐填充;synchronized使用的锁对象是存储在对象头里。
对象头中有一块为Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。
32位操作系统Mark Word为32bit,64 位操作系统Mark Word为64bit。下面就是对象头的一些信息:
3、synchronized锁实现
synchronized锁是依赖底层编译后的指令,添加锁的监视器实现,需要我们提供一个同步对象,来记录是否加锁、以及锁的状态。
六、AQS
全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。抽象同步队列,是java代码实现线程同步非常重要的一个底层实现类。
思路:
- 在类中定义了一个state变量(初始化为0,表示有没有线程访问共享资源)和一个双向链表队列(head结点代表当前占用的线程)。
- 有线程访问时,第一个抢到执行权的线程放在头节点,将state加1。期间如果有其他的线程访问时,如果state=1,将其他线程添加到队列中,等待锁的释放。
state由于是多线程共享变量,所以定义成volatile,以保证state的可见性,但不能保证原子性,所以AQS提供了对state的原子操作方法,保证了线程安全。
队列由Node对象组成,Node是AQS中的内部类。
AQS 的锁模式分为:独占和共享
独占锁:每次只能有一个线程持有锁,比如ReentrantLock是以独占方式实现的。
共享锁:允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
ReentrantLock锁实现
ReentrantLock是java.util.concurrent.locks包下的类,实现Lock接口。
public class ReentrantLock implements Lock, java.io.Serializable{ }
ReentrantLock基于AQS,在并发编程中可以实现公平锁和非公平锁来对共享资源进行同步。ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
ReentrantLock构造方法:
- 无参构造方法默认是非公平实现
- 有参构造方法可以选择,true—公平实现,false—非公平实现
NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法。
static final class NonfairSync extends Sync {
//若通过 CAS 设置变量 state 成功,就是获取锁成功,则将当前线程设置为独占线程。
//若通过 CAS 设置变量 state 失败,就是获取锁失败,则进入 acquire 方法进行后续处理。
final void lock() {
if (compareAndSetState(0, 1))//每个线程进入到lock方法时,会尝试获取锁,有可能获取到了
setExclusiveOwnerThread(Thread.currentThread());
else//获取不到,将线程添加到队列中,排队获取锁
acquire(1);
}
//尝试获取锁,无论是否获得都立即返回
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法。
static final class FairSync extends Sync {
final void lock() {//公平锁,默认排队获取锁
acquire(1);
}
}
七、JUC常用类
1、ConcurrentHashMap
HashMap是线程不安全的,不能在多线程环境下使用
Hashtable是线程安全的,但是synchronized直接锁住的是整个方法,效率低(public synchronized V put(K key,V value{}))
ConcurrentHashMap是线程安全的,效率高于Hashtable。
不像Hashtable将整个方法锁起来,将每个位置的第一个节点当做锁对象,将锁的力度减小,进而提高了效率;同时可以有多个线程对ConcurrentHashMap进行操作,如果多个线程操作的是同一个位置,那么必须等待,因为用的是同一把锁。当算出的位置,第一个节点为null时,采用CAS机制添加。
Hashtable和ConcurrentHashMap不支持存储null键和null值。源码中看到为null,就报空指针异常。为什么这样设计呢?
为了消除歧义,因为无法分辨key的值为null还是key不存在返回的null,这在多线程里面是模糊不清的,所以压根就不让 put null。
2、CopyOnWriteArrayList
ArraayList是线程不安全的,在高并发情况下可能会出现问题;
Vector是线程安全的,get、add方法都加锁,读读都互斥,效率低。
CopyOnWriteArrayList在读的时候不加锁,写入也不会阻塞读取操作,只有同时写入和写入之间需要进行同步等待,提高了读的效率。
CopyOnWriteArrayList在进行add、set等修改操作时,是通过底层数组的副本实现的。先将底层数组进行复制,修改复制出来的数组,修改后将数据赋值给原来的底层数组。写入时,不影响其他线程读
3、CopyOnWriteArraySet
CopyOnWriteArraySet线程安全的,底层使用的是CopyOnWriteArrayList不能存储重复数据
4、辅助类 CountDownLatch
CountDownLatch允许一个线程 等待其他线程各自执行完毕后再执行。底层实现是通AQS来完成的,创建CountDownLatch对象时指定一个初始值(线程的数量)。每当一个线程执行完毕后,AQS内部的state就-1,当state的值为0时,表示所有线程都执行完毕,然后等待的线程就可以恢复工作了。
八、对象引用
在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为:
- 强引用
- 软引用(SoftReference)
- 弱引用(WeakReference)
- 虚引用(PhantomReference)
这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。
1、强引用(不是垃圾)
有引用指向该对象,Object obj = new Object(); 这种情况下new出来的对象不能被垃圾回收的。
软引用、弱引用、虚引用都是用来标记对象的一种状态。当一些对象称为垃圾后,通过不同的状态来判断什么时候被清理。可以继承SoftReference、WeakReference、PhantomReference或者把自己的对象添加到软、弱、虚的对象中。
2、软引用(内存不足时回收)
被软引用关联的对象,被判定为垃圾时,可以不用立即回收;直到垃圾回收后内存仍然不够用时,才会回收软引用关联的对象。
Object obj = new Object();// 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用
3、弱引用(发现时回收)
弱引用管理的对象,只能存活到下一次垃圾回收。
4、虚引用(对象回收跟踪)
最弱的引用,对对象的生命周期没有任何的影响,跟踪对象是否被回收(如果对象被回收后,会给队列返回信息)
Object obj = new Object();
ReferenceQueue phantomQueue = new ReferenceQueue();//声明引用队列
PhantomReference<Object> sf = new PhantomReference<>(obj,phantomQueue);//声明虚引用(还需要传入引用队列),如果对象被回收后,会给队列返回信息
obj = null;
九、线程池
1、池的概念
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,频繁创建线程和销毁线程需要时间。 可以事先创建出一些连接对象,每次使用时,从集合中直接获取,用完不销毁。减少频繁创建、销毁。
在 JDK5 版本中增加了内置线程池实现 ThreadPoolExecutor,同时提供了Executors来创建不同类型的线程池。
池的好处:减少频繁创建销毁时间,统一管理线程,提高速度。
2、ThreadPoolExecutor类
Java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。
ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,但是前三个构造器都是调用的第四个构造器进行的初始化工作。
3、构造器中各个参数的含义
1.corePoolSize
核心池的大小,一旦创建不会被销毁的;非核心池中的线程,在没有被使用时,可以被回收。
2.maximumPoolSize
线程池最大线程数量,包含核心池中的数量。
3.keepAliveTime
非核心线程池中的线程,在不被使用后,多久就终止。(假如核心线程池5个,最大数量10,但是任务少的情况下,核心线程池够用了,等多长时间,就把非核心线程池中的线程终止)
4.unit
为keepAliveTime设置时间单位,有7种取值。
5.workQueue
一个阻塞队列,用来存储执行的任务。有以下工作队列:
- ArrayBlockingQueue:数组实现的有界阻塞队列,创建时必须设置长度,按FIFO排序。
- LinkedBlockingQueue:链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置是一个最大长度为 Integer.MAX_VALUE;
6.threadFactory
创建线程的工厂。
7.handler
拒绝策略。当线程池中的核心池、阻塞队列、非核心池已满时,如果有任务继续到达,如何执行。有以下四种拒绝策略:
- AbortPolicy();直接抛出异常,拒绝执行。
- CallerRunsPolicy();交由当前提交任务的线程执行(如果任务被拒绝了,则由提交任务的线程(例如:main)直接执行此任务)
- DiscardOldestPolicy();丢弃等待时间最长的任务。
- DiscardPolicy();直接丢弃,不执行。
4、线程池的执行
创建完成ThreadPoolExecutor之后,当向线程池提交任务时,通常使用execute方法。 execute方法的执行流程图如下:
当请求到来时,如果核心线程池没有满,就提交到核心线程池,如果核心线程池已满,则添加到队列中(前提是队列没有满);如果队列中已满,则在非核心线程中创建线程,直到到达最大线程数量;如果非核心线程池也已经满了,那么则使用适当的拒绝策略处理。
execute与submit的区别
- execute() 提交任务,没有返回值
- submit() 提交任务,可以有返回值(任务需要实现callable接口)
关闭线程池
- shutdownNow() 直接关闭,对还未开始执行的任务全部取消
- shutdown() 等待任务执行完关闭
//任务
public class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":task "+taskNum+"执行完毕");
}
}
public class Test {
public static void main(String[] args) {
//创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
5, 200,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
executor.prestartAllCoreThreads();
for(int i=1;i<=8;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);//添加任务到线程池
//Future<?> submit = executor.submit(myTask);
//submit.get();//返回值
}
executor.shutdown();
}
}
十、ThreadLocal
本地线程变量,可以为每个线程都创建一个属于自己的变量副本,使得多个线程之间隔离,不影响。(在每一个线程里都有一个自己的localNum)
package com.ffyc.javapro.thread.threadlocal;
public class ThreadLocalDemo {
//创建一个ThreadLocal对象,复制保用来为每个线程会存一份变量,实现线程封闭
private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
localNum.set(1);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
localNum.set(localNum.get()+10);
System.out.println(Thread.currentThread().getName()+":"+localNum.get());//11
}
}.start();
new Thread(){
@Override
public void run() {
localNum.set(3);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
localNum.set(localNum.get()+20);
System.out.println(Thread.currentThread().getName()+":"+localNum.get());//23
}
}.start();
System.out.println(Thread.currentThread().getName()+":"+localNum.get());//0(main线程)
}
}
ThreadLocal底层实现:
在一个线程中使用ThreadLocal时,为每个当前线程创建了一个ThreadLocalMap,看似用唯一的ThreadLocal对象作为键,其实每个线程中都有一个属于自己的ThreadLocalMap,所以每个线程中都有一个自己的变量副本。
ThreadLocal会造成内存泄漏:
由于ThreadLocal被弱引用关联,有可能在下一次垃圾回收时被回收掉,会导致key为null,而value还存在着强引用。但是value却被Entry对象关联,Entry又被ThreadLocalMap关联,ThreadLocalMap又被Thread关联,要是当前线程长期不结束,value就不能被销毁,但是key有可能已被回收,就获取不到value造成内存泄漏。
正确的使用:不再使用这个本地线程变量后,将其主动删除掉,调用remove方法删除。