并发编程的故事——并发之共享模型

news2024/11/17 8:46:11

并发之共享模型

文章目录

  • 并发之共享模型
  • 一、多线程带来的共享问题
  • 二、解决方案
  • 三、方法中的synchronize
  • 四、变量的线程安全分析
  • 五、习题
  • 六、Monitor
  • 七、synchronize优化
  • 八、wait和notify
  • 九、sleep和wait
  • 十、park和unpark
  • 十一、重新理解线程状态
  • 十二、多把锁
  • 十三、ReentrantLock


一、多线程带来的共享问题

比如共享资源在被两个线程使用的时候。线程1对变量i=0进行+1操作,但是在+1途中切换到了线程2,线程2对变量i进行-1操作,然后把-1返回到共享变量。但是再切换回线程1已经+1也就是局部变量已经是1,重新赋值给共享变量会导致并发问题
其二是共享资源如果一直被一个线程使用,线程可能会由于sleep,wait,io等操作浪费cpu的使用时间,那么可以把这段时间让给别人

问题分析
i++的字节码是
getstatic i 获取静态变量i,也就是主存的i。

iconst_1 准备常量1

iadd 进行相加

pustatic i

所以我们看到的i++并不是一条原子指令,既然不是原子指令,那么线程自然可以在切换过程中导致这些指令交错执行。最后导致共享资源是个脏数据的问题。

临界区
实际上就是多个线程访问的代码里有共享资源,那么这段代码就是临界区

竟态条件
如果在临界区中多线程发生执行指令序列不同导致结果无法预测的状况就是竟态条件

二、解决方案

①synchronize
线程1上锁后,线程2无法获取锁不能执行临界区,线程2阻塞等待线程1完成释放锁之后才能够使用。可以把synchronize类比为一个房间,每次有锁的人才能够进入房间做事情,就算cpu时间片 用完,只要没有这个锁的钥匙其它线程是无法进入房间的。当用完之后就会释放锁,并且唤醒那些阻塞的线程。
在这里插入图片描述

static int count=0;
    static Object lock=new Object();
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
//                room.increment();
                synchronized (lock){
                    count++;
                }

            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
//                room.decrement();
                synchronized (lock){
                    count--;
                }

            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
//        log.debug("{}", room.getCounter());
        log.debug("{}",count);
    }

思考
①如果synchronize放到for外面?实际上就是要执行完整个for才会把锁放出去-原子性
②如果t1和t2使用的是不同的obj会怎么样?相当于进入了不同的房间,那么锁是没有效果的,两个线程仍然会各自执行临界区的代码块导致执行序列不同。
③如果t1加锁,但是t2没有?那就相当于t2可以随时执行临界区

面向对象改进
实际上就是把那些需要加锁的临界区和共享资源全部封装到一个类上。在方法上加synchronize相当于锁住了this。如果是静态方法那么相当于是锁住了类对象.class

class Room {
    private int counter = 0;

    public synchronized void increment() {
        counter++;
    }

    public synchronized void decrement() {
        counter--;
    }

    public synchronized int getCounter() {
        return counter;
    }
}

三、方法中的synchronize

线程八锁
情况1:没有sleep的时候n1执行a和b方法都是锁住自己的对象。所以是互斥的。
情况2:有sleep其实还是一样,执行a和b方法的两个线程看谁先获取到锁。那么谁就先执行。不管里面是不是有sleep,没获取到锁的线程就是要等待

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number n1 = new Number();
//        Number n2 = new Number();
        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
            log.debug("begin");
            n1.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

情况3:这种情况c是没有加锁,也就是随意都可以执行。可能出现的结果是3 12,也可能是32 1,也可能是23 1。

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number n1 = new Number();
//        Number n2 = new Number();
        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
            log.debug("begin");
            n1.b();
        }).start();

        new Thread(() -> {
            log.debug("begin");
            n1.c();
        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }

    public void c(){
        log.debug("3");
    }
}

情况4:这种他们绑定的锁都是不同的,相当于就是无锁。最后的结果肯定是21。因为1线程sleep了

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
            log.debug("begin");
            n2.b();
        }).start();

//        new Thread(() -> {
//            log.debug("begin");
//            n1.c();
//        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
//
//    public void c(){
//        log.debug("3");
//    }
}

