目录
概念、理论
并发:多个线程操作相同的资源,优点:效率高、资源利用率高,缺点:线程可能不安全、数据可能不一致,需要使用一些方式保证线程安全、数据一致
高并发:服务器能同时处理大量请求
线程安全:当多个线程访问某个类,不管采用何种调度方式、线程如何交替执行,这个类都能表现出正确的行为。
造成线程不安全的原因
- 存在共享资源
- 多个线程同时操作同一共享资源,操做不具有原子性
如何实现线程安全?
- 使多线程不同时操作同一共享资源: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接口来实现自定义的锁。要注意是否可能发生异常,需不需要把释放锁放在finally中。
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使用ThreadLocalMap存储所在线程中的线程私有数据,一个线程对应一个ThreadLocalMap。ThreadLocal可以实现各个线程私有数据的隔离,并发场景下可以实现无状态调用,常用于存储可能会被多个线程访问、但线程之间需要独立操作的变量。
//如果不指定初始值,默认为null(泛型不能为基本类型)
private ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
//可以指定初始值
private ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "aaa");
private ThreadLocal<User> threadLocal3 = ThreadLocal.withInitial(() -> new User());
public void test() {
//设置|更新值
threadLocal1.set(100);
threadLocal2.set("bbb");
threadLocal3.set(new User("chy"));
//获取值
Integer integer = threadLocal1.get();
String string = threadLocal2.get();
User user = threadLocal3.get();
//重置为初始值
threadLocal1.remove();
threadLocal2.remove();
threadLocal3.remove();
}
- 每个线程自己维护一个ThreadLocalMap,用于存储线程自身所有的ThreadLocal变量。
- map自然是以键值对的形式存储,键值对对应的内部类是Entry(ThreadLocal<?> k, Object v),key是ThreadLocal对象,value是该ThreadLocal对象对应的值。
使用ThreadLocal的注意点
- 每个线程的ThreadLocal都是线程私有的,主线程的ThreadLocal也是主线程自身私有的,不会共享给子线程。
public void test() {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
//此处操作的是主线程自己的ThreadLocal
threadLocal.set(100);
threadLocal.get();
//lambda表达式可以访问外部变量,可以使用主线程中定义的threadLocal
//但创建线程时,是把最初的ThreadLocal深拷贝一份,放在线程自身的ThreadLocalMap中,拷贝的是初值,不是主线程自身的ThreadLocal
new Thread(() -> {
//此处取到的是初始值null
threadLocal.get();
threadLocal.set(200);
}).start();
new Thread(() -> {
//取到的也是初始值null
threadLocal.get();
}).start();
}
- 线程池会复用线程,在提交给线程池的任务中使用ThreadLocal时,最后一定要使用remove()清除对ThreadLocal做的修改、重置到初始值,防止影响到此线程后续执行的使用了该ThreadLocal的任务。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
6,
20,
10, TimeUnit.MINUTES,
new LinkedBlockingQueue<Runnable>(100),
new ThreadPoolExecutor.AbortPolicy()
);
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadPoolExecutor.execute(() -> {
threadLocal.set(100);
//...
threadLocal.remove();
});
threadPoolExecutor.execute(() -> {
threadLocal.set(200);
//...
threadLocal.remove();
});
volatile
public static volatile boolean flag = true;
- volatile只能修饰变量
- 2个作用:实现了所修饰变量的可见性、禁止指令重排序。可见性指的是变量值被某个线程修改时,其它使用此变量的线程可以观测到这个变化。
- volatile只实现了可见性,没有实现原子性,严格来说并没有实现线程安全。
volatile、synchronized的比较
Atomic系列原子类
i++、++i、i–、–i、+=、-=等操作都不是原子性的,juc的atomic包下的类提供了自增、自减、比较赋值、取值修改等原子性方法,可以线程安全地进行操作
AtomicInteger atomicInteger = new AtomicInteger(0);
AtomicLong atomicLong = new AtomicLong(0);
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
//引用
AtomicReference<User> user = new AtomicReference<>(new User());
//以上4种内部都维护了一个目标类型的成员变量,都可以使用带参构造器给该成员变量设置初始值
//也可以使用无参构造器,使用无参构造器时,AtomicInteger、AtomicLong默认初始值0,AtomicBoolean默认初始值false,AtomicReference默认初始值null
//数组
AtomicIntegerArray atomicIntegerArray1 = new AtomicIntegerArray(new int[]{1, 23});
//参数可以是数组,也可以是数组的元素个数
AtomicIntegerArray atomicIntegerArray2 = new AtomicIntegerArray(10);
AtomicLongArray atomicLongArray1 = new AtomicLongArray(new long[]{1, 23});
AtomicLongArray atomicLongArray2 = new AtomicLongArray(10);
//对象的引用型字段,此处的引用型字段是 orderList
AtomicReferenceFieldUpdater<User, List> userOrderList = AtomicReferenceFieldUpdater.newUpdater(User.class, List.class, "orderList");
//对象的Integer型字段,相当于 AtomicReferenceFieldUpdater<User, Integer>
AtomicIntegerFieldUpdater<User> atomicIntegerUserAge = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
//对象的Long型字段,相当于 AtomicReferenceFieldUpdater<User, Long>
AtomicLongFieldUpdater<User> atomicLongUserId = AtomicLongFieldUpdater.newUpdater(User.class, "id");
- 原子类适合多线程读写简单数据类型的变量。只能保证单个变量的原子性,只能进行简单操作,如果要保证多个变量、稍微复杂点的操作的原子性,要用其它方式来实现线程安全(一般是加锁)。
- 原子类使用CAS实现乐观锁,并发支持好、效率高
- CAS提交修改失败时会while循环进行重试,如果重试时间过长,会给cpu带来很大开销
- 可能发生ABA问题。有2个原子类解决了ABA问题 :AtomicMarkableReference、AtomicStampedReference,使用标记、邮戳实现乐观锁,和版本号、时间戳机制差不多,避免了ABA问题。
我们自己实现原子性操作时,也可以使用CAS算法。
并发容器
ArrayList、LinkedList、HashSet、HashMap等常见的普通集合容器都不是线程安全的,多线程并发读写同一个普通集合时(多个线程同时修改同一个集合,或者有的线程读、有的线程写),会抛出并发修改异常。
多线程并发读写集合时,不能使用普通集合,需要使用线程安全的集合
- Vector、Hashtable 的方法都使用synchronized修饰,是线程安全的,但缺点较多,基本不使用这2个类。
- Collections.synchronizedXxx()可以将集合转换为线程安全的集合,是使用synchronized锁住整个集合,读写都加锁,同一时刻只能单线程操作该集合,效率低下。
- juc提供了常用的并发容器,线程安全,性能也不错。
Copy-On-Write集合容器CopyOnWriteArrayList、CopyOnWriteArraySet
//有序,按照插入顺序排列,内部使用Object[]存储元素
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
//无序,CopyOnWriteArraySet内部使用CopyOnWriteArrayList存储元素
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
//写操作都要先获取锁|加锁
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//从原数组复制得到新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//往新数组中添加元素
newElements[len] = e;
//指向新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
//读操作都不加锁
public E get(int index) {
return get(getArray(), index);
}
- 线程进行写操作时,需要先获取锁,避免多个线程同时进行写操作。获取到锁后,复制内置数组得到一个新数组,在新数组上进行修改,最后将引用指向新数组。
- 某个线程进行写操作时,其它线程不能进行写操作,但依然可以进行读操作,可以读取旧数组中的内容。
Copy-On-Write总结
- 设计思想:读写分离,最终一致性
- 优点:多线程并发读的时候不加锁,性能好
- 缺点:写的时候需要复制内置数组,元素多的时候复制时间长,同时存在2个数组,内用占用大,容易触发GC;且写的时候其它线程可以对旧数组进行读操作,可能存在数据不一致的情况。
- 应用场景:适合读多写少的场景,比如黑名单、白名单。
- 使用建议:每次调用修改的方法时,都会复制数组,要操作多个元素时,尽量使用addAll()、removeAll()之类的方法,一次性修改,只复制数组一次,可以提升性能。
ConcurrentHashMap、ConcurrentLinkedQueue
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); //map
ConcurrentLinkedQueue<String> queue1 = new ConcurrentLinkedQueue<>(); //基于链表的队列
阻塞队列LinkedBlockingQueue、ArrayBlockingQueue
常用于在生产者/消费者的线程协作模式中。
//基于链表的阻塞队列,如果参数指定了元素个数,则有界、不能扩容,如果未指定,则无界
LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>();
//基于数组的阻塞队列,指定容量,不能扩容(有界)
ArrayBlockingQueue<String> queue3 = new ArrayBlockingQueue<>(20);
//可以指定是否是公平锁,默认false
ArrayBlockingQueue<String> queue4 = new ArrayBlockingQueue<>(20,true);
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
//入队的3个方法
//返会操作结果,boolean,如果队列满了放不下,返回false
queue.offer("");
//返会操作结果,boolean,如果队列满了放不下,会抛出异常
queue.add("");
try {
//如果队列满了,会阻塞线程,直到队列有空位可以放进去
queue.put("");
} catch (InterruptedException e) {
e.printStackTrace();
}
//出队的3个方法
//如果队列中没有元素,返回null
queue.poll();
//如果队列中没有元素,直接抛出异常
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 栅栏
CountDownLatch用于等待一些线程全部执行完毕,CyclicBarrier用于一些线程在执行中的某个阶段互相等待,等待的线程都执行到执行阶段时才继续往下执行
//指定await要应用的线程数
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
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()才会继续往下执行。
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();
锁的分类
-
自旋锁:多线程切换上下文会消耗系统资源,频繁切换上下文不值得,线程在没获取到锁时暂时执行空循环等待获取锁,即自旋,循环次数即自旋次数;如果在指定自旋次数内没获取到锁,则挂起线程,切换上下文,执行其它线程。锁默认是自旋的。
-
自适应自旋锁:自旋次数不固定,由上一次获取该锁的自旋时间及锁持有者的状态决定,更加智能
-
阻塞锁:阻塞锁会改变线程的运行状态,让线程进入阻塞状态进行等待,当获得相应信号(唤醒或阻塞时间结束)时,进入就绪状态
-
重入锁:已持有锁的线程,在未释放锁时,可以再次获取到该锁
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’,放到仓库中。因为数据量一般很大,所以通常要分批读取,防止仓库装不下。如果要操作多张表,很多操作都差不多,可以抽象出接口
- 消费者处理仓库中的数据
操作时需要更新中间表中的数据状态、处理时间