多线程与高并发编程总结、提升QPS

线程启动的三种方式

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:打乱顺序)。例如不会一直调用某台服务,让服务调用均匀。

猜你喜欢

转载自blog.csdn.net/dandanforgetlove/article/details/106038156
今日推荐