情况5-8都是加上了static,那么就根据他们锁的对象来判断是不是同一把锁就可以了

四、变量的线程安全分析

静态变量和成员变量是否有线程安全问题?
如果只是读就没有,如果是读写就要关注临界区

局部变量?
如果是引用类型的话那么就有。
局部变量的值存储在线程的栈帧里面,也就是私有的。而不是像static变量那样先从方法区中取出这个变量然后再进行对应的修改。
在这里插入图片描述
情况1:ThreadUnsafe的list是在类里面创建的,那么就会造成,线程处理的是堆里面同一个list导致的线程安全问题

情况2:把list放到method1里面,那么就是一个局部变量的引用,而且每个线程调用方法后都有自己的一个list。那么就不会造成线程安全问题

情况3:有子类重写了这个方法那么也能获取这个list,导致多个线程能够操作list。解决办法就是给方法加上final防止子类重写。

public class TestThreadSafe {

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

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");
    }

    private void method3(ArrayList<String> list) {
        System.out.println(1);
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
//    @Override
    public void method3(ArrayList<String> list) {
        System.out.println(2);
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

在这里插入图片描述
线程安全类
Integer
HashTable
String
Random
Vector
JUC下的类

它们的单个方法是线程安全的,但是多个方法执行的时候就不一样了。
下面的代码出现的问题就是线程1判断成功之后切换,刚好释放了锁,然后就是线程2获取锁进行判断,再次切换线程1获取锁处理put,切换线程2也可以获取锁处理put。因为单个方法执行完就会释放锁。所以这样还是需要整体上加锁才能够继续处理。
在这里插入图片描述
不可变类线程安全
String
String和Integer都是不可变的,String本质上就是一个char[]数组。如果是substring方法实际上就是复制一个新的数组出来,然后再给String的char数组进行赋值。replace实际上也是创建数组,然后对比愿数组的旧值,如果是旧值那么直接给新的数组那个位置赋新值。

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                //创建新数组
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    //根据原来的数组是旧值的位置改变成新值
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
 public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
     //实际上就是创建了一个新的String,而不是修改了值
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}


  public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
      //实际上就是创建新数组并且进行复制
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

实例分析
这种线程不安全,原因就是这个MyServlet是共享的,而且UserService是在堆里面的。可以被多个线程调用它的方法修改count,可能会引发并发问题。
在这里插入图片描述
这种也是有并发问题的,单例共享,可以被多个多线程调用,覆盖start这种变量。
在这里插入图片描述
这种不会发生线程安全问题,原因是没有任何变量可以被修改。
在这里插入图片描述
这里是会发生线程安全问题,原因是Connection暴露出去,可以被多个线程处理。如果线程1在处理getCon的时候切换到线程2刚好close那么线程1的Connection就没办法继续执行,因为已经被修改了。
在这里插入图片描述
这个地方没有线程安全问题,原因是每次都是新创建的一个UserDao相当于是一个局部变量。并不是一个共享资源
在这里插入图片描述
这个地方可能会出现线程安全问题是因为通过子类方法把局部变量暴露出去,可能会被子类对象通过线程把这个局部变量进行修改
在这里插入图片描述
总结:线程是否安全取决于是否有能够被修改的共享资源

五、习题

出现线程安全问题的是在卖票的时候可能多个线程在卖票,同时取出了共享变量count,最后的count数量就是最后那个线程处理的值。而不是共同处理的值,因为他们的执行序列发生了错误,导致count没有等待处理完就被另外一个线程先读取进来了。

解决办法就是给临界区加锁,实际上就是window的sell。那么为什么不给amountList和window加锁?原因是他们操作的并不是同一个共享资源处理不同自然就不需要加锁。但是之前的HashTable两个操作get和put都是针对同一个共享资源,导致最后的value会被最后的那个线程覆盖。因为判空成功的时候线程1还没有完成put。而且也没有锁住两个操作,而是做一个放一个

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);

        // 所有线程的集合
        List<Thread> threadList = new ArrayList<>();
        // 卖出的票数统计
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                // 买票

                try {
                    Thread.sleep(random(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int amount = window.sell(random(5));
                // 统计买票数
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }

        for (Thread thread : threadList) {
            thread.join();
        }

        // 统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int random(int amount) {
        return random.nextInt(amount) + 1;
    }
}

