阿里程序员总结出的Java并发组件(详细)-高并发与多线程

Java并发组件一之CountDownLatch

使用场景:

一个或N个线程,等待其它线程完成某项操作之后才能继续往下执行。CountDownLatch描述的是,一个或N个线程等待其他线程的关系。

使用方法:

  1. 设CountDownLatch个数:CountDownLatch countDownLatch=new CountDownLatch(3);
  2. 在等待线程中await:countDownLatch.await();
  3. 在其他线程中减少count值:countDownLatch.getCount();
  4. 一旦其他线程中的countDownLatch.getCount()的次数为实例化时的count值,就唤醒等待线程
public class T06_TestCountDownLatch {
    public static void main(String[] args) {
        usingJoin();
        usingCountDownLatch();
    }

    private static void usingCountDownLatch() {
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for(int i=0; i<threads.length; i++) {
            threads[i] = new Thread(()->{
                int result = 0;
                for(int j=0; j<10000; j++) result += j;
                latch.countDown();
            });
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("end latch");
    }

    private static void usingJoin() {
        Thread[] threads = new Thread[100];

        for(int i=0; i<threads.length; i++) {
            threads[i] = new Thread(()->{
                int result = 0;
                for(int j=0; j<10000; j++) result += j;
            });
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }

        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("end join");
    }
}

Java并发组件二之CyclicBarriar

使用场景:

多个线程相互等待,直到都满足条件之后,才能执行后续的操作。CyclicBarrier描述的是各个线程之间相互等待的关系。

使用步骤:

  1. 正常实例化:CyclicBarrier sCyclicBarrier=new CyclicBarrier(3);
  2. 带runnable的实例化,打破屏障时,优先执行Runnable:CyclicBarrier sCyclicBarrier=new CyclicBarrier(3,new Runnable(){//todo});
  3. await线程:sCyclicBarrier.await();
  4. 当wait线程数量为,count值时。唤醒所有等待线程。

原理:这里直接用源码解释(后续补充)

  1. 锁用的是ReentrantLock重入锁。ReentrantLock lock = new ReentrantLock()
  2. 获取该锁的condition实例。Condition trip = lock.newCondition()
  3. wait线程,用的是Condition.await
  4. signal线程,用的是Condition.signalAll
  5. 两个构造函数,初始化parties值,count值,以及Runnable(唤醒时优先执行runnable)
  6. count值是用来计算线程数的。每有一个线程执行了await方法,--count。直到count==0
  7. await方法,包含了是否超时,以及超时时间
  8. dowait是线程wait的主要逻辑代码。当count==0时执行唤醒操作。不为0时,进入for循环,执行Condition.await。
  9. dowait中的for循环是为了超时操作的逻辑。
  10. 正常跳出for循环是通过dowait方法中的(nextGeneration()重新实例化了gennaration)
  11. breakBarrier() 打破屏障,唤醒所有wait线程
  12. nextGeneration() 更新状态,唤醒所有wait线程
  13. reset()重置状态。里面调用了breakBarrier()和nextGeneration()

Java并发组件三之Semaphore

  • 使用场景:常用于使用有限的资源,限制线程并发的最大数量。默认情况下,信号量是非公平性的(先等待先执行为公平。类似于买东西的时候大家排队付款,先来的先付款是公平的。但是这时候有人插队,那就是非公平的)设定信号量的最大个数:Semaphore semaphore=new Semaphore(3);
  1. 获取信号量:
  2. semaphore.acquire(); //获取信号量
  3. semaphore.acquire(3); //获取多个许可
  4. semaphore.tryAcquire(3); //尝试获取多个许可
  5. semaphore.tryAcquire(5, TimeUnit.SECONDS); //给定时间内获取许可
  6. 释放信号量:semaphore.release();
代码:
public class T11_TestSemaphore {
    public static void main(String[] args) {
        //Semaphore s = new Semaphore(2);
        Semaphore s = new Semaphore(2, true);
        //允许一个线程同时执行
        //Semaphore s = new Semaphore(1);
        new Thread(()->{
            try {
                s.acquire();
                System.out.println("T1 running...");
                Thread.sleep(200);
                System.out.println("T1 running...");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }).start();
        new Thread(()->{
            try {
                s.acquire();
                System.out.println("T2 running...");
                Thread.sleep(200);
                System.out.println("T2 running...");
                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

多线程与高并发需要体会记住的点

  1. Class文件加载到同一个ClassLoader空间中是单例的,如果是不同的classloader则不是单例。
  2. synchronized如果加在静态方法中,则锁住的是类(一般方法中锁住的是this),等同于synchronize(T.class)。
  3. synchronized锁住的任何对象,看对象头顶的两位01,如果是已经标记过的,则是偏向锁。
  4. synchronized锁对象底层jvm虚拟机规范没有要求,但是hostpot markword中是用头顶两位用来标记,组合分别是不同锁的类型。
  5. synchronized(this)表示锁定当前对象,与synchronized方法是等值的,如果静态方法则是锁定的T.class。
  6. synchronized既保证了原子性又保证了可见性。
  7. 程序中如果出现异常,则锁会被释放,其他程序会乱入,可能导致数据不一致。
  8. 我就是厕所所长(一、二)。
  9. syn(obj) markdown 记录线程id(偏向锁),如果线程过来发现ID是自己的 那就直接用,偏向锁如果线程争用,则会升级为自旋锁(默认旋10次),十次以后还没得到这把锁则升级为重量级锁(-OS), 去操作系统申请资源,变为等待状态则不占用CPU,锁只可以升级,没法降级。(由于java jvm没有规定synchronized如何实现,所以理论上调整jvm也是可以降级锁的),争用大部分是自旋锁,占CPU但是不访问操作系统,所以是用户态,不是内核态,加锁解锁比经过内核态效率要高。

10.执行时间长或线程数量多用系统锁(-OS),时间短(指加锁代码时间短)用自旋锁(但是线程不能太多)。

11.锁定的对象不能是String常量 Integer(特殊处理过) Long等基本数据类型,比如String在 常量池中,相同字面量是同一个锁,会出现一个线程死循环,其他线程没法进来,的当使用不可变类对象(final Class)作为对象锁时,使用synchronized同样会有并发问题 比如Integer类,由于不可变特性,作为锁但同步块内部仍然有计算操作,会生成一个新的锁对象(虽然String也是final Class,但它的原因却是字面量常量池),经过反编译之后对于锁定发Integer 变量i, 执行i++操作相当于执行了
i = Integer.valueOf(i.intValue()+1),通过查看Integer的valueOf方法实现可知,其每次都new了一个新的Integer对象,锁变了.。

12.Lock与synchronized区别在于,Lock中用的是CAS(很多实现用的是自旋)操作,占用CPU资源,而synchronized自旋(占用CPU时间)之后会进入wait队列,即不占用CPU时间。

13.volatile(大厂默认必会的知识点),意思:可变的、易变的,
1、保证线程可见性,不是实时监控。每一个线程有自己的局部内存,当需要使用共享内存中的变量时,会copy一份到自己的局部内存,修改之后会写回共享内存,但是其他需要使用这个变量的线程并不能及时读到自己的内存,这叫线程之间不可见,加了volatile之后可以保证一个线程做出改变之后其他线程能及时感受到。本质是使用了CPU的缓存一致性协议(MESI)
代码演示:
2、禁止指令重排序,跟CPU有关系,现代的CPU为了提高效率会并发执行命令,这种新的架构之上,要求编译器对指令进行重排序,而volatile会禁止指令重排序(JVM级别),
举例子:单例模式的双重检查时,要不要加volatile?要!

public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT
    private Mgr06() {
    }
    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }
    public void m() {
        System.out.println("m");
    }
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

一个内存的赋值分为三个步骤,第一步是在堆内存中new,申请地址空间(如果是基本数据类型默认是0),第二步是赋值,第三步是赋值给栈内存中的变量,但是如果有指令 重排序会有第一步之后栈内存的变量就有值(0),即初始化一般后变量就有值了,那么双重检查时就会发现变量已经有值了,可以直接使用。超高并发时会有这种情况。 加了volatile之后就不会有指令重排序的情况。则可以避免。没法测试,除非ASM编辑二进制在第二三步之间加入睡觉代码。

14.锁应尽可能细化,但是如果一段代码块中很多很多锁,则粗化。

15.锁定方法和非锁定方法可以同时执行。

16.CAS(compare and set缩写,无锁优化,自旋锁,也叫乐观锁,CPU原语级别的支持所有中间不可以被打断),
java.util.concurrent.atomic.AtomicInteger,凡是类加了Atomic的类都是用了CAS优化,线程安全

扫描二维码关注公众号,回复: 14804437 查看本文章

高并发多线程二

  1. CAS中的ABA问题
    CAS(V,Expected,NewValue),CAS自旋时,如果操作途中Expected值没有改变则会赋予其新的值(NewValue),如果发生改变则会将改变后的值赋给Expected重新进行上一步操作,但是这中间如果有其他线程将Expected的值改变为N之后又改回Expected,则CAS无法识别,称之为ABA问题,基本数据类型时不会有问题,但是引用数据类型则不行,地址还是原来的地址,但是内容可能会有变化,解决这个问题的方法就是加一个版本号(version),同时检查Expected和version。
  2. CAS中多处是用Unsafe这个类保证安全
    Unsave类,只能调用静态方法(getinstance)获取单例对象,直接操作虚拟机里面的内存,具备C++/C类似的能力,比如allocateMemory(分配内存)
  3. LongAdder 分段并发多线程(分段锁)
  4. synchronized是可重入锁,也就是自己重新进入时如果是自己锁定的,则可以直接进去,称为重入
  5. ReentrankLock是可以替代synchronized的,但是Lock需要手动解锁,最后一定是try-catch-finally{Lock.unlock},防止发生错误永远解不了锁
    1、与synchronized相比,ReentrankLock可以try-lock尝试锁,如果锁不到的话可以继续执行2、可以执行lockInterruptibly方法,对interrupt()方法做出响应继续执行,这儿区别于synchronized,synchronized是需要interrupt打断wait()3、是公平锁,先来先得(相对于synchronized竞争而言)
  6. Phaser类,维护着阶段这个变量,
  7. 互斥锁(排它锁),
  8. 日常工程一般不写新的锁,多数用synchronized这种老锁,除非特别追求效率
  9. Semaphore 信号灯,Semaphore s = new Semaphore(N,fair),赋予值N后,每次线程运行时获取s.acquire(),s值都会减小1,线程运行结束时释放s.release(),加1,s减小到0线程则等待,这样保证了同时运行的线程数量不超过N(限流的作用),可以理解只有N条车道,当赛车(线程)很多时最多只能N辆车运行。fair值可有可没有,没有的话默认不公平,如果设置为true则公平,表示队列里线程会排序进入车道(s.acquire())
  10. Exchanger,交换器,假设第一个线程有一个字符串S1,第二个线程有一个字符串S2,当线程内部调用exchanger(s)后,达到两个线程交流交换字符串的目的。
  1. CyclicBarriar,多个线程相互等待,直到都满足条件之后,才能执行后续的操作。CyclicBarrier描述的是各个线程之间相互等待的关系。(以后补充)

学习更多JAVA知识与技巧,关注与私信博主(学习)免费学习领取JAVA 课件,源码,安装包,还有最新大厂面试资料等等等

猜你喜欢

转载自blog.csdn.net/m0_67788957/article/details/123793648
今日推荐