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写起来简单),

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

  • 相关阅读:
    数据库中总结2
    PyMySQL的基本使用
    数据库总结
    并发编程之多线程
    并发编程之多进程知识
    并发编程之多进程
    操作系统基础知识
    模块二总结
    Python函数进阶
    文件操作
  • 原文地址:https://www.cnblogs.com/qyf2199/p/14699737.html
Copyright © 2011-2022 走看看