// 售票窗口
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // 获取余票数量
    public int getCount() {
        return count;
    }

    // 售票 synchronized
    public  int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

Vector自己的方法就已经带锁

public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

转账问题
这里其实会发生线程安全问题。主要就是在a转账的同时b也进去转账,那么b获取到的肯定就是没有转账的a,相对a也是。那么直接给方法加上synchronize行不行?如果绑定的是本类对象,很明显是不行,因为是两个账户,锁是不同的,进入了不同房间。那么解决方案就是可以使用唯一的Account.class的本类,那么就能够锁住了

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

// 账户
class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    // 转账
    public void transfer(Account target, int amount) {
        synchronized(Account.class) {
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

六、Monitor

java对象头
包括了markword主要就是存储hashcode,age(gc生命值),biase_lock是不是偏向锁,01加锁的情况
还有就是klassword主要就是指向类对象(类的信息)。
如果是数组那么还包含了数组的长度
在这里插入图片描述
Monitor锁
monitor锁是os提供的,成本很高
工作原理
实际上就是把obj的markword前面30bit记录monitor的地址,指向monitor。然后如果有线程要执行临界区代码时就把monitor的owner指向对应的线程。如果又有线程进来,那么看看obj是否关联锁,然后再看看是否有owner,如果有那么就进入到EntryList阻塞等待。等待线程释放锁之后,唤醒EntryList然后重新开始竞争。
在这里插入图片描述
字节码的角度
字节码的角度其实就是先把lock的引用复制放到slot1,然后就是monitorentry,把lock的markword指向monitor,并且把hashcode等存入monitor。接着执行业务代码,最后就是取出引用slot1,然后就是monitorexit,解锁。而且对业务代码也就是同步块进行了异常监视,如果出现异常,那么还是会进行解锁操作的
在这里插入图片描述

七、synchronize优化

小故事
如果两个线程之间没有竞争那么就可以使用轻量级锁,相当于就是挂书包,如果发现是对方书包那么就在外面等待。后来另一个线程不用了,那么另一个线程就可以刻名字在门外,相当于就是偏向锁,如果这个时候有人来竞争,那么就会升级为书包也就是轻量级锁。后来另一线程回来了,发现那个线程把很多个门都刻上名字,就去找os把那些名字批量刻成自己的。也就是修改偏向锁。最后名字实在刻太多取消了偏向。

偏向锁是单个线程专属的,如果单个线程处理某个代码没有竞争,那么就可以使用偏向锁,如果有竞争那么就可以升级为轻量级锁。
1.轻量级锁
本质就是线程的调用临时区方法的栈帧的锁记录保存对象的引用和对象markword的信息。接着就是把对应锁记录的锁信息与obj进行交换,比如说把01改成了00告诉obj这是一个轻量级锁,而且告诉了obj锁记录的地址,相当于就是给obj贴上是谁的锁的标签。如果是可重入锁,那么锁记录markword部分就是null表示的是这是可重入的,用的是同一个锁。
在这里插入图片描述
2.锁膨胀
其实就是竞争轻量级锁的时候,没有地方给竞争的线程放着,那么这个时候就需要把轻量级锁转换成重量级锁monitor,其实就是把obj的markword指向monitor。然后就是monitor的owner指向当前线程的锁记录。把阻塞线程放到等待队列里面。
恢复的时候,CAS尝试把线程的锁记录给恢复过去,但是发现失败。这个时候恢复方式改成了重量级锁的恢复方式,唤醒list,然后owner设置为null,线程重新竞争monitor。如果没有就把monitor保存的hashcode信息恢复。
在这里插入图片描述
3.自旋优化
实际上就相当于等红绿灯,如果很快到绿灯就等一会,如果还有很久那么就拉手刹。自旋就是旋多一会等别人释放重量级锁,如果成功一次,那么下次就会确定成功几率加大,自旋多几次。如果没有等到那么就阻塞。
自旋的原因:阻塞会导致线程的上下文切换需要消耗cpu时间和资源。速度相对比较慢。

4.偏向锁
之所以要使用偏向锁是因为轻量级锁的锁重入每次都调用CAS进行对比,CAS是一个OS指令操作所以速度很慢。所以偏向锁是把ThreadId直接赋值给markword,那么下次就能直接在java上对比这个markword。
偏向锁带有延迟性,通常对象创建过一会才会生成
先生成偏向锁-》轻量级锁-》重量级锁
如果给临界区使用偏向锁,那么对应执行线程的id赋值给markword
如果使用了锁的hashcode,那么偏向锁就会被禁止,因为hashcode占用的bit太多
轻量级在锁记录上记录hashcode,重量级在monitor上记录
如果两个线程用同一个偏向级锁,那么锁会变成不可偏向的,升级为轻量级锁
在这里插入图片描述
批量重偏向
其实就是多个没有竞争的线程,使用同一个锁,如果jvm发现撤销的锁偏向次数超过20次,那么就会自动偏向另外一个线程。比如t1线程使用一堆锁,锁偏向t1。但如果t2使用这些锁,并且需要撤销锁但偏向超过20次,那么这些锁会全部偏向t2

批量撤销
如果撤销超过40次那么jvm就会撤销所有对象的偏向

5.锁消除
其实就是JIT发现锁的临界区里根本没有共享资源,那么就取消了这个锁
在这里插入图片描述

八、wait和notify

小故事
小南需要烟才能工作,但他又要占着锁让别人无法进来。那么这个时候开一个waitSet相当于就是休息室让小南进去休闲,并且释放锁。如果烟到了,那么notify小南就能够继续工作了。

Blocked和Waiting区别
其实就是waiting释放了锁,blocked是没有锁
waiting被notify之后仍然需要进入到entrylist进行等待
在这里插入图片描述
wait和notify的规则
线程调用对象wait和notify的时候一定是要使用这个锁成为monitor的主人的时候。这样才能够wait释放锁和被其它获取这个锁的人notify

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
//            obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

wait()方法可以限制等待的时间,wait(参数)

九、sleep和wait

区别
sleep:Thread调用,静态方法,而且不会释放锁
wait:所有obj,但是要配合synchronized使用,可以释放锁
扩展
通常锁会加上final防止被修改

正确使用
小南需要烟才能工作,如果是使用sleep不释放锁,那么其它需要等待干活的人就会干等着。但是wait可以让小南释放锁,让其他线程工作,并且唤醒小南
存在问题
会不会有其他线程在等待着锁?如果是那么会不会唤醒错了线程?

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }

}

