• 【Java入地】 01 多线程与高并发


    多线程与高并发 synchronized 篇

    进程 线程 协程/纤程(Quasur)




    线程:一个程序里不同的执行路径

    public static class T1 extends Thread{
      @Override 
      public void run(){
        System.out.println("Override Theme 中的 run 方法");
      }
    }
    // 方法一
    new MyThread().start();
    // 方法二
    new MyThread().start();
    // 方法三
    new Thread(()->{
    	Sout("Hello World!");
    })
    
    • 创建线程的两种方式:

      • 创建一个类,继承Thread,重写方法
      • 定义一个类,实现 Runnable 接口,然后重写 run 方法
    • 启动:

    //方法一:
    new MyThread().start();
    
    //方法二:
    new Thread(new MyRun()).start();
    
    //方法三:
    new Thread(()->{ Sout("Hello World!");});
    
    • 面试
      • 问:启动线程的三种方式是?
      • 答:
        • 1、从 Thread 继承
        • 2、实现 Runnable 接口
        • 3、从线程池中启动 Executors.newCachedThrad



    线程的基本方法

    在T1中调用 T2.join();则执行到 join 之后,T1进入等待模式,先执行完T2之后,再返回执行T1。


    // sleep 睡眠
    Thread.sleep(500); // 毫秒
    
    // Yield 让出一下CPU 进入等待队列(如果没有等待的则继续执行)
    // 使用场景,较少
    Thread.yield();
    
    // Join
    Thread T1 = new Thread(()->{
        T1.join();
    })
    Thread T2 = new Thread(()->{
        Sout("T2");
    })
    
    • 其他方法
    // 暴力结束线程(不建议使用)
    .stop()
    
    // 唤起线程
    .intereptor( )
    
    // 获取线程状态。
    .getState()
    



    线程的锁


    1. 上锁
    private int count = 10;
    private Object o = new Object();
    
    public void m(){
        synchronized(0){ //任何县城要执行下面的代码,则必须先拿到o
            count--;
        }
    }
    // synchronized(this) 等值于 synchronized(方法);
    
    
    public class T{
        private static int count  = 10;
        public synchronized static void m(){ // 等同于synchronized(T.class)
        	count --;
    	}
        public static void mm(){
            synchronized(T.class){
                count--;
            }
        }
    }
    



    synchronized的特性

    1、锁的是对象,不是进程  / 线程 ; 2、能不加synchronized()锁就不加,加锁之后效率极低;


    • 可重入性

      • 一个方法m1加锁,另一个方法m2也加了锁(同一把锁),那么m1是可以调用m2的。
    • 异常的锁

      • 程序中的锁的内容出现了异常,那么该锁将被释放



    synchronized 底层实现

    synchronized(Object) ;  括号中一定要是Object对象,不能是String 或其他任何类型;


    早期的锁:需要就去找操作系统申请、

    发展后的:先乐观锁、后自旋锁、最后找系统实现(重量级锁 | 最浪费时间)

    • 抱着“没有线程跟我争用”的心态去申请一个资源:此时是 偏向锁,只记录ID,不锁(默认没有第二个线程来访问)
    • 如果有线程争用:升级为 自旋锁 循环10次(占用CPU)
    • 10次之后要访问的资源还被锁着?升级为重量级锁 去操作系统申请锁(不占用 CPU)

    自旋锁:在用户态解决问题,不经过内核态。

    执行时间长的用系统锁,(加锁代码)执行时间特别短,线程较少,用自旋锁。




    总结


    • Lock( ) CAS使用自旋
    • synchronized 是一种锁,其锁的目标是 对象 而不是线程 / 进程(用对象代替进程更易操作)
    • 被锁的对象 必须是 Object 类型,不能是 String 或其他对象
    • 锁申请资源的时候一定是 :乐观锁
    • 锁第一次升级的时候一定是:自旋锁(自旋十次,耗CPU,不走内核)
    • 锁第二次升级的时候一定是:重量级锁(内核态,耗内核、耗时,不耗CPU)





    多线程与高并发 2 代码优化 及 volatile修饰 篇




    volatile 指令介绍

    偏向锁  >  循环锁  >  重量级锁


    • 指令介绍

      • volatile // 可变的,易变的
    • 指令功能

      • 保证线程可见性,禁止指令重排序。

      • 保证线程可见性:一个类的值给两个类同时调用,里面的变量改变后无法轻易发现(线程之间不可见)。

        volatile可以让一个线程发生改变之后,另一个线程可以马上知道。

        // 原理:CPU的缓存一致性协议。

      • 禁止指令重排序:CPU迸发执行指令,所以会对指令重新排序,加了volatile来保证重排序。




    举例介绍 及 代码优化


    • 饿汉式:(定义类的时候就实例化方法)
    public class Mgr01{
      private static final Mgr01 INSTANCE = new Mgr01();
      private Mgr01(){};
      public static Mgr01 getInstance(){return INSTANCE;}
      public void m() {System.out.println("m");}
      public static void main(String[] args){
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
      }
    }
    
    • 懒汉式:什么时候调用方法什么时候初始化(类似于懒加载)
    public class Lazy{
        private Lazy(){}
        //默认不会实例化,什么时候用什么时候new
            private static Lazy lazy=null;
            public static synchronized Lazy getInstance(){
                if(lazy==null){
                    lazy=new Lazy();
            	}
            	return lazy;
        	}
    }
    
    饿汉式 懒汉式
    安全
    节省内存
    • 懒汉饿汉合并:

    类的定义:

    public class Mgr01{
    private /*volatile*/ static Mgr0x INSTANCE;
    private Mgr0x(){};
    public static Mgr01 getInstance(){
    	//以下所有代码写的都是这一个方法
    }
    }
    

    以下所有方法写的都是上面的 getInstance()方法。

    以上方法没有加volatile,最后会写上。


    • 直接判断null
    // 先判断是否为空 然后再那啥:
    public static Mgr03 getInstance(){
        if(INSTANCE == null){
            try{
                Thread.sleep(1);
            }catch(InterruotedException e){
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }
    

    ↑ ↑ ↑ ↑ ↑ 这是一种错误的书写方式,自己抿;

    • 先锁再null
    public static synchronized Mgr04 getInstance(){
        if(INSTANCE == null){
            try{
                Thread.sleep(1);
            }catch(InterruotedException e){
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }
    

    ↑ ↑ ↑ ↑ ↑ 修改正确,但是违背了 能不加锁就不加锁 原则。

    • 锁细化:
    public static Mgr05 getInstance(){
        if(INSTANCE == null){
            synchronized (Mgr05.class){
                try{
                    Thread.sleep(1);
                }catch(InterruotedException e){
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }
    

    ↑ ↑ ↑ ↑ ↑ 这也是一种错误的书写方式(重复初始化);

    • 双重检查:
    public static Mgr05 getInstance(){
        if(INSTANCE == null){
            synchronized (Mgr05.class){
                if(INSTANCE == null){
                    try{
                        Thread.sleep(1);
                    }catch(InterruotedException e){
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr05();
                }
            }
        }
        return INSTANCE;
    }
    

    ↑ ↑ ↑ ↑ ↑ 修改正确.......而且锁不加载外面,效率增高~~

    • 关于volatile(主要是 指令重排序 )超高超高迸发的情况可能会发生:
    // new对象的三步
    INSTANCE = new Mgr06();
    
    1. 申请内存并给初始值(int = 0,String = null;)
    2. 修改值
    3. 将值给对象
    

    volatile 防止第二步第三步会颠倒;

    • 一个求结果是 100000 的小程序
    public class T{
        volatile int count = 0; // 加上vilatile
        synchronized void m(){  // 加上 synchronized
            for(int i=0;i<10000;i++){count++;}
        }
    
    
        public static void main(String[] args){
            T t = new T();
            List<Thread> threads = new ArraysList<~>();
    
            for(int i=0;i<10;i++){
                threads.add(new Thread(t::m,"threads-"+i));
            }
    
            threads.forEach((o)->o.start());
    
            threads.forEach((o)->{
              try{
                  o.join();
              } catch(InterruptedException e){
                  e.printStackTrace();
              }
            });
                System.out.println(t.count);
        }
    }
    

    只有加上了 synchronized & volatile 才能运行出正确结果,其中 synchronized 用来保证原子性




    锁优化场景


    • 锁力度变小(争用不是很激烈的话)

    如果有一群要争用的代码,那么可以将方法上的 synchronized 写到 count++ 上;

    • 锁力度变大(争用很激烈很频繁的话)

    假如一个方法里面 总共 20 行代码,加了19个锁,那不如直接用一个大的锁。




    锁的对象被调用


    public class = T{
        Object o = new Object();// 错误修改点
        
        synchronized(0){
            sout("123");
        }
    
        public void zbc(){
            T t = new T();
            t.o = "a";
        }
    

    ↑ ↑ ↑ 以上代码错误!以下为修改 ↓ ↓ ↓

    final Object o = new Object();
    



    有些类在创建的时候直接加了锁


    Atomic 开头的 ( AtomicInteger count = new AtomicInteger( ); // 让count进行原子性加减)




    CAS ( Compare And Set ) 无锁优化 乐观锁

    在请求的时候就乐观的认为 代码里的值就是我的期望值


    cas (V ,Expected,NewValue){
        if (V == Expected){
            V = NewValue;
    	}else{
            tryAgain or fail;
        }
    }
    

    ↑ ↑ ↑ 以上是在CPU 原语上的支持,不能被打断。




    ABA 问题(与前女友复合之后,其实她已经经历了n个男人;)


    有个对象 object == 1;想使用cas把它变成2:

    cas(object,1,2);//没有线程进行操作,可以进行更改
    

    如果在更改的时候有一个线程给 object 改成了2,然后又改成了 1 ;在基础类型(如:int)没有影响,但是 Object 对象有影响;

    解决方法:做 cas 的时候加个版本号:version

    解决方法:使用 AutomicStampedReference ( unsafe 什么时候调用什么时候返回这个值 )




    思考


    • 什么是 volatile ? 它有什么用?
    • 什么是 synchronized ? 什么是 CAS ?两者有什么区别?分别在什么场景下使用?
    • 什么时候要对锁进行细化?什么 时候进行泛化?
    • 什么是 ABA 问题?有什么影响?怎么解决?






    各式锁的实际应用

    公平锁?不公平锁?乐观锁?悲观锁?自旋锁?重量级锁?读写锁?




    乐观锁 cas

    (要改的对象,期望的值,要给的值)无锁操作,其实是一个乐观锁......cas本身可以看成是一个锁;


    • automic : 一种使用 cas 实现的原子性操作(上篇中提过)



    原子操作的简单方法:


    函数 效果 备注
    AtomicInteger a = new AtomicInteger(0); int a = 0; 创建对象a并且赋初值为0;
    a.incrementAndGet( ); i++; 对原值+1后返回;
    a.getAndIncrement( ); ++i; 对原值返回后+1;
    a.addAndGet(i); a+=i; 返回a+i;
    a.getAndAdd(i); a+=i; 返回原值之后给a+i;

    在线程很多的情况下:LongAdder(分段锁:在线程多的时候有优势) > Atomic > synchronized。




    Synchronized 的可重入性:


    //可重入:
    synchronized void m1(){
    for(int i = 1;i<10;i++){
    try{
    	TimeUtil.SECONDS.sleep(1);// 睡一秒
    	}catch(InterruptedException e){
    	e.printStackTrace();
    	}
    sout(i);
    }
    
    }
    
    synchronized void m2(){sout("m2...");}
    
    public static void main(String[] args){
    T01_ReentrantLock1 r1 = new T01_ReentrantLock1();
    new Thread(r1::m1).start();
    try{
    TimeUtil.SECONDS.sleep(1);// 睡一秒
    }catch(InterruptedException e){
    e.printStackTrace();
    }
    new Thread(r1::m2).start();
    }
    
    

    输出结果:0 1 23 4 5 6 7 8 9 m2...

    代码修改:synchronized

    //可重入:
    synchronized void m1(){
      for(int i = 1;i<10;i++){
        try{
        	TimeUtil.SECONDS.sleep(1);// 睡一秒
        }catch(InterruptedException e){
          e.printStackTrace();
        }
        sout(i);
        if(i == 2){
        	new Thread(r1::m2).start();
        }
      }
    }
    
    synchronized void m2(){sout("m2...");}
    
    public static void main(String[] args){
      T01_ReentrantLock1 r1 = new T01_ReentrantLock1();
      new Thread(r1::m1).start();
      try{
        TimeUtil.SECONDS.sleep(1);// 睡一秒
      }catch(InterruptedException e){
        e.printStackTrace();
      }
    
    }
    

    输出结果:0 1 2 m2 ... 3 4 5 6 7 8 9




    lock():替代 synchronized 的方法;


    Lock lock = new ReentrantLock();
    
    • 特点:

      • 需要手动上锁 lock.lock( );
      • 需要手动解锁lock.unlock( );
      • 防止进程出错而导致死锁,需要try{ …… }catch( ){ …… }

    • 优点:

      • 可以使用tryLock()尝试上锁;

      • synchronized遇到锁之后只能等待,而tryLock()可以自定义等待时间;

      • locked = lock.tryLock(SECONDS(时间长度),TimeUtil.SECONDS(时间格式:秒));
        

    • 常用方法:

    方法 参数 用法
    .lock( ); null 锁定
    .unlock( ); null 释放
    .tryLock(n,TimeUtil.SECONDS); 时间长度
    时间单位
    等待参数时间过程中:
    如果当前进程释放了,则锁定;
    不释放则不锁定;
    .lockInterruptibly( ); null; 可以相应被打断的锁;
    .interrupt( ); Null; 打断这个锁;



    公平锁

    ReetrantLock lock = new ReentrantLock( true );


    • 概念:
      • 当执行队列中有线程正在排队的时候:
        • 公平锁:继续等待,排队执行;
        • 不公平锁:不等待,直接抢,有可能抢到第一个执行;
    • 创建方式:
      • 在创建锁的时候加个 true 创建出来的就是公平锁;
    public class T05_ReentrantLock extends Thread(){
    	private stratic ReentrantLock lock = new ReentrantLock(true);
      public void run(){
        for(int i = 0;i<100;i++){
          lock.lock();
          try{
            Sout(Thread.currentThread().getName()+"获得锁");
          }finally{
            lock.unlock();
          }
        }
      }
    }
    



    一个倒计时的门栓 CountDownLatch


    CountDownLatch latch = CountDownLatch( threads.length ); //创建一个length长度的门栓

    .await() 阻塞

    原join() 当前线程结束自动往前走

    .countDown() 原子性--




    栅栏工具 CyclicBarrier

    循环栅栏工具


    // 一个参数:不到20的时候,等待,到了20个,这20个发车,再来的继续等待
    CyclicBarrier barrier = new CyclicBarrier(20);
    // 两个参数:
    CyclicBarrier barrier = new CyclicBarrier(20,run);
    run(){ Sout("满员,发车!"); }
    //lambdo 表达式
    CyclicBarrier barrier = new CyclicBarrier(20,()->Sout("满员,发车!"));
    



    同步进行的 Phaser

    按照不同的阶段对线程进行划分。


    • 使用场景:

      • 遗传算法
      • 现实生活一步一步执行的场景(如:婚礼)
      • 像是一个一个栅栏一样

    • 使用方法:

      • 自定义一个类,继承 Phaser类;

        static class MarrigePhaser extends Phaser

      • 重写onAdvance方法;(栅栏被推倒的时候自动调用)

        protected boolean onAdvance(int phase,int registeredParties)


    • 方法:

      phaser.arriveAndAwaitAdvance();	//执行结束,开始等待;
      phaser.arriveAndDeregister();	//执行结束,不进入下一阶段;
      



    读写锁

    程序中的读写锁(一种排他锁、共享锁)


    • 概念

      • A进程在读取ABCD的时候,B进程也来读取ABCD,同时发现A进程在读取,则读取成功;
      • A进程在读取ABCD的时候,B进程来修改ABCD,同时发现A进程在读取,若此时更改ABCD的内容,则A进程读取会出问题,所以修改失败;
      • 总结:两个都是读取的进程可以同时进行,当有 读 进程在进行时,无法进行 进程,写同理;
    • 作用

      • 避免 / 减少 脏数据
    static ReadWriteLoak readWriteLock = new ReentrantReadWriteLock();
    //在 ReentrantReadWriteLock 中 分出一个 `readLock`一个`writeLock`
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();
    
    public static void read(Lock lock){
        try{
            lock.lock();
            Thread.sleep(1000);
            Sout("read over!");
            // 模拟读取过程
        }catch(InterruptedException e){
            e.peintStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    public static void write(Lock lock,int a){
        try{
            lock.lock();
            Thread.sleep(1000);
            Sout("write "+ a +"over!");
            // 模拟读取过程
        }catch(InterruptedException e){
            e.peintStackTrace();
        }finally{
            lock.unlock();
        }
    }
        
    public static void main(String[] args){
        
        Runnable readR = ()->read(lock);    
        //Runnable readR = ()->read(readLock);
        
        Runnable write = ()->write(lock,new Random().nextInt());
        
        for (int i=0;i<18;i++)new Thread(readR ).start();
        for (int i=0;i<2 ;i++)new Thread(writeR).start();
    	}
    }
    

    // 如果使用 ReentrantLock的话,以上代码在执行的时候也需要等待一秒;

    // 解决方法:将Main方法中的锁换成`Runnable readR = ()-> read(readLock);





    Semaphore 一个有意思的线程池

    Semaphore s = new Semaphore(x);x是几则这个 < 线程池 > 就 允许几个线程 同时执行。


    public static void main(String[] args){
        Semaphore s = new Semaphore(1);
        //括号中数字为x时,允许x个线程同时执行
        
        // T1 Running
        new Thread(()->{
            try{
                s.acquire();
                // 进来一个进程 1 变成 0 ,别的线程不能执行
                Sout("T1 Running");
                Thread.sleep(200);
                Sout("T1 Running");
            }catch(InterruptedException e){
                e.printStackTrace();
            }finally{
                s.release();
                // 离开一个进程 0 变成 1 ,别的线程可以执行
            }
        });
        
            // T2 Running
            new Thread(()->{
            try{
                s.acquire();
                // 进来一个进程 1 变成 0 ,别的线程不能执行
                Sout("T2 Running");
                Thread.sleep(200);
                Sout("T2 Running");
            }catch(InterruptedException e){
                e.printStackTrace();
            }finally{
                s.release();
                // 离开一个进程 0 变成 1 ,别的线程可以执行
            }
        });
    }
    

    如果x==1则运行结果是T1 T1 T2 T2,否则可能是T1 T2 T1 T2



    Exchanger 用于 < ! 两个 ! > 线程交换数据的方法

    使用场景:双人游戏中两人交换装备!执行一次就失效,可以循环等待下一次;


    public static void main(String[] args){
        // T1
        new Thread(()->{
            String s = "T1";
            try{
                s = sxchanger.exchange(s);
            }cathc(InterruptedException e){
                e.printStackTrace();
            }
            Sout(Thread.currentThread().getName()+""+s);
        },"t1").start();
        
        // T2
        new Thread(()->{
            String s = "T2";
            try{
                s = sxchanger.exchange(s);
            }cathc(InterruptedException e){
                e.printStackTrace();
            }
            Sout(Thread.currentThread().getName()+""+s);
        },"t2").start();
    }
    

    线程中有两个变量,分别是 s 和 s (局部变量),两个线程同时执行,最后交换 T1 与 T2 的值;




    分布式锁


    只是某个类型的锁,将来补充概念。



    总结 :


    • 无论何种情况,优先考虑使用synchronized
    • 什么情况下使用lock()?它与 synchronized()相比有什么优点?
    • 为什么使用读写锁?读写锁是怎么实现的?
    • 随机列举一些跟锁一起使用的方法~~
    • 把标题下的各种锁的使用场景和实现方式全都想一遍~

  • 相关阅读:
    第 12 章 Docker Swarm
    第 1 章 虚拟化
    第 0 章 写在最前面
    第 11 章 日志管理
    第 11 章 日志管理
    第 11 章 日志管理
    第 11 章 日志管理
    第 11 章 日志管理
    第 11 章 日志管理
    第 11 章 日志管理
  • 原文地址:https://www.cnblogs.com/hskcool/p/14221500.html
Copyright © 2020-2023  润新知