一、多线程的意义和使用
tomcat默认并发数是150。
并发:每秒支持的最大线程数。
并行:每一时刻支持的最大线程数。
java中线程的创建
- 继承Thread类
- 实现Runable接口
- Callable/Future带返回值的
线程的状态
通过jps命令查看正在运行的进程的pid,然后通过jstack pid查看各个线程的内存占用情况。
阻塞
WATING、TIME_WATING、BLOCKED、IO阻塞
java中的线程状态:6种
- NEW
- 运行状态(就绪/运行,执行完start()方法会进入)
- WATING(这些方法会进入sleep(),wait(),join(),LockSupport.park())
- TIMED_WATING(这些方法会进入sleep(long),wait(long),join(long),LockSupport.park(long))
- BLOCKED(锁等待的时候会进入)
- 终止
Thread.currentThread().isInterrupted();interrupted表示是否终断的标记。
interrupt()
- 设置一个共享变量的值为true
- 唤醒处于阻塞状态下的线程
操作系统中的状态:5种
二、并发编程带来的挑战
锁(synchronized)
锁的范围
- 实例锁,对象实例
- 类锁,静态方法,类对象
- 代码块
互斥锁的本质:共享资源
jol-core包可以打印对象的布局
锁状态 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁态 | 对象的hashcode | 分代年龄 | 0 | 01 | |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向互斥量的指针 | 10 | |||
偏向锁 | 线程id | Epoch | 分代年龄 | 1 | 01 |
CAS:会比较预期数据和原始数据是否一致,如果一致则修改,不一致修改失败。
-
偏向锁
当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁,如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。
-
轻量级锁
如果偏向锁关闭或当前偏向锁已被其他线程获取,那么这个时候如果有线程去抢占同步锁时,锁会升级到轻量级锁。
-
重量级锁
多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
- 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占的情况。
- 轻量级锁采用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程帧中的LockRecord,该工件存储对象原本的标记字段,它针对的是多个线程在不同时间段内申请通过一把锁的情况。
- 重量级锁会阻塞、唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。
三、volatile本质
lock汇编指令保证可见性问题。
L1,L2,L3高速缓存,因为高速缓存的存在,会导致缓存一致性问题。
缓存一致性协议
MSI、MESI、MOSI。。。
MESI表示缓存行的四种状态
- Modify 修改 表示共享数据指缓存在当前CPU缓存中,并且时被修改状态,也就是缓存的数据和主内存中不一致
- Exclusive 独占 便是缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
- Shared 共享 表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一直
- Invalid 失效 表示缓存已经失效
由于MESI导致的变量同步到内存中,要进行优化。
Strore Buffers
引入Strore Buffers后导致了指令重排序问题。
Store Buffers是一个写的缓冲,CPU0可以先把写入的操作缓存到Store buffers中,Store buffer中的指令再按照缓存一致性协议去发起其他CPU缓存行的失效,当收到ACK响应时在更新数据到缓存,再从缓存同步到内存。
内存屏障
通过内存屏障禁止了指令重排序问题。
#lock指令等价于内存屏障
- 读屏障 #store 处理器再读屏障之后的读操作,都在读屏障之后执行。配合写屏障配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作时可见的
- 写屏障 #load 就是使得写屏障之前的指令的结果对于屏障之后的读或写是可见的
- 全屏障 #fence 确保屏障前的内存读写操作的结果提交到内存之后,在执行屏障后的读写操作
软件方面
JMM
JMM定义了共享内存中多线程程序读写操作的行为规范:通过规范对内存的读写操作从而保证指令的正确性,他解决了CPU多级缓存、处理器优化、指令重排序的内存访问问题,保证了并发场景下的可见性。
导致可见性问题有俩个:一是高速缓存导致的可见性问题,二是指令重排序。对于缓存一致性问题,有总线锁和缓存锁是基于MESI协议。对于指令重排序,硬件层面提供了内存屏障指令。而JMM在这个基础上提供了volatile、final等关键字。
Happens-before模型
int a=0;
int b=0;
void test(){
a=1; //a
b=1; //b
int c=a*b; //c
}
程序顺序规则(as-if-serial)
a happens-before b,b happens-before c
传递性规则
a happens-before b,b happens-before c,a happens-before c.
volatile变量规则
volatile修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作。
内存屏障机制来防止指令重排序。
public class VolatileExample{
int a=0;
volatile boolean flag = false;
public void writer(){
a=1; //1
flag=true; //2
}
public void reader(){
if(flag){ //3
int i=a; //4
}
}
}
1 happens-before 2,3 happens-before 4 -->1 happens-before 4
监视器锁规则
int x=10;
synchronized(this){
//后续线程读取到的x一定是12
if(x<12){
x=12;
}
}
x=12;
start规则
public class StartDemo{
int x=0;
Thread t = new Thread(()->{
//读取的x的值一定是12
});
x=20;
t.start();
}
join规则
public class JoinDemo{
int x=0;
Thread t = new Thread(()->{
x=200;
});
t.start();
t1.join();//保证结果的可见性
//在此处读取到的x的值一定是200
}
final关键字提供了内存屏障的规则。
总线锁:在多cpu下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个LOCK信号,这个信号使得其他处理器无法通过总线访问到共享内存中的数据。
缓存锁:指内存区域如果被缓存在处理器的缓存行中,并且在LOCK期间被锁定,那么它执行锁操作回写到内存中,不在总线上加锁,而是修改内部的内存地址,基于缓存一致性协议来保证操作的原子性。
四、ThreadLocal
能够对线程间访问的变量进行隔离。
0x61c88647黄金分割数
public class HashDemo {
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) {
magicHash(16);
magicHash(32);
}
private static void magicHash(int size){
int hashcode=0;
for(int i=0;i<size;i++){
hashcode = i*HASH_INCREMENT+HASH_INCREMENT;
System.out.print((hashcode&(size-1))+" ");
}
System.out.println();
}
//7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
//7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
}
因为ThreadLocal里面存储数据缓存用的是弱引用,所以里面的Entry的key可能为null。(弱引用可以被GC)
线性探测
用来解决hash冲突的一种策略。
-
写入,找到发生冲突最近的空闲单元
-
查找,从发生冲突的位置,往后查找
-
sleep,join/yiled的区别
sleep让线程睡眠指定时间,会释放cpu时间片
join,wait/notify,让线程的执行结果可见
yiled让出时间片,触发重新调度。
-
java中volatile数组对引用可见,对数组中的元素不具备可见性
volatile缓存行的填充?–性能问题
-
线程什么时候会抛出InterruputExecption?
t.interrupt()去中断一个处于阻塞状态下的线程时(join/wati/sleep)
五、Lock(Synchronized)
-
ReentrantLock(重入锁)
-
ReentrantReadWriteLock(重入读写锁)
-
StampedLock(读写锁)
读和读不互斥
读和写互斥
写和写互斥
public class AtomicDemo { static Lock lock = new ReentrantLock(); static int count = 0; public static void incr(){ lock.lock();//获得锁 ThreadA获得了锁 count++; decr();//不会导致死锁,只是增加了重入的次数 lock.unlock();//ThreadA释放了锁 } public static void decr(){ lock.lock();//ThreadA再次来抢占锁:不需要再次抢占锁,而是只增加重入的次数 count--; lock.unlock(); } public static void main(String[] args) throws InterruptedException { for (int i=0;i<1000;i++){ new Thread(()->{ incr(); }).start(); } Thread.sleep(4000); System.out.println(count); } }
- volatile state=0(无锁),1代表是持有锁,>1代表重入
- 在某个地方存储当前获得锁的线程ID,判断下次抢占锁的线程是否为同一个。
AQS:AbstractQueuedSynchronizer 同步队列
六、Condition
Condition是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时线程才会被唤醒。
ConditionWait
public class ConditionDemoWait implements Runnable {
private Lock lock;
private Condition condition;
public ConditionDemoWait(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("begin -ConditionDemoWait");
try {
lock.lock();
//线程挂起
condition.await();
System.out.println("end -ConditionDemoWait");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
ConditionSignal
public class ConditionDemoSingal implements Runnable{
private Lock lock;
private Condition condition;
public ConditionDemoSingal(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("begin -ConditionDemoSignal");
try {
lock.lock();
//唤醒阻塞的线程
condition.signal();
System.out.println("end -ConditionDemoSignal");
} finally {
lock.unlock();
}
}
}
七、阻塞队列
BlockingQueue
- 添加元素的时候,队列满了,阻塞添加元素的线程
- 获取元素时,队列为空,获取数据的线程会阻塞
队列
- 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
- 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
BlockingQueue
-
放入数据
(1)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);
(2)offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。(3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
-
获取数据
(1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
(2)poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败。
(3)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
(4)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
- ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
- LinkedBlockingQueue
LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
- DelayQueue
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
使用场景:
DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
- PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
- SynchronousQueue
一种无缓冲的等待队列,类似于无中介的直接交易。分为公平模式和非公平模式。
如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
CountDownLatch
缺点:
CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
原理
- 初始化CountDownLatch实际就是设置了AQS的state为计数的值
- 调用CountDownLatch的countDown方法时实际就是调用AQS的释放同步状态的方法,每调用一次就自减一次state值
- 调用await方法实际就调用AQS的共享式获取同步状态的方法acquireSharedInterruptibly(1),这个方法的实现逻辑就调用子类Sync的tryAcquireShared方法,只有当子类Sync的tryAcquireShared方法返回大于0的值时才算获取同步状态成功,否则就会一直在死循环中不断重试,直到tryAcquireShared方法返回大于等于0的值,而Sync的tryAcquireShared方法只有当AQS中的state值为0时才会返回1,否则都返回-1,也就相当于只有当AQS的state值为0时,await方法才会执行成功,否则就会一直处于死循环中不断重试。
Semaphore(信号灯)
停车位。限流的机制。
CyclicBarrier(屏障,可循环使用)
当存在需要所有的子任务都完成时,才执行主任务,这个时候就可以使用CyclicBarrier
Atomic原子操作
八、HashMap
为什么初始化容量是2的指数幂
计算hashcode值得时候,为了hash值和数组长度取模与hash值和长度与运算相等。
为什么加载因子是0.75
在空间与时间平衡的大量计算中,0.75最合适。
ConcurrentHashMap
在jdk1.7的实现上,concurrenthashmap由一个个Segment组成,简单来说,ConcurrentHashMap是一个Segment数组,它通过继承ReentrantLock来进行加锁,通过每次锁住一个segment来保证每个segment内的操作的线程安全性从而实现全局线程安全。
jdk1.8 vs jdk1.7
- 取消了segment分段设计,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率。
- 将原本数组+单向链表的数据结构 —>>> 数组+单向链表+红黑树的结构。为什么使用红黑树,正常情况下,key hash之后如果能够均匀的分散在数组中,那么table数组中的每个队列的长度主要为0或者1,但是实际情况下,还是会存在一些队列长度过长的情况。如果还采用单线列表的方式,那么查询某个节点的时间复杂度就变为O(n);因此对于队列长度超过8的列表,jdk1.8采用了红黑树的结构,那么查询的时间复杂度就会降低到O(lognN),提升了查找的性能。
java的线程模型
hotspot(Oracle对于JVM的实现)
Java:重量级线程
go语言:(纤程/协程)轻量级线程
有操作系统的参与的线程叫重量级线程
CAS(compare And Swap)属于轻量级锁
解决ABA问题:加个版本号
- 向内存写值得时候,先读取当前值,看看是否值跟之前一致
- 如果一致,则往里面写值
- 如果不一致,则读取当前值,返回当前值操作
- 进行循环即自旋
对象在内存中的存储布局
普通对象
- markword(64位中占8个字节)
- 类型指针class pointer
- 实例数据instance data
- 对齐(前面三个不能被8整除,需要用这个补齐)
一个Object在内存中占16个字节
markword中记录的是锁信息、hashcode、GC信息
- 偏向锁
不需要锁竞争,先进去的线程占有。
偏向锁会延时4s(–Xx BasicLockingStart),JVM启动的时候就有锁竞争,所以先延时4s让其用其他锁进行竞争。
- 轻量级锁
也叫自旋锁,等待的时候消耗cpu,自旋超过10次,进入重量级,或者自旋线程数超过cpu的一半,1.6之后,加入自适应自旋
- 重量级锁
等待的时候不消耗cpu
L1、L2、L3缓存
一个cpu,有两个核。L1,L2都在核中,L3在cpu中。
缓存行:
缓存行的大小为64字节。
disruptor单机最快的MQ
DCL(double check lock)双重检查,第一层判断是为了提高效率。
对应的实体需要加volidate才能保证线程安全,因为执行class的时候,创建实例的会先初始化实体给个默认值(实现了半个实例),然后在实例化代码中的实例。所以要加上volidate关键字。
汇编语言lock addl实现volitile的两个作用(内存可见性,指令重排序)