零.内存模型
计算机在执行程序时,每条指令都是在CPU中执行的,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。在多核CPU中,每条线程可能运行于不同的CPU中。如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。为了解决缓存不一致性问题,通常来说有以下2种解决方法:
1.通过在总线加LOCK#锁的方式
由于在锁住总线期间,其他CPU无法访问内存,导致效率低下
2.通过缓存一致性协议
MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
一.java内存模型
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?
1.原子性
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现
2.可见性
对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性
3.有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性
happens-before 原则
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序
(1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作(这里并不是指完全按顺序,也可能重排序,虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序)
(2)锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
(3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
(4)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
(5)线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
(6)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
(7)线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
(8)对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
3.synchronized
synchronized实际上是用对象锁保证了临界区代码的原子性
1.使用方法
(1)synchronized修饰普通同步方法,此时锁的是当前实例的对象
(2)synchronized修饰静态同步方法,此时锁的是类的class对象
(3)synchronized修饰同步代码块,此时锁的是括号内的对象
2.原理
JVM 是通过进入、退出 对象监视器(Monitor) 来实现同步,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁
3.优化
synchronized是一种重量级锁,会涉及到操作系统状态的切换。jdk1.6后,对synchronized进行优化。引入了偏向锁,轻量级锁
(1).偏向锁
偏向锁会偏向于第一个占有锁的线程。如果没有竞争,已经获得偏向锁的线程,在将来进入同步块时不会进行同步操作。
①markword中偏向锁标识设置为1,锁标志位为01
②如果为可偏向状态,则检查线程ID是否指向当前线程,如果是,执行同步代码块
③如果不是,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程。竞争失败,则说明发生竞争,获得偏向锁的线程被挂起(撤销偏向锁,会导致stop the world),偏向锁升级为轻量级锁
(2)轻量级锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。一定时间内获取不到则膨胀为重量级锁
(3)重量级锁
①为锁对象申请monitor锁,让锁对象指向重量级锁地址
②然后进入monitor的entrylist阻塞
③当轻量级锁解锁时,会失败。这时根据monitor地址找到monitor对象,唤醒entrylist中阻塞的线程
4.volatile
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1.使用volatile关键字会强制将修改的值立即写入主存
使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效,由于线程1的工作内存中缓存变量的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。这其实就是MESI)
2.禁止进行指令重排序
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
//线程1:
context = loadContext(); //语句1
volatile inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
如果不用volatile,可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
二.并发基础
1.AQS
AQS,抽象队列同步器。J.U.C是基于AQS实现。它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
1.两种资源共享方式
Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
2.自定义同步器实现方式
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
(1)isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
(2)tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
(3)tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
(4)tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
(5)tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的
3.acquire流程
(1)调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
(2)没成功,则addWaiter()将该线程加入等待队列的尾部,调用LockSupport.park()进入阻塞状态
(3)轮到自己,会被unpark()会去尝试获取资源。获取到资源后才返回
2.CAS
1.cas
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
2.cas怎么保证拿到的内存中的值是最新的?
volatile修饰变量,可以保证其可见性
3.ABA问题?
就是说一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,发现A没有变化,就误以为是原来的那个A(别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律)
4.解决ABA问题
(1)AtomicMarkableReference,维护一个boolean类型的标记(可以知道是否被修改过)
(2)AtomicStampedReference,维护一个版本号,发生改动后版本号会改动(可以知道被修改了几次)
三.J.U.C
1.Lock
1.synchronized和Lock的区别?
synchronized是java内置的锁,不需要手动释放锁。lock必须手动释放锁。这其实既是优点也是缺点,优点是使用灵活,缺点就是使用不当容易发生死锁。
(1)synchronized 获取锁的线程被阻塞,其它线程就只能一直等待。lock可以实现阻塞一定时间后自动释放trylock(time),或可中断锁lockInterruptibly()
(2)读操作和读操作不发生冲突,用synchronized,两个读操作之间都需要相互等待
2.Lock接口的方法
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
(1)lock():用来获取锁,如果已被其它线程获取,则进行等待
(2)trylock():尝试获取锁。获取成功返回true,获取失败返回false。(无论如何都会立即返回,拿不到锁时不会一直等待)
(3)tryLock(long time, TimeUnit unit):和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false
(4)lockInterruptibly():获取可中断锁。在阻塞的时候调用线程的interrupt()中断线程的等待。(注意:只有等待时可以响应中断)
(5)unlock():释放锁
(6)newCondition():线程之间的交互,synchronized主要通过两个函数完成,Object.wait()和Object.notify()。而Lock通过Condition的await()和signal()实现
3.ReentrantLock
ReentrantLock可重入的互斥锁,是实现Lock接口的一个类,支持公平锁和非公平锁两种方式
(1).互斥锁
同一时间一个线程能访问同步代码块
(2).公平锁和非公平锁
公平锁在进行lock时,首先会进行tryAcquire()操作。在tryAcquire中,会判断等待队列中是否已经有别的线程在等待了。如果队列中已经有别的线程了,则tryAcquire失败,则将自己加入队列。如果队列中没有别的线程,则进行获取锁的操作。
非公平锁,在进行lock时,会直接尝试进行加锁,如果成功,则获取到锁,如果失败,则进行和公平锁相同的动作。
(区别就是非公平锁在lock的时候直接尝试进行加锁的操作,如果成功,获取到锁,失败则和公平锁一样,放到队列等待)
4.ReentrantReadWriteLock
ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
1.读写锁怎么实现分别记录读写状态的?
重写AQS中的方法,将同步状态state的拆分为高16位和低16位。低16位写状态用来表示写锁获取的次数。高16位读状态用来表示读锁获取的次数
2.什么情况下能获得读锁?什么情况下获得写锁?
(1)当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态
(2)当写锁被其他线程获取后,读锁获取失败(本线程获取写锁后是可以直接获取读锁),否则获取成功利用CAS更新同步状态
3.锁降级
(1)锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
(2)为什么要把持住当前写锁?保证可见性,如果先释放写锁再获取读锁,中间可能有其它的线程获取写锁并更新数据再释放写锁。那么当前线程无法感知另一个线程的更新
2.Atomic
原子类底层通过 volatile 和 CAS(Unsafe.class) 来保证了内存可见性与原子性
3.工具类
1.CountDownLatch
CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。
你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。
( 举个例子,有三个工人在为老板干活,这个老板有一个习惯,就是当三个工人把一天的活都干完了的时候,他就来检查所有工人所干的活)
//工人线程
public class Worker implements Runnable{
private CountDownLatch downLatch;
private String name;
public Worker(CountDownLatch downLatch, String name){
this.downLatch = downLatch;
this.name = name;
}
public void run() {
this.doWork();
try{
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
}catch(InterruptedException ie){
}
System.out.println(this.name + "活干完了!");
this.downLatch.countDown();
}
private void doWork(){
System.out.println(this.name + "正在干活!");
}
}
//老板线程
public class Boss implements Runnable {
private CountDownLatch downLatch;
public Boss(CountDownLatch downLatch){
this.downLatch = downLatch;
}
public void run() {
System.out.println("老板正在等所有的工人干完活......");
try {
this.downLatch.await();
} catch (InterruptedException e) {
}
System.out.println("工人活都干完了,老板开始检查了!");
}
}
//测试
public class CountDownLatchDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(3);
Worker w1 = new Worker(latch,"张三");
Worker w2 = new Worker(latch,"李四");
Worker w3 = new Worker(latch,"王二");
Boss boss = new Boss(latch);
executor.execute(w3);
executor.execute(w2);
executor.execute(w1);
executor.execute(boss);
executor.shutdown();
}
}
2.CyclicBarrier
允许一组线程互相等待,直到到达某个公共屏障点,也就是阻塞在调用cyclicBarrier.await()的地方
(就像王者农药,10个人都加载完才可以开局。每个人加载完后调用await()阻塞等待其它人加载完)
public void latch() throws InterruptedException {
int count = 10;
CyclicBarrier cb = new CyclicBarrier(count, new Runnable() {
@Override
public void run() {
System.out.println("全部加载完毕");
}
});
ExecutorService executorService = Executors.newFixedThreadPool(count);
while (true){
for (int x=0;x<count;x++){
executorService.execute(new Worker(x,cb));
}
}
}
class Worker extends Thread {
Integer start;
CyclicBarrier cyclicBarrier;
public Worker(Integer start, CyclicBarrier cyclicBarrier) {
this.start = start;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println(start + " 已加载完");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
3.Semaphore
Semaphore 信号量维护了一个许可集,每次使用时执行acquire()从Semaphore获取许可,如果没有则会阻塞,每次使用完执行release()释放许可。
使用场景:Semaphore对用于对资源的控制,比如数据连接有限,使用Semaphore限制访问数据库的线程数。
public void latch() throws InterruptedException, IOException {
int count = 5;
Semaphore semaphore = new Semaphore(1);
ExecutorService executorService = Executors.newFixedThreadPool(count);
for (int x=0;x<count;x++){
executorService.execute(new Worker(x,semaphore));
}
System.in.read();
}
class Worker extends Thread {
Integer start;
Semaphore semaphore;
public Worker(Integer start, Semaphore semaphore) {
this.start = start;
this.semaphore = semaphore;
}
@Override
public void run() throws IllegalArgumentException {
try {
System.out.println(start + " 准备执行");
TimeUnit.SECONDS.sleep(1);
semaphore.acquire();
System.out.println(start + " 已经执行");
semaphore.release();
System.out.println(start + " 已经释放");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
0 准备执行
2 准备执行
1 准备执行
3 准备执行
4 准备执行
2 已经执行
2 已经释放
4 已经执行
4 已经释放
1 已经执行
1 已经释放
0 已经执行
0 已经释放
3 已经执行
3 已经释放
*/
4.并发集合
(1)ConcurrentHashMap:线程安全的HashMap的实现
(2)ConcurrentSkipListMap:是线程安全的有序的哈希表(相当于线程安全的TreeMap); 它继承于AbstractMap类。ConcurrentSkipListMap是通过“跳表”来实现的
(3)CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
(4)CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素
(5)ConcurrentSkipListSet:线程安全的有序的集合(相当于线程安全的TreeSet)。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的
(6)ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
(7)LinkedBlockingQueue:基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue
四.线程池
1.线程状态
ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量
目的是将线程状态和线程个数合二为一,这样可以用一次cas原子操作进行赋值
2.构造参数
(1)corePoolSize:核心线程池的大小,如果核心线程池有空闲位置,新的任务就会被核心线程池新建一个线程执行,执行完毕后不会销毁线程,线程会进入缓存队列等待再次被运行。
(2)workQueue:缓存队列,用来存放等待被执行的任务。
(3)maximunPoolSize:线程池能创建最大的线程数量。如果核心线程池和缓存队列都已经满了,新的任务进来就会创建救急线程来执行。但是数量不能超过maximunPoolSize,否侧会采取拒绝接受任务策略,我们下面会具体分析。
(4)keepAliveTime:救急线程线程能够空闲的最长时间,超过时间,线程终止。这个参数默认只有在线程数量超过核心线程池大小时才会起作用。只要线程数量不超过核心线程大小,就不会起作用。
(5)threadFactory:线程工厂,用来创建线程
(6)handler:拒绝策略
①abortPolicy:抛出异常(默认)
②discardPolicy:放弃本次任务
③discardoldestPolicy:放弃队列中最早的任务,本任务取代
④callerrunPolicy:让调用者运行任务
3.实现方式
(1)newFixedThreadPool
核心线程数=最大线程数(没有救急线程,因此也无需超时时间)
阻塞队列是无界的,可以放任意数量任务
(2)newCachedThreadPool 核心线程数为0,最大线程数是Integer.MAX_VALUE,救急线程存货时间60s
队列采用SynchronousQueue。它没有容量,没有线程取是放不进去的
(3)newSingleThreadExecutor
线程固定为1,其它任务来时放进无界队列等待
4.常用方法
//执行任务
void execute(Runnable command);
//提交任务(有返回值)
submit(Callable task);
//提交任务队列所有任务
invokeAll()
//提交任务队列所有任务,哪个先执行完毕,返回结果,其它任务取消
invokeAny();
5.线程池原理
如果当前线程池中正在执行的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中正在执行任务的的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理
五.ThreadLocal
java.lang.ThreadLoca是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据(t1.get t1.set)
1.synchronized和threadlocal的区别
(1)Synchronized同步机制采用了“以时间换空间”的方式,仅提供一份变量,让不同的线程排队访问
(2)而ThreadLocal采用了“以空间换时间”的方式,每一个线程都提供了一份变量,因此可以同时访问而互不影响。
2.运用场景——转账
转账转入和转出要在同一个事务,service层获得Jdbccollection,然后setautoconmit(false)来使用事务。dao层也要获得一个connection操作数据库。但这两个connection不一致,事务就会失效
1.常规解决
1.传参:将service的collection直接传到dao层
2.加锁:将getconnection方法加锁
2.ThreadLocal解决方案
static ThreadLocal<Connection> tl = new ThreadLocal<>();
public static Connection getConnection(){
Connection conn = tl.get();
if(conn == null){
conn = ds.getConnection();
tl.set(conn);
}
return conn;
}
优点:
(1)不用参数传递,避免了代码的耦合
(2)各线程之间数据相互隔离却又具有并发性,避免了同步方式带来的性能损失
3.ThreadLocal内部结构
1.结构
1.8前,每个ThreadLocal都创建一个map,然后thread作为map的key,要存储的局部变量作为map的value
1.8后,每个thread线程内部都有一个map,然后threadlocal作为key,要存储的局部变量作为map的value
2.1.8后的优点:
(1).每个map存储的entry数量变少(不是每个thread都创建threadlocal)
(2).当thread销毁时threadlocal也销毁,减少内存的使用
3.threadlocal方法
(1)initialValue:返回当前线程局部变量的初始值
(2)set:设置当前线程绑定的局部变量
(3)get:获取当前线程绑定的局部变量
(4)remove:移除当前线程绑定的局部变量
4.ThreadLocalMap基本结构
ThreadLocalMap是ThreadLocal的内部类,没有实现map接口,用独立的方式实现map功能,其内部的Entry也是独立实现的
Entry继承weakReference,也就是key(ThreadLocal)是弱引用,目的是将ThreadLocal对象的生命周期和线程生命周期解绑
1.什么是弱引用
正是因为有引用,对象才会在内存中存在。
当对象的引用数量归零后,垃圾回收程序会把对象销毁。
弱引用不会增加对象的引用数量。 引用的目标对象称为所指对象(referent)。 因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
2.ThreadLocalMap的key使用弱引用,还会出现内存泄漏吗?怎么避免?
虽然ThreadLocal的key是弱引用保证了threadLocal能及时回收。但是Thread指向Entry依然是强引用,所以可能导致内存泄漏
避免方案:使用完ThreadLocal,手动删除这个Entry
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。