共享问题

资源共享

  • 由于线程和进程的关系,在上一篇并发基础中有写到,多个线程可共享一块内存区域,并且每个线程也都有自己独立的工作空间。
  • 大模型训练 怎么利用共享GPU内存 共享模型元素_多线程

  • 经典案例:从这里我们开始对自增自减进行迫害
static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter--;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",counter);
    }
  • 上述代码的结果会是0吗?
  • 分析:结果可能是正数、负数或者0。why? 因为java中的自增自减不是原子操作。
  • 上字节码: i++
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
  • 单线程下,结果肯定是0,但是多线程环境下,自增和自减的指令交错,就会出现问题,
  • 比如t1线程刚获取静态变量i的值为1,t2线程立马完成减1操作并且赋值,t1线程操作的值是1,而实际上i已经变成了0

临界区

  • 一个程序在运行的时候要考虑多线程对他的影响,也就是多个线程在同时访问一个共享资源的时候,如果读写指令发生交错,那就乱套了
  • 一段代码内如果存在对 共享资源的多线程读写操作,称这段代码块为临界区
  • 举个栗子
static int i=0;
static void add(){
    //临界区
    i++;
}
static void decre(){
    //临界区
    i--;
}
  • 补充:自增和自减操作我们从字节码里可以看出,分为读和写组成的,并非原子性操作。

竞态条件

多个线程在临界区执行,由于代码的执行顺序问题而导致结果的不可见,无法预测,称之为发生竞态条件

synchronized

用阻塞的方式治疗

  • synchronized作为java的一个关键字,它能够将一个方法或者一段代码块锁起来,确定同一时间内,只有一个线程在执行被锁住的代码。
  • synchronized通过互斥的方式,能够解决上面的临界区问题,也是阻塞式的。
  • 其他线程想来获得这个锁,那就需要进行等待(阻塞),确保拥有锁的线程能够安全的执行完这段被锁住的代码。

==互斥和同步都可以通过synchronized关键字实现,but二者是有区别滴

  • 互斥是保证临界区的竞态条件发生,同一时刻只有一个线程执行临界区代码
  • 同步是由于线程完成任务的先后、顺序不同,需要一个线程等待另一个线程完成到某个点

==

语法

synchronized (obj){
	//代码块
}

//解决上面的问题

 static int counter = 0;
static final Object obj=new Object()
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized(obj){
                	counter++;
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                 synchronized(obj){
                	counter--;
                }
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",counter);
    }

方法上的synchronized

class Test{
 public synchronized void test() {
 
 }
}
//等价于
class Test{
     public void test() {
     	synchronized(this) {
 
 		}
 	}
}

class Test{
 public synchronized static void test() {
 }
}
//等价于
class Test{
 public static void test() {
      synchronized(Test.class) {
 
        }
    }
}

线程安全分析

成员变量和静态变量是否线程安全?

  • 如果没有共享,则安全
  • 如果被共享,根据他们的状态是否能改变,又分为
  • 只有读操作,那没事了
  • 如果有读写操作,则这段代码就是临界区,需要考虑线程安全问题

局部变量是否安全?

  • 局部是线程安全的
  • 但是局部变量引用的对象不一定
  • 如果该对象没有逃离方法的作用范围,他是安全的
  • 如果对象逃离方法的作用范围,那就要考虑线程安全问题
public static void test1() {
	 int i = 10;
	 i++;
}

每个线程在调用test1方法是,i变量都会创建一次,不存在共享的问题

class ThreadUnsafe {
    
    ArrayList<String> list = new ArrayList<>();   //成员变量
    
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }
    private void method2() {
        list.add("1");
    }
    private void method3() {
        list.remove(0);
    }
}

//执行
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
     ThreadUnsafe test = new ThreadUnsafe();
     for (int i = 0; i < THREAD_NUMBER; i++) {
         new Thread(() -> {
        	 test.method1(LOOP_NUMBER);
         }, "Thread" + i).start();
     }
}

分析:

  • ThreadUnsafe中的list是成员变量,无论是哪个线程中method2引用的都是同一个list变量 共享
  • 这里执行的是调用method1方法,method1去调用的method2和3,这两个方法中的list是同一个。
  • 如果method2还没有执行,list中还为空,那么执行了方法3就会报错。

解决:将list改为局部变量

