【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()相比有什么优点?
  • 为什么使用读写锁?读写锁是怎么实现的?
  • 随机列举一些跟锁一起使用的方法~~
  • 把标题下的各种锁的使用场景和实现方式全都想一遍~

原文地址:https://www.cnblogs.com/hskcool/p/14221500.html