解决办法
可以通过while多次判断条件是否成立,直接使用notifyAll来唤醒所有的线程。然后线程被唤醒之后可以先再次判断条件是否成立,成立那么往下面执行,如果不成立那么继续wait。
在这里插入图片描述

十、park和unpark

与wait和notify的区别
不需要与monitor一起使用
可以精准唤醒和阻塞线程
可以先unpark,但是不能先notify。但是unpark之后park就不起作用了

工作原理
①park,先去到counter里面判断是不是0,如果是那么就让线程进入队列,并且再次把counter设置为0
②unpark,如果线程正在阻塞,那么先把counter置为1,然后唤醒线程,恢复运行,并把counter设置为0
③先unpark后park,那么就unpark补充counter为1,park判断counter是1,认为还有体力就继续执行

十一、重新理解线程状态

情况1:new->runnable
线程start
情况2:runnable->waiting
notify和wait。wait进入阻塞,notify让他们重新竞争锁进入runnable,其他还是进入blocked
情况3
park和unpark
情况4
t.join调用它的线程会进入等待,等待t完成任务或者是被interrupt
情况5-8
其实就是wait,join,sleep加上时间而已,都是从runnable->blocked
情况9
synchronize获取锁失败那么就会进入blocked
情况10
所有代码执行完那么就是terminated
在这里插入图片描述

十二、多把锁

一个房间睡觉和学习。但是只有一把锁睡觉的时候不能学习并发度非常低。那么这个时候可以通过细化锁的粒度,分成两把锁,一把是学习房间的锁,一把是卧室的锁,那么就能够让两个功能并发执行。

问题
如果锁太多,一个线程需要多把锁会导致死锁的发生

public class TestMultiLock {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            bigRoom.study();
        },"小南").start();
        new Thread(() -> {
            bigRoom.sleep();
        },"小女").start();
    }
}

@Slf4j(topic = "c.BigRoom")
class BigRoom {

    private final Object studyRoom = new Object();

    private final Object bedRoom = new Object();

    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (this) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }

}

死锁的案例
t1有A但是想要B,t2有B但是想要A

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

