zoukankan      html  css  js  c++  java
  • JUC 多线程-并发编程

    多线程基础

      一个Java程序实际上是一个JVM进程,

      JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。

      此外,JVM还有负责垃圾回收的其他工作线程等。

      内存角度:单线程相当于栈空间里的函数压栈、串行运行;多线程是每个线程开辟一个栈空间,CPU给多个线程并发运行。

    什么是JUC

     jdk包的三个首字母Java Util Concurrent

    准备工作:

    保证IDEA设置:

    1)File=>Project Structure=>Modules + Project这两个都是java-8版本

    2)File=>Setting=>Build=>Compiler=>Java Compiler是java-8版本


    多线程状态&转换:


     sleep 休眠

      模拟网络延时、倒计时(单位是毫秒ms)

     

      

    TimeUnit.SECONDS.sleep(5);  //休眠

    yield 礼让

      运行态==>就绪态

      

       就绪态回到运行态后,会继续执行run()方法后面的内容

    join 强制执行(插队)

       

    interrupt 中断(他杀)

      thread.interrupt();  //中断线程是一个线程,让另一个thread中断

    wait(配合notify)

    wait必须在同步代码块中(可结合线程状态图来理解)

    (sleep可以在任何地方) 


    jvm中的runnable相当于上图中操作系统状态的running+runnable

    统称为阻塞状态三种:它们分别是 Blocked(同步锁定阻塞)Waiting(等待)Timed Waiting(计时等待) .

     JVM 6状态:

     1) 观察线程状态(6种)

        

    2)获取线程名字:

       

    3)

    创建 多线程(3种方法):

     

    三种方法创建线程:

    1. 继承Thread类,重写run()方法(不推荐,因为java单继承,如果继承Thread就占用了继承的名额)

      

       

       这时候main的线程和TestThread1的线程并发执行

    2. 实现Runnable接口(无返回值)

       

    3. Callable和Future submit接口(有返回值)

     例子:

      

       

    CompleteFuture(Future的优化版):

      

    Callable

    callable和runnable的区别:

    可以有返回值、可以抛出异常

    方法不同:run() 和 call()


     

    线程优先级 (大的优先) 

      


    后台线程 daemon (也叫守护线程)

       

      实例:

       

    同步锁 Synchronized

      1. 同步方法:临界资源 方法名前 加上"synchronized"关键字;加锁对象默认是this

      2. 同步代码块:创建同步的块,将临界资源及其操作放到里面:

    只要在方法前加上一个关键词就好了

        public synchronized void sell(){

    volatile 和 Synchronized 区别

    作用级别:volatile 只用于变量;    变量、方法、类
    可见性&原子性:volatile可见性,不保证原子性;synchronized 修改可见性、原子性
    阻塞:volatile不阻塞;synchronized 阻塞。
    编译器优化:volatile不会优化(内存屏障);synchronized可以优化

    Lock锁

    ReentrantLock 可重入锁 (re entrant lock)

       

       


     Lock锁示例:

        Lock lock = new ReentrantLock();
        public void sell(){
            lock.lock();
            try {
                if(number>0){ //限制票数大于0
                    System.out.println(Thread.currentThread().getName() + "卖出了一张票,还剩下:" + (number--) + "张票。");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        } //lock锁必须成对( 一个lock() + 一个unlock() , 不然会死锁 )

    ReadWriteLock 读锁+写锁  (ReadLock、WriteLock;读锁==共享锁;写锁==独占锁

       

     例子:

       

     读锁、写锁分别使用:

    Lock和Synchronized的区别:

    相同点:

      都是可重入锁

    区别:

      lock是显式锁(手动开关锁);  synchronized隐式锁(出了作用域自动释放)

      lock只有代码块锁,  synchronized有方法锁、代码块锁

      lock锁,JVM调度线程花费小、性能好。并且扩展性好(提供更多子类)

      优先使用顺序:lock>synchronized代码块>synchronized方法

     

    乐观锁、悲观锁

    乐观锁:

    假定不发生冲突,提交时,检查版本、回退。

    CAS(自旋锁:达到预期就改,否则自旋等待)

    ABA问题(A=>B=>A)解决方法用版本机制,AtomicInteger.getStamp()

    悲观锁:

    假定发生冲突,用锁把事务锁起来:

    代码块加锁synchronized

    MySQL排它锁(写锁、X锁)

    2)公平锁、非公平锁

    公平锁:不能插队

    非公平锁(默认):可以插队

    3)可重入锁  ReentrantLock

    可重入锁也叫:递归锁

        => 拿到外面的锁,就自动获得里面的锁

    4)自旋锁  SpinLock

        => 不断循环尝试,直到成功为止

    5)死锁排查

    遇到死锁(不会爆异常),Terminal命令行操作:

        1)使用  jps -l  来定位进程号:

    => 可以找到死锁对应的进程号

        2)使用 jstack + 进程号 来查看具体信息:

      

    进程和线程

    区别

    根本区别:

      进程是操作系统 资源分配 的基本单位,而线程是处理器任务 调度和执行 的基本单位

    资源开销:

      每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

    包含关系:

      如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

    内存分配:

      同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

    影响关系:

      一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮

    执行过程:

      每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行


    多个线程共享进程的:堆 + 方法区资源,

    每个线程有自己的:程序计数器 + 虚拟机栈 + 本地方法栈


    进程:QQ.exe  Music.exe  有java环境的.jar

    java默认有2个线程:main、GC垃圾回收

    开线程方式:Thread(继承)、Runnable(实现接口)、Callable(实现接口+有返回值)

    java无法直接开线程,start() 要调用native本地方法,用底层的C++来操作硬件

    System.out.println(Runtime.getRuntime().availableProcessors());    //获取CPU核数

     并发编程的目的:充分利用多核CPU的资源


    线程间通信

    1)wait + notify 生产者消费者:

    • wait ()
    • notify() /notifyAll(

    ps: notifyAll 更常用

    ps: wait()还有种用法就是wait(1000)这样的加上时间参数,在等待时间结束之后,就不等notify()了

    2)join() 方法==>插队

    使用场景:

      主线程创建并启动子线程,如果子线程中进行大量的运算,主线程往往早于子线程结束。这时主线程要等待子线程完成之后再结束

      比如子线程处理一个数据主线程要取得这个数据中的值,就要用到join()方法

    join()方法就是等待线程对象销毁。

    join的实现其实是基于等待通知机制(wait+notify)。

    3)Volatile (利用 可见性)内存共享

    • 每次修改变量后,立刻回写到主内存。
    • 在此过程中,会通知其他线程读取新值。

    4) 管道通信

    5)三个常用的辅助类=》计数(CountDownLatch、CyclicBarrier、Semaphore

    (1)CountDownLatch 用来倒数

    (2)CyclicBarrier  线程计数器

    (3)Semaphore  信号量


    进程间通信

    套接字(socket)网络通信

    消息队列(messagequeue)

    管道(pipe)

      半双工

      管道分为pipe(无名管道)和fifo(命名管道)两种

      pipe:父子进程

    信号量(semaphore) ==》PV操作(同步、互斥

    共享内存(shared memory)


    生产者消费者

    例子:2个线程交替执行

    /**
     * 线程之间通信问题,线程交替执行
     * wait notify
     * */
    public class Test {
        public static void main(String[] args) {
            MyData myData = new MyData();  //新建对象
            new Thread(()->{
                for(int i=0;i<10;i++){
                    try {
                        myData.increment();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            },"product").start();
            new Thread(()->{
                for(int i=0;i<10;i++){
                    try {
                        myData.increment();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            },"product-2").start();
            new Thread(()->{
                for(int i=0;i<10;i++){
                    try {
                        myData.decrement();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            },"consumer").start();
            new Thread(()->{
                for(int i=0;i<10;i++){
                    try {
                        myData.decrement();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            },"consumer-2").start();
        }
    }
    
    class MyData{//数字 资源类
        private int number =0;
    
        public synchronized void increment() throws InterruptedException {
            while(number != 0)this.wait();  //wait要放在while中     //不能用if,if会虚假唤醒
            number++;
            System.out.println(Thread.currentThread().getName()+ "=>" + "number = " + number);
            this.notifyAll();
        }
        public synchronized void decrement() throws InterruptedException {
            while(number == 0)this.wait();
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number);
            this.notifyAll();
        }
    }

    JUC版 生产者消费者(Condition + lock)

    Synchronized => Lock

    wait + notifyAll => Condition (await + signal)

     1)Condition + lock 替代原有锁的功能

    class MyNewData{//数字 资源类
        private int number =0;
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
    
        public void increment() throws InterruptedException {
            lock.lock();
            try {
                while(number != 0)condition.await();  //wait要放在while中     //不能用if,if会虚假唤醒
                number++;
                System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number);
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
        }
        public synchronized void decrement() throws InterruptedException {
            lock.lock();
            try {
                while(number == 0)condition.await();
                number--;
                System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number);
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
        }
    }

    2)Condition + lock 精准通知唤醒

     例子:3个线程循环

    package ProducerConsumer;
    
    import lombok.Data;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @author 晋青杨 j50016344
     * @create 2021-04-26
     **/
    public class Test_3 {
        public static void main(String[] args) {
            Data3 data3 = new Data3();
            new Thread(()->{
                try {
                    for(int i =0;i<10;i++)data3.printA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"Thread_A").start();
            new Thread(()->{
                try {
                    for(int i =0;i<10;i++)data3.printB();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"Thread_B").start();
            new Thread(()->{
                try {
                    for(int i =0;i<10;i++)data3.printC();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"Thread_C").start();
        }
    }
    
    @Data
    class Data3{
        private Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();  //Alt+Enter自动生成等号左边
        Condition condition2 = lock.newCondition();
        Condition condition3 = lock.newCondition();  //【重点】 建立3个condition,每个都具有自己的await和signal,可以精准的等待、唤醒private int number =1; //1A  2B  3C
    
        public  void printA() throws InterruptedException {
            lock.lock();
            try {
                while(number!=1)condition1.await(); //1等待
                System.out.println(Thread.currentThread().getName() + "=> A");
                //唤醒2:
                number =2;
                condition2.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
        }
    
        public  void printB() throws InterruptedException {
            lock.lock();
            try {
                while(number!=2)condition2.await();
                System.out.println(Thread.currentThread().getName() + "=> B");
                //唤醒3:
                number =3;
                condition3.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
        }
    
        public  void printC() throws InterruptedException {
            lock.lock();
            try {
                while(number!=3)condition3.await();
                System.out.println(Thread.currentThread().getName() + "=> C");
                //唤醒1:
                number =1;
                condition1.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
        }
    }

    8锁问题

    /**
     * ====== 8锁:关于锁的8个问题 ======
     * 1) 在中间有1s间隔情况下,两个线程是先 send 还是 call ? 先send => 因为先在synchronized那里抢到锁,就会先执行 => send第0s  call第1s
     * 2) send在1)的基础上,方法内部追加3s的延迟,两个线程是先 send 还是 call ? 先send => 补充:send和call都是在第3s执行
     * 3) 新定义一个不加锁的方法:hello,然后线程B来调用此方法。=> 先hello,再send => hello在第一秒运行到,此时也不用抢锁,直接执行
     * 4) 如果两个实例对象,就会有两个不同的锁;两个对象互不影响
     * 5) 6)将类中的方法用static修饰,于是锁的是类,于是两个实例对象也会共用一把锁
     * 7) 8)一个方法是 静态加锁; 另一个是 普通加锁 => 用的是两个锁,只有静态的方法才会参与类加锁
     * 上面的 5)7)是一个实例对象;  6)8)是两个实例对象
     * */
    
    public class lock8 {
        public static void main(String[] args) throws InterruptedException {
    //        Phone phone1 = new Phone();
    //        Phone phone2 = new Phone();
            Phone phone = new Phone();
            new Thread(()->{
                try {
                    phone.sendMessage();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"Thread_A").start();
    
            TimeUnit.SECONDS.sleep(1);
    
            new Thread(()->{
                phone.hello();
            },"Thread_B").start();
        }
    
    }
    
    class Phone{
        // synchronized锁的对象是:方法的调用者 => 调用者:普通情况下是对象,加static后是类
        public static synchronized void sendMessage() throws InterruptedException {
            TimeUnit.SECONDS.sleep(2);  //由于这里会等2s所以:如果共用一把锁,则send会先执行,否则别的方法先执行
            System.out.println("send message");
        }
        public synchronized void call(){
            System.out.println("call");
        }
        //没有锁的方法:
        public void hello(){
            System.out.println("hello");
        }
    }

    集合类不安全

    Java STL内置的线程安全的Concurrent Collection:

              

    Atomic原子操作的封装类

      

    getAndIncrement()  //num++

    IncrementAndGet()  //++num

    • 原子操作实现了无锁的线程安全;

    • 适用于计数器,累加器等。

    AtomicInteger

      利用 CAS (compare and set) + volatile 来保证原子操作,

      从而避免 synchronized 的高开销,执行效率大为提升。

    UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。

    另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

    CAS(CompareAndSet,比较并更新)

        => 如果期望的expect值达到了,就set;否则就不更新,并一直循环等(自旋锁)

    CAS缺点:

    1、循环会耗时

    2、一次性只能保证一个共享变量的原子性

    3、ABA问题

    原子引用 解决ABA问题

    ABA问题就是:A->B->A,其他线程以为没有变化,但实际上是改变过了的

    解决方法就是用时间戳来判断有没有被动过

     

     CAS比较并更新的对象是:

      值+stamp 2个内容


    ConcurrentHashMap原理

    HashMap:

    Map<String, String> map = new HashMap<String, String>(16, 0.75f);
    // HashMap 加载因子,初始容量

    ConcurrentHashMap并发原理:

    jdk1.7:

      Segment + HashEntry 数据结构,对Segment加锁;各个Segment之间互不影响

    jdk1.8:

      数组 +(链表 / 红黑树),对链表头结点 / 红黑树根节点  加锁;

      各个(链表 / 红黑树)之间互不影响

    CAS + synchronized实现更加细粒度的锁

    JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?★★★★★

    • 在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
    • 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

    读写锁  ReadWriteLock

    读写分离,读可以并发,写只能用一个线程。

    阻塞队列 BlockingQueue

    BlockingQueue是Collection下和Set、List平级的类

    运用:多线程并发处理,线程池

    线程池

    优点:

    • 避免重复创建、销毁线程;提高性能
    • 便于线程管理(可以控制最大并发数、时间等)

     线程池必会:3大方法、7大参数、4种拒绝策略

    三大线程池:

    SingleThreadExecutor:单线程池

    FixedThreadPool:线程数固定的线程池

    CachedThreadPool:线程数动态调整

     => 3种方法只是封了一层,使得输入的参数减少了。源码中都是通过ThreadPoolExecutor来产生的线程池,只是3种之间的参数不一样。

    7大参数:(3个核心参数)

    (建议用 ThreadPoolExecutor 7大参数来自定义,而不是 Executors 去创建,否则可能会导致 OOM。)

        public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小int maximumPoolSize,  //最大线程池大小。定义策略:CPU密集型=>几核就定义几个(如12个) 
                                  long keepAliveTime,  //超时没有使用 就会释放
                                  TimeUnit unit,    //超时 时间单位
                                  BlockingQueue<Runnable> workQueue,  //阻塞队列(含同步队列)
                                  ThreadFactory threadFactory,  //线程工厂:固定用于创建线程
                                  RejectedExecutionHandler handler) { //拒绝策略:超过maximumPoolSize之后的拒绝策略   //7大参数

    如果:需要的线程数 > 最大线程池(获取到线程的个数) + 阻塞队列(排队中),就会触发拒绝策略

      

     4种拒绝策略:

      Abort【默认】:超出线程,会抛出异常

      CallerRuns:超出线程,会还给main或者调用它的地方

      Discard:直接丢掉多余的任务,不抛异常

      DiscardOldest:尝试与最早使用线程池的线程竞争,而不是直接丢掉。也不会抛异常

    最大线程池大小,定义策略(调优):

    1)CPU密集型 =>几核就定义几个(如12个)

    int coreNum = Runtime.getRuntime().availableProcessors(); //因为不同的电脑不一样,通过此方法动态获取CPU核数
    System.out.println("coreNum = " + coreNum);

    2)IO密集型 => 比如有15个大型io密集任务,就开辟大于任务数的线程数

    设置线程池大小maximumPoolSize为 约2倍 的任务数即可。

    Java8新特性

    四大 函数式接口(重点)

    函数式接口 Function、断定型接口 Predicate、消费型接口 Consumer、供给型接口 Supplier

    1)函数型接口Function:只有一个方法的接口

    典型例子:

    @FunctionalInterface
    public interface Runnable {
        public abstract void run();
    }

    简化编程模型,在新版本的框架底层大量应用

    函数式编程  例子:

    import java.util.function.Function;
    
    public class Test {
        public static void main(String[] args) {
            Function function = new Function<String,Integer>() {  //泛型中一个入参 + 一个出参,与里面重写的apply的入参、出参类型对应
                @Override
                public Integer apply(String str) {  //与上方的类型对应
                    return str.length();
                }
            };
            System.out.println(function.apply("2199"));
        }
    }

     简写为lambda表达式:

    Function<String,Integer> function = (str)->{ return str.length(); };

    2)断定型接口Predicate:

    有一个输入值,和一个返回的bool值(用于判断返回真伪)

    Predicate <String> predicate = s -> {return s.isEmpty();};  //lambda表达式来写
    System.out.println(predicate.test(""));

    3)消费型接口Consumer:

    只有入参,没有返回值

    Consumer <String> consumer =(str)->{ System.out.println("打印内容:" + str); };
    consumer.accept("2199");

    4)供给型接口Supplier:

    没有入参,只有返回值

    Supplier<String> supplier = ()->{return "2199";};
    System.out.println(supplier.get());

    Stream流式计算

    大数据:存储 + 计算

      存储 => 集合、MySQL

      计算 => 交给流

    public class StreamTest {
        public static void main(String[] args) {
            User u1 = new User(1,"a",21);
            User u2 = new User(2,"b",22);
            User u3 = new User(3,"c",23);
            User u4 = new User(4,"d",24);
            User u5 = new User(5,"e",25);
            User u6 = new User(6,"f",26);
            User u7 = new User(7,"g",27);
            //集合用于存储
            List<User> list = Arrays.asList(u1,u2,u3,u4,u5,u6,u7); //记住这套路
            System.out.println("list = " + list);
    
            /**
             * 题目要求:
             * 1) id是奇数
             * 2) age大于22
             * 3) 用户名转换为大写
             * 4) 按照用户名倒叙排列
             * 5) 限制输出2个用户
             * */
    
            //计算用流stream
            list.stream()
                    .filter(u-> u.getId()%2==1)  //.filter里面直接放bool条件
                    .filter(u-> u.getAge()>22)
                    .map(u->{u.getName().toUpperCase(); return u;})
                    .sorted((x,y)->{return y.getName().compareTo(x.getName());}) //倒序
                    .limit(2)
                    .forEach(System.out::println);
        }//流式计算 + 链式编程 + lambda表达式 + 函数式接口, jdk-8的四大要素都有了
    }

    jdk-8 时代程序员:lambda表达式、函数式接口、链式编程、Stream流式计算

    分支合并 ForkJoin(jdk1.7)

     ForkJoin在jdk1.7,并行 执行任务,提高效率,大数据量

    大数据MapReduce:把大任务拆分成小任务

     ForkJoin特点:工作窃取 => 一个线程干活太快,把别的线程的任务抢过来

     双端队列Deque:从两边都可以取出来

     代码理解:

    public class Test {
        @org.junit.Test
        public void test1(){
            long startTime = System.currentTimeMillis();
            Long sum = 0L;
            for (Long i = 0L; i<=10_0000_0000; i++){  //10_0000_0000量级
                sum += i;
            }
            long endTime = System.currentTimeMillis();
            System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //朴素计算=>5.4秒
        }
    
        @org.junit.Test
        public void test2() throws ExecutionException, InterruptedException {
            long startTime = System.currentTimeMillis();
    
            ForkJoinPool forkJoinPool = new ForkJoinPool();
            ForkJoinTest forkJoinTest = new ForkJoinTest(0L, 10_0000_0000L);
            ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);
            Long sum = submit.get();
    
            long endTime = System.currentTimeMillis();
            System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //ForkJoin方法(类似递归)=>3.6秒
        }
    
        @org.junit.Test
        public void test3() throws ExecutionException, InterruptedException {
            long startTime = System.currentTimeMillis();
            Long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0,Long::sum);
            long endTime = System.currentTimeMillis();
            System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //Stream方法=>0.14秒
        }
    
        @org.junit.Test
        public void test4(){
            long startTime = System.currentTimeMillis();
            Long sum = (0L+ 10_0000_0000L) * (10_0000_0000L-0 + 1) /2;
            long endTime = System.currentTimeMillis();
            System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //公式作弊方法=>0毫秒
        }
    }
    //在10_0000量级:
    //朴素计算6ms; ForkJoin用时8ms Stream用时45ms =>和上面的情况完全反过来了
    //所以不在大数据量的情况下,Stream不一定就很快

    异步回调Async

    类似于Ajax(C与S之间) ,不过这里是Java内部的异步调用。

    public class Future {
        @Test
        public void FutureTest1() throws ExecutionException, InterruptedException {
            CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
                try {
                    TimeUnit.SECONDS.sleep(2);//异步线程耗时,就会返回到主线程继续执行,等到有结果了才会返回来sout
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "runAsync");
            });
            System.out.println("在主线程ing...");
            completableFuture.get();    //获取阻塞执行结果
        }
        
        @Test
        public void FutureTest2() throws ExecutionException, InterruptedException {
            CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
                System.out.println(Thread.currentThread().getName());
    //            int x = 2/0; //导致异常的语句
                return 2199;
            });
            completableFuture.whenComplete((t,u)->{
                System.out.println("success.");  //正常返回结果
                System.out.println("t=>" + t);
                System.out.println("u=>" + u);
            }).exceptionally((e)->{
                System.out.println("exception.");  //错误返回结果
                System.out.println(e.getMessage());
                e.printStackTrace();
                return 2333;
            });
        }
    }

    volatile

    1)保证可见性

    2)不保证原子性

    3)禁止指令重排

    JMM(Java 内存模型)(引出volatile)

    指令重排:为了提高性能编译器和处理器常常会对指令做重排序。

    内存屏障:用于保证指令顺序执行。内存屏障分为 LoadLoad、StoreStroe、LoadStore、StoreLoad 四种。(用在volatile前后加)

    一、保证可见性

    • 每次修改变量后,立刻回写到主内存。
    • 在此过程中,会通知其他线程读取新值。

    (不然各个线程,存有一个值的  不同副本)

       

    二、不保证原子性

    原因:

    例如你让一个volatile的integer自增(i++),其实要分成3步:

    1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回,让其它的线程可见。

    这3步的jvm指令为:

     在内存屏障之前的几步都是不安全的 ==》所以不保证原子性

    解决方法:Atomic类

    // 2)volatile 不保证原子性,所以就使用Atomic和CAS来保证原子性
    public class Test2 {
    //    private volatile static int num =0;
        private static AtomicInteger num = new AtomicInteger(); //使用原子类的Integer
    
        public static void add(){
            //num++; //不是一个原子方法
            num.getAndIncrement();  //AtomicInteger的+1方法  //用的是底层CAS方法(见下方)  //比锁高效非常多倍
        }
    
        public static void main(String[] args) {
            for (int i =0;i< 20;i++){
                new Thread(()->{
                    for(int j =0;j<100_0000;j++){
                        add();
                    }
                }).start();
            }
            while(Thread.activeCount()>2){  // main + gc留下,其他都停掉;相当于finally打扫战场
                Thread.yield();
            }
    
            System.out.println(Thread.currentThread().getName() + num);
        }
    }
    //不保证原子性的后果:多线程修改num时,会导致最终计算结果经常不正确(例如下图的例子中,结果就不是正确结果:)

    下图中,左边使用AtomicInteger,右边使用普通int(计算结果不正确)

      

    Atomic机理CAS(CompareAndSet,比较并更新)

        => 如果期望的expect值达到了,就set;否则就不更新,并一直循环等(自旋锁)

    CAS缺点:

    1、循环会耗时

    2、一次性只能保证一个共享变量的原子性

    3、ABA问题

    public class CAS {
    
        public static void main(String[] args) {
            AtomicInteger atomicInteger = new AtomicInteger(2020);
            //如果期望的expect值达到了,就set,否则就不更新。CAS是CPU的并发原语
            atomicInteger.compareAndSet(2020,2021);
            System.out.println(atomicInteger.get());  // true 2021
            atomicInteger.compareAndSet(2020,2021);
            System.out.println(atomicInteger.get());  // false 2021
        }
    }

    unsafe类:

    ABA问题:版本号解决

    ABA问题就是:A->B->A,其他线程以为没有变化,但实际上是改变过了的

    解决方法就是用时间戳来判断有没有被动过

     

     CAS比较并更新的对象是:

      值+stamp 2个内容

     三、禁止指令重排

    在符合上下指令之间的依赖性的前提下,编译器+执行器,会进行重排。

        源代码-->编译器 优化重排-->指令并行 可能会重排-->内存系统 重排 -->执行

     volatile写 的前后,加内存屏障,避免指令重排现象。

    volatile两种功能:

    1)可见性

      每个线程都有一块单独内存,存储的共享变量会有不同副本

    2)禁止(编译器)指令重排、顺序优化

      由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。

      这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。

    volatile 和 Synchronized 区别

    作用级别:volatile 只用于变量;    变量、方法、类
    可见性&原子性:volatile可见性,不保证原子性;synchronized 修改可见性、原子性
    阻塞:volatile不阻塞;synchronized 阻塞。
    编译器优化:volatile不会优化(内存屏障);synchronized可以优化

    深入单例模式(synchronized + volatile)

    单例模式:一个类只能构造一个实例对象("构造器私有")

    场景:

      Windows任务管理器、回收站

      项目中,配置文件的类,一般只有一个对象

      Spring中的Bean(缓存中取bean很快,减少jvm垃圾回收)(当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象)

    1)饿汉式单例

      ==》上来直接new对象,所有类实例化。坏处是:大量浪费不必要的资源(因为很多类   不需要实例化)

    2)懒汉式单例 

       ==》按需创建;如果单例已经创建,会返回之前创建的对象。

        线程不安全:多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,

        因此需要加锁解决线程同步问题( Synchronized 同步锁来修饰 getInstance 方法)


    三种线程安全的方法:

      

    方法一:双重锁检测 DCL

      ==》首先将类加同步锁(syn),但是new语句不是原子操作,所以对了类的实例加volatile锁(可见性)

    第一把锁:synchronized锁

    第二把锁:volatile锁

     

      

    方法二:静态内部类

    public class Singleton {
        private static class SingletonHolder {
             /**
             * 静态初始化器,由JVM来保证线程安全
             */
            private static Singleton instance = new Singleton();
        }
        private Singleton(){}
        public static Singleton getInstance(){
            return SingletonHolder.instance;
        }
    }

      

    方法三:枚举

      ==》直接把类名前的class替换成enum就好了,因为枚举无法反射

     

     

    比较: 

    使用选择:

    一般情况下直接使用饿汉式就好了,

    如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类(比DCL写起来简单),

    如果涉及到反序列化创建对象时会试着使用枚举方式来实现单例。

  • 相关阅读:
    Nim or not Nim? hdu3032 SG值打表找规律
    Maximum 贪心
    The Super Powers
    LCM Cardinality 暴力
    Longge's problem poj2480 欧拉函数,gcd
    GCD hdu2588
    Perfect Pth Powers poj1730
    6656 Watching the Kangaroo
    yield 小用
    wpf DropDownButton 源码
  • 原文地址:https://www.cnblogs.com/qyf2199/p/14699737.html
Copyright © 2011-2022 走看看