目录
概念、理论
并发:多个线程操作相同的资源,优点:效率高、资源利用率高,缺点:线程可能不安全、数据可能不一致,需要使用一些方式保证线程安全、数据一致
高并发:服务器能同时处理大量请求
线程安全:当多个线程访问某个类,不管采用何种调度方式、线程如何交替执行,这个类都能表现出正确的行为。
造成线程不安全的原因
- 存在共享资源
- 多个线程同时操作同一共享资源,操做不具有原子性
如何实现线程安全?
- 使多线程不同时操作同一共享资源:eg. 只使用单线程、必要的部分加锁、使用juc的并发容器、并发工具类
- 使对共享资源的操作具有原子性:eg.使用原子类
- 不共享资源:eg. 使用ThreadLocal
- 用final修饰共享资源,使之只读、不可修改
只要实现以上任意一点,即可实现线程安全
互斥锁的特性
- 互斥性:同一时刻只能有1个线程对这部分数据进行操作,互斥性也常叫做操作的原子性
- 可见性:如果多个线程同时操作相同的数据(读、写),对数据做的修改能及时被其它线程观测到。可见性用happens-before原则保证
锁的实现原理
获取锁:把主内存中对应的共享资源读取到本地内存中,将主内存中的该部分共享资源置为无效
释放锁:把本地内存中的资源刷到主内存中,作为共享资源,把本地内存中的该部分资源置为无效
juc包简介
juc包提供了大量的支持并发的类,包括
- 线程池executor
- 锁locks,locks包及juc下一些常用类CountDownLatch、Semaphore基于AQS实现。jdk将同步的通用操作封装在抽象类AbstractQueuedSynchronizer中,acquire()获取资源的独占权(获取锁),release()释放资源的独占权(释放锁)
- 原子类atomic,atomic包基于CAS实现,实现了多线程下无锁操作
- 并发容器(集合)collections
- 并发工具类tools
实现线程安全的常用方式
synchronized
synchronized的用法
// 修饰普通方法
public synchronized void a(){
}
// 修饰静态方法
public static synchronized void b(){
}
public static Object lock = new Object();
public void c(){
// 修饰代码块。同步代码块,锁住一个对象
synchronized (lock){
}
}
synchronized可以修饰方法、代码块,修饰的操作是原子性的,同一时刻只能有1个线程访问、执行
- 修饰普通方法,执行该方法时会自动锁住该方法所在的对象
- 修饰静态方法,加的是类锁,执行该方法时会锁住所在类的class对象,即锁住该类所有实例
- 修饰代码块,加的是对象锁,会锁住指定对象
如果要修饰方法,尽量用普通方法,因为静态方法因为会锁住类所有的实例,严重影响效率。
synchronized的实现原理
synchronized使用对象作为锁,对象在内存的布局分为3部分:对象头、实例数据、对齐填充,对象头占64位
- 前32位是Mark Word,存储对象的hashCode、gc分代年龄、锁类型、锁标志位等信息
- 后32位是类型指针,存储对象所属的类的元数据的引用,jvm通过类型指针确定此对象是哪个类的实例
Mark Work结构如下
每个对象都关联了一个Monitor(这也是为什么每个对象都可以作为锁的原因),锁的指针指向对象对应的Monitor,当某个线程持有锁时,Monitor处于锁定状态
synchronized的4种锁状态及膨胀方向
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 无锁:没有线程要获取锁,未加锁
- 偏向锁:大多数情况下,锁不存在多线程竞争,很多时候都是同一线程多次申请锁。偏向锁简化了线程再次申请锁的流程,减少了同一线程多次获取同一个锁的代价。偏向锁只适用于锁竞争不激烈的情况
- 轻量级锁:适用于锁竞争一般的情况
- 重量级锁:适用于锁竞争激烈的情况
使用Lock接口
synchronized使用前自动加锁、使用完自动释放锁,很方便。synchronized是悲观锁的实现,每次操作共享资源前都要先加锁;以前是重量级锁,性能低,经过不断优化,量级轻了很多,性能和Lock相比差距不再很大。
Lock需要自己加锁、用完需要自己释放。Lock是乐观锁的实现,每次先操作共享资源,提交修改时再验证共享资源是否被其它线程修改过;Lock是轻量级锁,性能很高。
Lock接口有很多实现类,常用的有ReentrantLock 可重入锁、ReadWriteLock 读写锁,也可以自己实现Lock接口来实现自定义的锁。
ReentrantLock 可重入锁
重入:一个线程再次获取自己已持有的锁
public class Xxx{
public final static ReentrantLock lock=new ReentrantLock(); //锁对象都可以加个final防止被修改
//public final static ReentrantLock lock=new ReentrantLock(true); //可指定是否是公平锁,缺省时默认false
public void a() {
lock.lock(); //获取锁,如果未获取到锁,会一直阻塞在这里
// lock.tryLock(); //只尝试1次,如果未获取到锁,直接失败不执行后面的代码
//.... //操作共享资源
lock.unlock(); //释放锁
}
public void b() {
try {
lock.tryLock(30, TimeUnit.SECONDS); //如果获取锁失败,会在指定时间内不停尝试。此句代码可能会抛出异常
//.... //操作共享资源
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if (!lock.isFair()){
lock.unlock(); //如果获取到锁,最终要释放锁
}
}
}
public void c() {
lock.lock();
try {
//.... //操作共享资源
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock(); //如果操作共享资源时可能发生异常,最终要释放锁
}
}
}
ReentrantLock如何实现公平锁、非公平锁?
使用链表存储等待同一把锁的线程,将线程添加到链表尾部,释放锁后
- 公平锁:将锁分配给链表头部的线程
- 非公平锁:将锁分配个链表中的任意一个线程
将获得锁的线程从链表中移出
synchronized、ReentrantLock的比较
- synchronized是关键字,ReentrantLock是类
- 机制不同,synchronized是操作对象的Mark Word,ReentrantLock是使用Unsafe类的park()方法加锁
- synchronized是非公平锁,ReentrantLock可以设置是否是公平锁
- ReentrantLock可以实现比synchronized更细粒度的控制,比如设置锁的公平性
- 锁竞争不激烈时,synchronized的性能往往要比ReentrantLock高;锁竞争激烈时,synchronized膨胀为重量级锁,性能不如ReentrantLock
- ReentrantLock可以设置获取锁的等待时间,避免死锁
ReadWriteLock 读写锁
ReadWriteLock将锁细粒度化分为读锁、写锁,synchronized、ReentrantLock 同一时刻最多只能有1个线程获取到锁,读锁同一时刻可以有多个线程获取锁,但都只能进行读操作,写锁同一时刻最多只能有1个线程获取锁进行写操作,其它线程不能进行读写操作。
读写锁做了更加细致的权限划分,加读锁时多个线程可以同时对共享资源进行读操作,相比于synchronized、ReentrantLock,在以读为主的情况下可以提高性能。
ReadWriteLock是接口,常用的实现类是ReentrantReadWriteLock 可重入读写锁。
public class Xxx {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); //从读写锁获取读锁
public static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //从读写锁获取写锁
//.....
public void a(){
//....
readLock.lock();
//..... 操作共享资源
readLock.unlock();
//....
}
}
读锁、写锁的操作方式和ReentrantLock完全相同,都可以设置超时,这3种锁都是可重入锁
锁降级
在获取写锁后,写锁可以降级为读锁
public class Xxx {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); //读锁
public static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //写锁
//.....
public void a(){
//....
writeLock.lock(); //获取写锁
//..... 对共享资源进行写操作
readLock.lock(); //获取读锁(仍然持有写锁)
writeLock.unlock(); //释放写锁(只持有读锁,写锁->读锁,锁降级)
//..... //对共享资源进行读操作
readLock.unlock(); //释放读锁
//....
}
}
- 锁降级后,线程仍然持有写锁,需要自己释放写锁
- 锁降级的意义在于:后续对共享资源只进行读操作,及时释放写锁可以让其它线程也能获取到读锁、进行读操作
- 锁降级的应用场景:对数据比较敏感,在修改数据之后,需要校验数据
- 写锁可以降级为读锁,但读锁不能升级为写锁
AQS如何用int值表示读写状态
AbstractQueuedSynchronizer,抽象类
int,4字节32位,高位(前16位)表示读锁状态,低位(后16位)表示写锁状态。状态指的是重入次数,最大为2^16-1=65536
StampedLock
StampedLock是jdk1.8新增的类,可以获取读写锁、读锁、写锁,可以选择悲观锁、乐观锁,但StampedLock是不可重入的,且API比其他方式复杂,使用难度稍高。
ThreadLocal
ThreadLocal维护了一个map,这个map中存储的数据是当前线程独有的。ThreadLocal可以保证各个线程的数据互不干扰,并发场景下可以实现无状态调用,适用于各个线程依赖不同的变量值完成操作的场景。
public class Xxx {
private static ThreadLocal<Integer> i = ThreadLocal.withInitial(() -> 100); //必须要初始化值
public void a() {
i.set(20); //设置值
Integer value = i.get(); //获取值
i.remove(); //移出set()赋的值,重置为初始化时的值,即100
}
}
volatile
volatile的使用
public static volatile boolean flag = true; //禁止对此变量进行指令重排序
volatile只能修饰变量,实现了该变量的可见性、可以禁止指令重排序,当该变量的被某个线程修改时会自动通知其它使用此变量的线程。
volatile只实现了可见性,没有实现原子性,严格来说并没有实现线程安全,一般只用于
- 作为开关 ,eg. while(flag){ }
- 在懒汉式单例中修饰对象实例,禁止指令重排序
volatile、synchronized的比较
原子类
i++、++i、i–、--i、+=、-=等操作都不是原子性的,juc的atomic包下的类提供了自增、自减、比较赋值、取值修改等原子性方法,可以线程安全地进行操作,因为类中的方法都是原子性的,所有叫做原子类。
public class Xxx {
public static AtomicInteger i = new AtomicInteger(0); //int
public static AtomicLong l = new AtomicLong(0); //long
public static AtomicBoolean b = new AtomicBoolean(false); //boolean
public static AtomicReference<User> user = new AtomicReference<>(new User()); //引用
public static AtomicIntegerArray intArr = new AtomicIntegerArray(new int[]{1, 23}); //int[ ]
public static AtomicLongArray longArr = new AtomicLongArray(new long[]{1, 23}); //long[ ]
public static AtomicIntegerFieldUpdater<User> userId1 = AtomicIntegerFieldUpdater.newUpdater(User.class,"id"); //对象的int型字段
public static AtomicLongFieldUpdater<User> userId2 = AtomicLongFieldUpdater.newUpdater(User.class,"id"); //对象的long型字段
public static AtomicReferenceFieldUpdater<User, List> userOrderList= AtomicReferenceFieldUpdater.newUpdater(User.class, List.class,"orderList"); //对象的引用型字段
}
- 原子类使用CAS实现乐观锁,并发支持好、效率高
- CAS提交修改失败时会while循环进行重试,如果重试时间过长,会给cpu带来很大开销
- 可能发生ABA问题。有2个原子类解决了ABA问题 :AtomicMarkableReference、AtomicStampedReference,使用标记、邮戳实现乐观锁,和版本号、时间戳机制差不多,避免了ABA问题。
- 只能保证单个变量的原子性,只能进行简单操作,如果要保证多个变量、稍微复杂点的操作的原子性,要用其它方式来实现线程安全(一般是加锁)
并发容器
Vector、Hashtable 的方法都使用synchronized修饰,是线程安全的,但缺点较多,基本不使用这2个类。
Collections.synchronizedXxx()可以将集合转换为同步集合,是使用synchronized锁住整个集合,效率低下,不推荐。
juc提供了常用的并发容器,使用CAS保证线程安全,效率高,常见的并发容器如下
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); //有序,按照插入顺序排列,内部使用Object[]存储元素
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>(); //无序,CopyOnWriteArraySet内部使用CopyOnWriteArrayList存储元素
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); //map
ConcurrentLinkedQueue<String> queue1 = new ConcurrentLinkedQueue<>(); //基于链表的队列
LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>(); //基于链表的阻塞队列,如果参数指定了元素个数,则有界、不能扩容,如果未指定,则无界
ArrayBlockingQueue<String> queue3 = new ArrayBlockingQueue<>(20); //基于数组的阻塞队列,指定容量,不能扩容(有界)
ArrayBlockingQueue<String> queue4 = new ArrayBlockingQueue<>(20,true); //可以指定是否是公平锁,默认false
阻塞指的是,在进行某些操作时,会阻塞线程
在生产者/消费者的线程协作模式中,常用阻塞队列LinkedBlockingQueue作为仓库
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); //基于链表的阻塞队列
//入队的3个方法
queue.offer(""); //返会操作结果,boolean,如果队列满了放不下,返回false
queue.add(""); //返会操作结果,boolean,如果队列满了放不下,会抛出异常
try {
queue.put(""); //如果队列满了,会阻塞线程,直到队列元素变少、可以放进去
} catch (InterruptedException e) {
e.printStackTrace();
}
//出队的3个方法
queue.poll(); //如果队列是空的,返回null
queue.remove(); //如果队列是空的,会抛出异常
try {
queue.take(); //在队列为空的时候,会阻塞线程,直到有元素可弹出
} catch (InterruptedException e) {
e.printStackTrace();
}
并发工具类
CountDownLatch
CountDownLatch是一个计数器,常用于等待某些线程执行完毕
CountDownLatch countDownLatch = new CountDownLatch(2); //指定次数
new Thread(()->{
//.....
countDownLatch.countDown(); //次数-1
}).start();
new Thread(()->{
//......
countDownLatch.countDown();
}).start();
try {
countDownLatch.await(); //阻塞当前线程,直到次数为0时才继续往下执行,即等待2个线程执行完毕
//......
} catch (InterruptedException e) {
e.printStackTrace();
}
CyclicBarrier 栅栏
CyclicBarrier cyclicBarrier = new CyclicBarrier(3); //指定await的线程数
new Thread(()->{
//......
try {
cyclicBarrier.await(); //第一个
//.....
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
//......
try {
cyclicBarrier.await(); //第二个
//.....
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
//.....
try {
cyclicBarrier.await(); //第三个
//.....
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
线程执行到await()处会阻塞,停下来,直到指定数量的线程都执行到await()才会继续往下执行。
CountDownLatch用于一些线程等待另一些线程执行完毕,类似超市收银员等待顾客挑好东西来结账;CyclicBarrier用于指定数量的线程互相等待,类似于大家指定地点集合。
Semaphore 信号量
Semaphore用于限流
Semaphore semaphore = new Semaphore(2); //指定信号量
// Semaphore semaphore = new Semaphore(2,true); //可指定是否使用公平锁,默认false
new Thread(() -> {
//......
try {
semaphore.acquire(); //使用1个信号量,信号量-1。如果信号量为0,没有可用的信号量,阻塞线程直到获取到信号量
//....
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); //操作完释放信号量,信号量+1
}
}).start();
Exchanger
交换机,用于2条线程之间交换数据,只能用于2条线程之间,即一个Exchanger对象只能被2条线程使用(成对)
Exchanger<String> stringExchanger = new Exchanger<>(); //泛型指定交换的数据类型
new Thread(()->{
try {
String data = stringExchanger.exchange("are you ok?");
System.out.println("线程1接收到的数据:" + data); //ok
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
String data = stringExchanger.exchange("ok");
System.out.println("线程2接收到的数据:" + data); //are you ok
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
单例与线程安全
单例有2种模式
- 饿汉式:在类加载时就实例化,线程安全
- 懒汉式:在需要使用实例时才实例化,可能是线程不安全的
饿汉式
//饿汉式
public class A {
private static A a=new A(); //用静态成员保存实例,调用构造方法创建实例。类加载时会初始化静态成员
//.....
private A(){ //构造方法私有化,隐藏掉
}
public static A getInstance(){ //把获取实例的方法暴露出去
return a; //只有1步,原子性,线程安全
}
//.....
}
懒汉式
//懒汉式 写法一
class A {
private static A a; //用静态成员保存实例
//.....
private A(){ //构造方法私有化,隐藏掉
}
public static A getInstance(){ //暴露获取实例的方法,多步,不具有原子性,不是线程安全的
if (null==a){
a = new A();
}
return a;
}
//.....
}
写法二:用synchronized修饰获取获取实例的静态方法,但这种方式获取实例时会锁住类,使多个线程不能同时获取实例,效率低下
//懒汉式 写法三
class A {
private static volatile A a; //volatile禁止指令重排序
//.....
private A(){
}
public static A getInstance(){
if (null==a){
synchronized (A.class){ //优化写法,只在创建实例时锁住类
a = new A();
}
}
return a;
}
//.....
}
锁的分类
-
自旋锁:未获取到锁时进入等待状态,多线程切换上下文会消耗系统资源,频繁切换上下文不值得,jvm会在线程没获取到锁时,暂时执行空循环等待获取锁,即自旋,循环次数即自旋次数;如果在指定自旋次数内没获取到锁,则挂起线程,切换上下文,执行其它线程。锁默认是自旋的。
-
自适应自旋锁:自旋次数不固定,由上一次获取该锁的自旋时间及锁持有者的状态决定,更加智能
-
阻塞锁:阻塞锁会改变线程的运行状态,让线程进入阻塞状态进行等待,当获得相应信号(唤醒或阻塞时间结束)时,进入就绪状态
-
重入锁:已持有锁的线程,在未释放锁时,可以再次获取到该锁
public class Xxx{
public final static ReentrantLock lock=new ReentrantLock();
public void a() {
lock.lock();
//.....
b(); //如果锁是可重入的,则b()直接获取到锁;如果锁不是可重入的,则b()需要单独获取获取锁,但锁还没被a()释放,b()会一直获取不到锁
//.....
lock.unlock();
}
public void b() {
lock.lock();
//......
lock.unlock();
}
}
-
读锁:是一种共享锁 | S锁(share),多条线程可同时操作共享资源,但都只能进行读操作、不能进行写操作
-
写锁:是一种排它锁 | 互斥锁 | 独占锁 | X锁,同一时刻最多只能有1个线程可以对共享资源进行写操作,其它线程不能对该资源进行读写
-
悲观锁:每次操作共享资源时,认为期间其它线程一定会修改共享资源,每次操作共享数据之前,都要给共享资源加锁
-
乐观锁:每次操作共享资源时,认为期间其它线程一般不会修改共享资源,操作共享资源时不给共享资源加锁,只在提交修改时验证数据是否被其它线程修改过,常用版本号等方式实现乐观锁
-
公平锁:等待锁的线程按照先来先得顺序获取锁(慎用)
-
非公平锁:释放锁后,等待锁的线程都可能获取到锁,不是先来先得
非公平锁可能导致某些线程长时间甚至一直获取不到锁,但这种情况毕竟是极少数;使用公平锁,为保证公平性有额外的开销,会降低性能,所以一般使用非公平锁
- 偏向锁:初次获取锁后,锁进入偏向模式,当获取过锁的线程再次获取该锁时会简化获取锁的流程,即锁偏向于曾经获取过它的线程
锁消除:编译时会扫描上下文,自动去除不可能存在线程竞争的锁
锁细化:如果只操作共享资源的一部分,不用给整个共享资源加锁,只需给要操作的部分加锁即可。使用细粒度的锁可以让多个线程同时操作共享资源的不同部分,提高效率。
锁粗化:要操作共享资源的多个部分,如果每次只给部分加锁,频繁加锁、释放锁会影响性能,可以扩大锁的作用范围,给整个共享资源加锁,避免频繁加锁带来的开销。
指令重排序
指令重排序:编译器、处理器会对指令序列重新排序,提高执行效率、优化程序性能
int a=1;
int b=1;
以上2条指令会被重排序,可能2条指令并发执行,可能int a=1;先执行,可能int b=1;先执行。
指令重排序遵循的2个原则
1、 数据依赖性,不改变存在数据依赖关系的两个操作的执行顺序。
int a=1;
int b=a;
b依赖于a,重排序不能改变这2个语句的执行顺序
2、as-if-serial原则,重排序不能改变单条线程的执行结果
int a=1;
int b=a;
执行结果是a=1、b=1,重排序后执行得到的也要是这个结果
数据同步接口
有时候需要对接第三方的项目,或者公司大部门之间对接业务,不能直接连接、操作他们的数据库,一般是建中间库|中间表,把我们|他们需要的数据放到中间库|表中,去中间库|表获取数据。更新数据库时需要同步更新中间库|表。
中间表的设计
- 只存储要使用的字段即可
- 需要用一个字段记录该条数据的状态:已入库、正在处理、处理时发生异常、已处理
- 需要用一个字段记录数据入库时间
- 需要用一个字段记录处理时间
记录时间是为了日后好排查问题、统计分析
对中间表的处理
可以使用生产者/消费者的线程协作模式
- 生产者分批读取中间表中未处理的数据 where status=‘xxx’,放到仓库中。因为数据量一般很大,所以通常要分批读取,防止仓库装不下。如果要操作多张表,很多操作都差不多,可以抽象出接口
- 消费者处理仓库中的数据
操作时需要更新中间表中的数据状态、处理时间