class ThreadSafe {
 public final void method1(int loopNumber) {
     ArrayList<String> list = new ArrayList<>();
     for (int i = 0; i < loopNumber; i++) {
         method2(list);
         method3(list);
 	}
 }
 private void method2(ArrayList<String> list) {
        list.add("1");
     }
 private void method3(ArrayList<String> list) {
    	 list.remove(0);
     }
}

方法修饰符带来的线程安全问题

子类进行方法覆盖

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadSafe{

    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

Monitor

java对象头

  • 32位虚拟机为例

大模型训练 怎么利用共享GPU内存 共享模型元素_大模型训练 怎么利用共享GPU内存_02


大模型训练 怎么利用共享GPU内存 共享模型元素_java_03


大模型训练 怎么利用共享GPU内存 共享模型元素_并发编程_04

  • 64位虚拟机为例

大模型训练 怎么利用共享GPU内存 共享模型元素_多线程_05

Monitor锁原理

  • Monitor被译为监视器或管程
  • 每个java对象都可以关联一个监视器,如果使用synchronized上锁后,该对象的Monitor中的Mark Word中就被设置为指向Monitor对象的指针
  • 结构如下
  • 大模型训练 怎么利用共享GPU内存 共享模型元素_java_06

  • 注意:
  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

wait/notify

API

  • wait:让当前对象锁的拥有者Onwer线程,进入到该对象头中的WaitSet等待,无限制等待
  • notify:随机唤醒WaitSet中的一个线程
  • notifyAll:唤醒所有WaitSet中的线程

wait/notify的正确姿势

  • sleep和wait的区别,sleep是线程调用的方法,将该线程进行睡眠,而且期间不会释放该线程所持有的资源;wait是在调用synchronized锁之后,通过锁对象调用的方法,而且会释放锁资源
  • 完成一个场景,小王和小李在厂里上班,小王做到一半想抽根华子,不抽不干活,等着人拿烟来;小李饿了,等着美团送外卖,没吃饭也不干活
@Slf4j
public class Test11 {
    static boolean hasHuaZi=false;
    static boolean hasWM=false;   //外卖
    static Object obj=new Object();
    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            log.debug("开始干活...");
            log.debug("来根华子好吗?秋梨膏");
            log.debug("开始等人送华子");
            synchronized (obj) {
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                while (true) {
                    log.debug("烟来了吗?");
                    if (hasHuaZi) {
                        log.debug("来劲了,开始干活,芜湖~");
                        break;
                    }else {
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        },"小王").start();

        new Thread(()->{
            log.debug("开始干活...");
            log.debug("没吃饭不干活");
            log.debug("点外卖了,等~ing");
            synchronized (obj) {
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                while (true) {
                    log.debug("外卖来了吗?");
                    if (hasWM) {
                        log.debug("吃饱了,开始干活,芜湖~");
                        break;
                    }else {
                        log.debug("没来再歇会儿");
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }
        },"小李").start();

        Thread.sleep(2000);
        new Thread(()->{
            synchronized (obj){
                log.debug("烟来咯");
                hasHuaZi=true;
                obj.notifyAll();
            }
        },"送烟的").start();

        Thread.sleep(2000);
        new Thread(()->{
            synchronized (obj){
                log.debug("美团外卖");
                hasWM=true;
                obj.notifyAll();
            }
        },"美团").start();
    }
}

park和unpark

可以说是加强版的wait/notify

基本使用

//这两个方法是LockSupport类中的方法
//暂停某个线程
LockSupport.park();

//恢复指定的线程 t1
LockSupport.unpark(t1);

先Park 再Unpark

@Slf4j
public class Test12 {
    public static void main(String[] args) throws InterruptedException {


        Thread t1 = new Thread(() -> {
            log.debug("run....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();
            log.debug("unpark...");
        }, "t1");

        Thread t2 = new Thread(() -> {
            log.debug("run...");
            LockSupport.park();
            log.debug("unpark...");
        }, "t2");

        t1.start();
        t2.start();

        Thread.sleep(1000);
        LockSupport.unpark(t1);
        

    }
}

结果

大模型训练 怎么利用共享GPU内存 共享模型元素_大模型训练 怎么利用共享GPU内存_07

先unpark再park

@Slf4j
public class Test12 {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("run....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("park");
            LockSupport.park();
            log.debug("unpark...");
        }, "t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("unpark");
        LockSupport.unpark(t1);
    }
}

结果:我们先对t1进行了unpark,然后再进行park,结果发现t1依然unpark了

大模型训练 怎么利用共享GPU内存 共享模型元素_多线程_08

原理:

  • 每个线程都有一个自己的Parker对象,由三部分组成: _counter_cond_mutex
  • 设想一个线程是一个旅行者:Parker就是他的背包,_counter则是包里的干粮,0表示没有,1表示充足
  • 调用park并不是直接就休息,而是检查干粮是否充足,如果没有了,则进行休息;如果充足则继续,不用停止
  • 调用unpark,就好比补充干粮,如果此刻在休息,则就叫醒出发;如果还在运行,则会补充一份备用干粮,那么当他下次调用park的时候,仅仅是耗掉备用干粮,也就是说,并不会停下来,因为只是消耗了备用干粮,_counter还是1,干粮依旧充足

大模型训练 怎么利用共享GPU内存 共享模型元素_并发编程_09

  • _counter初始状态为0,当进行park检查时,发现为0,则暂停,获得**_mutex锁,进入_cond**等待

大模型训练 怎么利用共享GPU内存 共享模型元素_并发编程_10

  • 然后调用 unpark,现将**_counter**设置为1;
  • 唤醒**_cond**条件变量中的线程
  • 线程恢复运行
  • _counter设值为0

以上是先park,再unpark

大模型训练 怎么利用共享GPU内存 共享模型元素_并发编程_11

  • 一波非常规操作,先unpark,将**_counter** 设置为1
  • 然后调用park的时候,发现 _counter 为1,则不暂停线程
  • 将**_counter** 设置为0

活跃性

死锁

当一个线程需要获取多把锁时,这时容易发生死锁

例如,t1先获取了A锁,然后要再去获取B锁;此时t1刚获取A锁的时候,t2获得了B锁,而t2又需要再去获得A锁。

定位死锁的工具 jconsole

活锁

两个线程相互改变对方的结束条件,最后谁也无法结束

public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

饥饿

线程始终得不到CPU调度,也不能结束

ReentrantLock

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断: lockInterruptibly

@Slf4j
public class Test12 {
    static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("run....");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断");
            }
            try {
                log.debug("获得了锁");
            }finally {
                lock.unlock();
            }

        }, "t1");
        lock.lock();
        log.debug("获得了锁");
        t1.start();
        try {
            Thread.sleep(1000);
            t1.interrupt();
            log.debug("执行打断");
        } finally {
            lock.unlock();
        }
    }
}