死锁的检查方式
1、jps定位进程id,jstack id来查看程序信息
2、jconsole直接查看进程信息

活锁
其实就是两个线程都在改变对方的解锁条件导致没有释放锁,但没有阻塞。死锁就是含有对方的锁不放开导致线程阻塞。
解决方案
可以通过改变线程执行的时间,让他们交错执行,快速执行解锁条件。
在这里插入图片描述
饥饿问题
其实就是线程一直获取(竞争)不到锁导致没有执行。

十三、ReentrantLock

相比synchronize
可以被中断
可以设置获取超时,超时之后就自动放弃获取锁
公平锁,防止饥饿问题
条件变量多

可重入
只要是同一个线程获取同一把锁,那么就能够被使用第二次。(在没有被解锁的时候可被使用第二次)

@Slf4j(topic = "c.test22")
public class MyTest22 {
    public static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try{
            log.debug("开始进入m1");
            m1();
        }finally {
            lock.unlock();
        }
    }

    public static void m1(){
        lock.lock();
        try {
            log.debug("m1进入");
            m2();
        }finally {
            lock.unlock();
        }
    }

    public static void m2(){
        lock.lock();
        try{
            log.debug("m2进入");
        }finally {
            lock.unlock();
        }
    }
}

可中断
lockInterrupt,这个方法才能够被其它线程中断等待锁。如果是lock那么就算中断也没有任何效果。这种可中断可以减少死锁的发生。

@Slf4j(topic = "c.test23")
public class MyTest23 {
    public static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("上锁");
            lock.lock();
//            try {
//                lock.lockInterruptibly();
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//                log.debug("无法获取锁");
//            }
            try {
                log.debug("获取到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        t1.start();

        lock.lock();
        Sleeper.sleep(1);
        log.debug("帮助t1解锁");
        t1.interrupt();


    }
}

超时
这个地方可以使用tryLock来设定获取锁的超时时间,如果超时那么就自动放弃获取锁。而不是一直锁住

@Slf4j(topic = "c.test24")
public class MyTest24 {
    public static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获得锁");

            try {
                if(!lock.tryLock(2, TimeUnit.SECONDS)){
                    log.debug("获取锁失败");
                    return ;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("获取锁失败1");
                return;
            }
            try{
                log.debug("获取锁成功");
            }finally {
                lock.unlock();
            }
        }, "t1");

        log.debug("main获取锁");
        lock.lock();
        t1.start();
        Sleeper.sleep(1);
        log.debug("main解锁");
        lock.unlock();
    }
}

解决哲学家的问题
思路
可以使用ReentrantLock的tryLock,如果尝试失败你那么就会往下面执行而不是一直等待获得锁,获取不到是不会阻塞线程的。

 @Override
    public void run() {
        while (true) {
            if(left.tryLock()){

                try{
                    if(right.tryLock()){
                        try{
                          eat();
                        }finally {
                            right.unlock();
                        }
                    }
                }finally {
                    left.unlock();
                }
            }
        }
    }

条件变量
定义
synchronize可以有一把锁,并且通过wait和notify来释放锁进入到waitSet。对于ReentrantLock就相当于有多个休息室waitSet,创建锁之后可以创建多个条件变量(多个房间),可以认为ReentrantLock里面有多个休息室,进入不同的休息室可以通过不同的小锁处理。但实际上释放的锁还是ReentrantLock,然后交给别人使用,只不过通过条件变量可以控制住不同的房间,让同房间但线程区竞争锁。

这里就是使用了条件变量,但他们进入但线程房间不同,操作的方式是相同的。唤醒的房间的线程不相同。

