线程启动的三种方式
1. new Thread().start()
2.new Runnable().start();
3.使用线程池。Executors.newCachedThreadPool()
线程的六种状态
1.new 新建一个线程,未启动。
2.runnable 已经start的线程,等待资源,比如cpu资源。
3.blocked 阻塞状态。等待进入同步代码块锁
4.waiting 。调用了wait(),join(),LockSupport.park() 。 等待重新被唤醒。
5.TIMED_WAITING :sleep、wait(time)、LockSupport.parkUntil(1000)、LockSupport.parkNanos(10000) 知道某个时刻被唤醒。
6.TERMINATED 死亡
synchronized 和 CAS锁
cas自旋锁比较占用cpu时间,synchronized不占用cpu,使用的是wait队列。
线程数少使用自旋,否则使用synchronized
synchronized 锁定的是对象而不是代码。重量级锁,直接调用的汇编语言的 lock 命令。
AtomicXXX的类都是使用CAS锁
CAS 锁 : compareAndSwap等等,都是CPU支持的。
使用CAS锁会存在ABA问题。ABA即,比如甲对丙说我给你买了一瓶农夫山泉,让乙带给你了。路上乙渴了然后喝掉了,去便利店买了一瓶农夫山泉给了丙。农夫山泉还是农夫山泉,但是已经不是甲给的那瓶。
解决ABA问题:加版本号。如果是int类型,基础类型的没什么关系。
AQS 底层就是CAS+ volatile 。关于AQS
volatile 可见性、禁止指令重排,但是不能保证原子性
volatile 通过 MESI (intel cpu协议) 缓存一致性协议 保证多线程可见性。
public class ThreadTest {
static volatile ThreadTest instance;
ThreadTest(){
}
static ThreadTest getInstance(){
if(instance == null){
synchronized (ThreadTest.class){
if(instance == null){
instance = new ThreadTest();
}
}
}
return instance;
}
如上单例代码 static volatile ThreadTest instance; 是否需要使用volatile。
必须! 因为 instance = new ThreadTest(); 非原子性操作。1.先向内存申请一片 ThreadTest 对象空间 2.对该对象成员变量初始化3.将这块内存赋值给 instance 。 volatile 可以禁止 1、2、3 指令重排。
hotspot中正式利用了这一点来区分无锁与偏向锁。无锁与偏向锁的锁状态都为01,但是无锁的biased_lock:表示为0,偏向锁表示为1这就很好的区分了无锁与偏向锁。
无锁------------001
偏向锁---------101
轻量级锁------00
重量级锁------10
GC--------------11
System.out.println(ClassLayout.parseInstance(MyThread.class).toPrintable());
@Contended 注解,解决伪共享
读数据的时候都是按块来读的。cache line缓存行读取可以提高效率。
缓存行对齐:对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享,可以使用缓存行对齐的编程方式。
@Contended 可以代替 public long p1,p2,p3,p4,p5,p6,p7; //cache line padding
需要加上 VM options 参数 -XX:-RestrictContended
线程类 https://blog.csdn.net/dandanforgetlove/article/details/105955047
乐观锁、自旋锁 CAS
悲观锁 synchronized
读写锁、排它锁 ReadWriteLock
分段锁 LongAdder、Phaser
JVM中的四种引用:强软弱虚
WeakHashMap
WeakHashMap是Java集合框架里的一员,从名字可以看出它是某种 Map。它的特殊之处在于 WeakHashMap 里的entry
可能会被GC自动删除,即使程序员没有调用remove()
或者clear()
方法。
更直观的说,当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况:
- 调用两次
size()
方法返回不同的值;- 两次调用
isEmpty()
方法,第一次返回false
,第二次返回true
;- 两次调用
containsKey()
方法,第一次返回true
,第二次返回false
,尽管两次使用的是同一个key
;- 两次调用
get()
方法,第一次返回一个value
,第二次返回null
,尽管两次使用的是同一个对象。
遇到这么奇葩的现象,你是不是觉得使用者一定会疯掉?其实不然,WeekHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
要明白 WeekHashMap 的工作原理,还需要引入一个概念:弱引用(WeakReference)。我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的“有效引用”并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。
WeakHashMap 内部是通过弱引用来管理entry
的,弱引用的特性对应到 WeakHashMap 上意味着什么呢?将一对key, value
放入到 WeakHashMap 里并不能避免该key
值被GC回收,除非在 WeakHashMap 之外还有对该key
的强引用。
这个“弱键”的原理呢?大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:
(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。
实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
(02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。
这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。
有了list 和set 为什么要有queue 。 queue主要是为了高并发多线程准备的 。
treemap :红黑树.
线程容器
1.CopyOnWriteArrayList 写时复制。 线程安全,写时加锁,进行array的复制添加
读多写少使用
2. BlockingQueue 阻塞队列
BlockingQueue<String> b = new LinkedBlockingQueue<>(); 无界。
BlockingQueue<String> b = new ArrayBlockingQueue<>(100); 有界的queue,需要设置大小
BlockingQueue<String> b = new DelayQueue(); 实现时间上的排序 底层是 PriorityQueue
BlockingQueue<String> b = new SynchronousQueue<>(); 专门用来给两个线程之间传递任务的,一个。
LinkedTransferQueue<String> tr = new LinkedTransferQueue<>(); 类似 SynchronousQueue ,但是可以传递多个
多线程接口
Queue<String> q = new ConcurrentLinkedQueue<>();
q.poll(); 取出一个元素并且remove
q.peek(); 取出一个元素但是不remove
q.offer("g"); 添加,返回一个boolean
使用put 添加才时才会被阻塞,使用take 获取没有时才会阻塞住
//生产者消费者
BlockingQueue<String> strs = new LinkedBlockingQueue<>();
new Thread(()->{
int i = 1;
while (true){
try {
strs.put("dddd-" + i);
System.out.println(strs.size() + " offer :" + i);
if(strs.size() > 100){
Thread.sleep(10000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}).start();
new Thread(()->{
while (true){
try {
if(strs.size() < 50){
Thread.sleep(10000);
}
String i = strs.take();
System.out.println( " 开始tabke : " + i + " " + strs.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
put 源码
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
private final Condition notFull = putLock.newCondition();
notFull.await(); 阻塞,await源码:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
使用的还是 LockSupport.park(this); 进行的阻塞
Queue 和list 的区别。queue 添加了很多对线程友好的api : offer、peek、poll。 put、take 会阻塞
提升Qps
估算线程数
线程数 = cpu可用核数 / (1 - 阻塞系数(io 密集型 接近1,计算密集型接近 0))
比如io密集型接近 0.9 , 16核, = 16 / (1 - 0.9) = 160
提高并发数
1.能用多线程用多线程
2.增加各种连接数:tomcat、mysql、redis等
3.服务无状态(session统一管理),便于横向扩展(扩机器)
4.让服务能力对等(serviceUrl:打乱顺序)。例如不会一直调用某台服务,让服务调用均匀。