锁超时:tryLock

立刻返回

@Slf4j
public class Test12 {
    static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("run....");
            if (!lock.tryLock()){
                log.debug("尝试获得锁失败。。。立即返回");
                return;
            }
            try {
                log.debug("获得锁成功");
            }finally {
                lock.unlock();
            }

        }, "t1");
        lock.lock();
        t1.start();
        try {
            Thread.sleep(1000);
        } finally {
            lock.unlock();
        }
    }
}

大模型训练 怎么利用共享GPU内存 共享模型元素_多线程_12

锁超时

@Slf4j
public class Test12 {
    static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("run....");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)){
                    log.debug("等待1秒后尝试获得锁失败。。。立即返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.debug("获得锁成功");
            }finally {
                lock.unlock();
            }

        }, "t1");
        lock.lock();
        t1.start();
        try {
            Thread.sleep(2000);
        } finally {
            lock.unlock();
        }
    }
}

大模型训练 怎么利用共享GPU内存 共享模型元素_多线程_13

公平锁

条件变量

@Slf4j
public class Test12 {
    static ReentrantLock lock=new ReentrantLock();
    static Condition waitRoom1=lock.newCondition();
    static Condition waitRoom2=lock.newCondition();
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("run....");
            lock.lock();
            try {
                Thread.sleep(2000);
            } catch (Exception e){
                e.printStackTrace();
            }
            try {
                log.debug("进入休息室1...waitRoom1");
                waitRoom1.await();
                log.debug("醒了...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }, "t1");

        t1.start();

        Thread.sleep(4000);
        log.debug("叫醒他");
        lock.lock();
        try {
            waitRoom1.signal();
        } finally {
            lock.unlock();
        }

    }
}