@Slf4j(topic = "c.Test24")
public class Test224 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {


        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
            ROOM.lock();
            try {
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送外卖的").start();

        sleep(1);

        new Thread(() -> {
            ROOM.lock();
            try {
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送烟的").start();
    }

}

用锁来固定顺序执行
思路
其实就是两个线程都需要用到这个锁,但是t1线程一定要t2线程运行之后才能运行,那么判断条件就是一个boolean,如果t2运行那么就修改,并且唤醒线程t1。线程t1如果发现t2没有运行那么wait进入等待,如果被虚假唤醒可以通过while来循环进入重新等待。

@Slf4j(topic = "c.25")
public class MyTest25 {
    static Object lock=new Object();
    static boolean t2Run=false;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock){


                try {
                    while(!t2Run){
                        lock.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("1");
            }
        }, "t1");


        Thread t2 = new Thread(() -> {
            synchronized (lock){
                log.debug("2");
                //唤醒线程1
                t2Run=true;
                lock.notify();
            }
        }, "t2");

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

    }
}

第二种方法
其实就是使用LockSupport的park方法处理。这种t1如果先执行那么就会park,进入阻塞,然后t2执行之后unpark唤醒t1。如果是t2先执行也没关系,这里线程的unpark会把counter变成1,t1如果park先检查counter发现是1那么就可以继续执行。

@Slf4j(topic = "c.26")
public class MyTest26 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1");
        }, "t1");

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

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


    }
}

轮流打印abc的思路(交替执行)
synchronized方式
其实还是对是否到这个线程的flag进行判断,如果是1那么就t1执行,如果是2那么就t2执行。执行之后还需要唤醒其它线程来查看是不是自己的条件,如果不是那么就继续进入等待条件,符合条件就获取锁继续执行。

public class MyTest27 {
    public static void main(String[] args) {
        WaitNotify1 waitNotify1 = new WaitNotify1(1, 5);
        Thread t1 = new Thread(() -> {
            waitNotify1.print("a",1,2);
        }, "t1");
        Thread t2 = new Thread(() -> {
            waitNotify1.print("b",2,3);
        }, "t2");
        Thread t3 = new Thread(() -> {
            waitNotify1.print("c",3,1);
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class  WaitNotify1{

    public void print(String s,int waitFlag,int nextFlag){

        synchronized (this){
            try {
                for(int i=0;i<loopNum;i++){
                    //如果不是1那么就等待
                    while(this.flag!=waitFlag){
                        this.wait();
                    }
                    System.out.print(s);
                    this.flag=nextFlag;
                    this.notifyAll();
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


    private int flag;
    private int loopNum;

    public WaitNotify1(int flag,int loopNum) {
        this.flag = flag;
        this.loopNum = loopNum;
    }
}

ReentrantLock处理顺序执行问题思路
这个地方执行的思路其实就是通过lock开多几个条件变量,条件变量控制的是各个休息室,使用条件变量来吧线程阻塞,之后再释放。a,b,c分别开了3个,然后就是执行一个之后调用另一个condition来解锁另外一个继续执行。一开始需要把a,b,c三个线程都上锁,接着就是手动解锁一个让循环开始执行。

public class MyTest28 {
    public static void main(String[] args) {
        WaitLock waitLock = new WaitLock(5);
        Condition a = waitLock.newCondition();
        Condition b = waitLock.newCondition();
        Condition c = waitLock.newCondition();

        new Thread(()->{
             waitLock.print("a",a,b);
         },"t1").start();
        new Thread(()->{
            waitLock.print("b",b,c);
        },"t2").start();
        new Thread(()->{
            waitLock.print("c",c,a);
        },"t3").start();

        Sleeper.sleep(1);
        waitLock.lock();
        try{
            //唤醒a
            a.signal();
        }finally {
            waitLock.unlock();
        }
    }
}

@Slf4j(topic = "c.lock")
class WaitLock extends ReentrantLock{

    private int loopNum;

    public WaitLock(int loopNum) {
        this.loopNum = loopNum;
    }

    public void print(String str,Condition cur,Condition next){
        for(int i=0;i<loopNum;i++){
            lock();
            try{
                cur.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }

        }
    }
}

LockSupport执行思路
其实这个地方直接就可以使用park先让三个线程阻塞,然后再手动开启一个线程。如果需要让阻塞的线程开启就需要unpark(t)就是需要传入对应的线程参数,唤醒对应的线程。t1执行完之后,唤醒t2,t2执行完之后唤醒t3,那么这个时候调用的方法就要传入对应的线程参数。而且线程参数是可以共享的,放到方法区,如果是放到main线程上是无法看到的。

@Slf4j(topic = "c.test29")
public class MyTest29 {
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
        WaitPark waitPark = new WaitPark(5);
        t1=new Thread(()->{
            waitPark.print("a",t2);
        },"t1");
        t2=new Thread(()->{
            waitPark.print("b",t3);
        },"t2");
        t3=new Thread(()->{
            waitPark.print("c",t1);
        },"t3");

        t1.start();
        t2.start();
        t3.start();
        Sleeper.sleep(1);
        LockSupport.unpark(t1);
    }
}
@Slf4j(topic = "c.lock")
class WaitPark{
    private int loopNum;

    public WaitPark(int loopNum) {
        this.loopNum = loopNum;
    }



    public void print(String str,Thread t){
        for(int i=0;i<loopNum;i++){
            LockSupport.park();
            log.debug(str);
            LockSupport.unpark(t);
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/956828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

瓜分双十一10亿红包设计:在线分享教程?

在如今激烈的市场竞争中&#xff0c;瓜分红包营销活动成为了各大企业争相使用的一种营销手段。这种活动不仅能够吸引用户的关注和参与&#xff0c;还能够提高用户的粘性和忠诚度。那么&#xff0c;如何自建瓜分红包营销活动呢&#xff1f;下面将为大家详细解析。 首先&#xff…

振动智能监测与设备可靠性:无线技术的契机

在现代工业领域&#xff0c;设备的可靠性和稳定运行对于生产效率和安全性至关重要。然而&#xff0c;由于设备的频繁使用和各种环境影响&#xff0c;设备故障和突发停机仍然是不可避免的挑战。为了有效地应对这些挑战&#xff0c;振动智能监测技术结合无线传感器的应用正在成为…

FPGA可重配置原理及实现(1)——导论

一、概述 可重配置技术是Xilinx提供的用来高效利用FPGA设计资源实现FPGA资源可重复利用的最新的FPGA设计技术&#xff0c;这种技术的发展为FPGA应用提供了更加广阔的前景。 术语“重构”是指FPGA已经配置后的重新编程。FPGA的重构有两种类型&#xff1a;完全的和部分的。完全重…

【附安装包】Substance3D 2022安装教程

软件下载 软件&#xff1a;Substance3D版本&#xff1a;2022语言&#xff1a;简体中文大小&#xff1a;4.0G安装环境&#xff1a;Win11/Win10&#xff08;1809版本以上&#xff09;硬件要求&#xff1a;CPU2.0GHz 内存4G(或更高&#xff0c;不支持7代以下CPU&#xff09;下载通…

即时通讯开发应用中的实时消息推送技术

即时通讯开发领域正以前所未有的速度蓬勃发展&#xff0c;实时消息推送技术成为促进即时通讯应用体验的关键要素。本文将深入探讨即时通讯应用中的实时消息推送技术&#xff0c;为读者呈现这一领域的全貌。 2. 实时消息推送的重要性 在当今数字化时代&#xff0c;人们日益需要…

git 提交错误,回滚到某一个版本

git log 查看版本号 commit 后面跟的就是版本号git reset --hard 版本号 &#xff08;就可以回滚到你要去的版本&#xff09;git push -f &#xff08;因为本地回滚了&#xff0c;所以和远程会差几个版本。所以这时候只有强制推送&#xff0c;覆盖远程才可以&#xff09;

实力认证!OceanBase获“鼎信杯”优秀技术支撑奖

6 月 30 日&#xff0c;2023 “鼎信杯”信息技术发展论坛在京隆重举办第二届“鼎信杯”大赛颁奖典礼。OceanBase 凭借完全自主研发的原生分布式数据库&#xff0c;以及丰富的核心系统国产数据库升级案例&#xff0c;斩获“优秀技术支撑奖”。 论坛上&#xff0c;国内首个基于在…

JavaScript基础03

JavaScript 基础 文章目录 JavaScript 基础for 语句for语句的基本使用循环嵌套倒三角九九乘法表 数组数组是什么&#xff1f;数组的基本使用定义数组和数组单元访问数组和数组索引数据单元值类型数组长度属性 操作数组 if 多分支语句和 switch的区别&#xff1a; 共同点 都能实…

C语言如何判断闰年?

首先需要了解闰年的判断规则&#xff0c;以下是百度百科的介绍&#xff1a; 1.普通年份能被4整除&#xff0c;且不能被100整除的&#xff0c;是闰年。&#xff08;如2004年就是闰年&#xff09; 2.世纪年份能被400整除的是闰年。&#xff08;如2000年是闰年&#xff0c;1900年不…

JVM-CMS

when 堆大小要求为4-8G 原理 初始标记&#xff1a;执行CMS线程->STW&#xff0c;标记GC Root直接关联的对象->低延迟 并发标记&#xff1a;执行CMS线程和业务线程&#xff0c;从GC Root直接关联的对象开始遍历整个对象图 重新标记&#xff1a;执行CMS线程->STW&a…

【炼气境】Java集合框架篇

【炼气境】Java集合框架篇 文章目录 【炼气境】Java集合框架篇概述接口Collection接口List接口ArrayList类LinkedList类 Set接口HashSet类LinkedHashSet类TreeSet类 Queue接口LinkedList类PriorityQueue类ArrayDeque Map接口HashMap类LinkedHashMap类TreeMap类 常用方法特性适用…

有点意思的 Java 递归调用

最近在刷一些问题的时候看到有下面一个问题 上面问的是当输入的字符串为什么的时候返回 True 总结 在做题目的时候&#xff0c;第一次还做错了。 这是因为解答这个题目的时间只有 3 分钟&#xff0c;没有自己看题目 后来拿着程序跑了下。 public void testGetPut() throws …

华为Mate60低调发布,你所不知道的高调真相?

华为Mate60 pro 这两天的劲爆新闻想必各位早已知晓&#xff0c;那就是华为Mate60真的来了&#xff01;&#xff01;&#xff01;并且此款手机搭载了最新国产麒麟9000s芯片&#xff0c;该芯片重新定义了手机性能的巅峰。不仅在Geekbench测试中表现出色&#xff0c;还在实际应用…

星际争霸之小霸王之小蜜蜂(八)--蓝皮鼠和大脸猫

系列文章目录 星际争霸之小霸王之小蜜蜂&#xff08;七&#xff09;--消失的子弹 星际争霸之小霸王之小蜜蜂&#xff08;六&#xff09;--让子弹飞 星际争霸之小霸王之小蜜蜂&#xff08;五&#xff09;--为小蜜蜂降速 星际争霸之小霸王之小蜜蜂&#xff08;四&#xff09;--事…

C++智能指针介绍与使用

什么是智能指针 智能指针是一种 C 标准库中的模板类&#xff0c;用于管理动态分配内存资源。它们提供了自动化的内存管理功能&#xff0c;可以帮助程序员在避免内存泄漏和野指针的同时&#xff0c;简化内存资源的手动释放。C 标准库中提供了三种主要的智能指针&#xff1a;std…

学习记录——Efficient MOdel轻量化主干模型(iRMB、EMO)、CATnet

Rethinking Mobile Block for Efficient Attention-based Models 结合 CNN 和 Transformer 的倒残差移动模块设计 ICCV-2023 实例化了一个面向移动端应用的iRMB基础模块&#xff08;Inverted Residual Mobile Block&#xff0c;倒残差移动模块&#xff09;&#xff0c;其同时具…

springboot如何区分测试环境、生产环境。分环境启动

一 通过Maven打包时&#xff0c;将不同环境文件打入jar包来区分环境&#xff0c;然后直接启动jar包即可 1 pom.xml文件project标签里面加入以下配置 环境关键字可以根据自己的习惯来改 <profiles><!--开发--><profile><id>dev</id><propert…

前端文件相关总结

先引用掘金上的一个总结&#xff0c;将前端会遇到的文件相关的知识点间的关系串联了起来。 前端技术提供了一些高效的解决方案&#xff1a;文件流操作和切片下载与上传。 1. 文件基本操作 1.1 数据流和文件处理的基本概念 数据流是指连续的数据序列&#xff0c;可以从一个源传输…

并发编程的故事——共享模式之无锁

共享模式之无锁 文章目录 共享模式之无锁一、提出问题二、CAS和volatile三、原子整数四、原子引用五、原子数组六、原子更新器七、原子累加器八、unsafe 一、提出问题 关于对共享变量修改的多线程问题其实就是指令交错问题导致取值的时机相同&#xff0c;最后修改之后以最后一…

【基于空间纹理的残差网络无监督Pansharpening】

Unsupervised Pansharpening method Using Residual Network with Spatial Texture Attention &#xff08;基于空间纹理的残差网络无监督泛锐化方法&#xff09; 近年来&#xff0c;深度学习已经成为最受欢迎的泛锐化工具之一&#xff0c;许多相关方法已经被研究并反映出良好…