并发编程的问题

并发编程的问题


安全性问题

​ 事实上,并不是所用的情况用多线程就一定比单线程快。多线程如果使用不当,不仅会带来严重的安全性问题,也会造成性能问题。

​ 在JVM内存结构中,我们知道栈、程序计数器、局部变量这些是线程私有的,但是进程范围内的资源,同一个进程内的线程都会共享进程内的地址空间,那么它们会共享堆上分配的对象。当多个线程并发运行时,它们就可能会访问或者修改其他线程正在使用的变量造成严重的后果,比如我们在JMM笔记中提到的read-modify-write。那么在并发编程中,如果我们可以对要访问的资源的可见性、有序性、原子性都加以保证,那么确实可以解决安全性问题,但是多线程以及JVM的编译优化给我们带来的好处就无法享受了,而事实上,我们可以由多种同步机制来维护并发编程中的数据安全,并不是所有的场景都需要无脑的加锁,我们完全可以根据我们的场景我们的需求,选择最适合我们的方案。

在这里插入图片描述

互斥

​ 最容易想到的在多线程编程下,对资源的保护那就是采用互斥的手段。所谓互斥,即同一时刻只有一个线程可以拿到这个资源。

临界区

​ 我们把一段需要互斥执行的代码就称为临界区


互斥的两种方案

​ 在操作系统种,对于进程互斥有两种实现方案:信号量与管程。事实上,它们只是两种方案,并不是实现。你完全可以用信号量去实现管程,也可以用管称去实现信号量。

以下内容部分摘自《操作系统–内核与设计》

信号量

​ 信号量,可以理解为有几张通行证的思想。简单的来说,信号量维护一个计数器和等待队列,这个信号量还会有三个方法:初始化、P和V,P和V分别是荷兰语的test(proberen尝试)和increment(verhogen增量),P方法和V方法可以理解为尝试获取资源与归还资源。

​ 当一个线程请求访问受到保护的临界区,这个线程首先要去尝试获取资源。怎么尝试呢,信号量就会先把计数器-1,然后查看当前计数器的值是否>0,如果>=0,就表明你可以获取资源;如果<0,就表示资源已经被人用完了,不好意思你要去等待队列等一等了。线程执行完业务逻辑,要归还资源,这时候信号量就会把计数器+1,如果当前计数器的值<=0,就表示等待队列还有人在等候,那么它就会去等待队列里唤醒一个线程,让它去重新尝试获取资源。
在这里插入图片描述

​ 内部它可以用下面的伪代码简单表达实现思路:

  public class FakeSemaphore {
   
    SemaphoreStruct semaphoreStruct;

    public FakeSemaphore(SemaphoreStruct semaphoreStruct) {
   
        this.semaphoreStruct = semaphoreStruct;
    }

    //获取资源
    public void up() {
   
        //计数器--
        semaphoreStruct.count.getAndDecrement();
        if (semaphoreStruct.count.get() < 0) {
   
            //线程阻塞
            //将线程插入到阻塞队列末尾
            //重新调度
        }
    }

    //释放资源
    public void down() {
   
        //计数器++
        semaphoreStruct.count.getAndIncrement();
        if (semaphoreStruct.count.get() <= 0) {
   
            //从阻塞队列中唤醒一个线程
        }
    }

}

class SemaphoreStruct {
   
    //事实上,如果count=1就可以用信号量实现线程互斥,count>1,就可以实现线程同步
    public AtomicInteger count = new AtomicInteger();
    public BlockingQueue queue;

    public SemaphoreStruct() {
   
        this.count.set(1);
        this.queue = new LinkedBlockingDeque();
    }
}

​ 信号量的count设计你会发现很巧妙,当count设计为1,就可以解决线程的互斥问题;当count设计>1,就可以解决线程间的同步问题。

这里只描述了信号量的简单设计,事实上jdk中已经有信号量的工具类java.util.concurrent.Semaphore

插个题外话,信号量的设计者Dijkstra也是图论中最短路径的大佬。膜拜一下。

管程

​ 管程也是一种互斥同步的解决方案,它其实就是将共享变量于对共享变量的操作封装起来,外部只能通过管程暴露的方法对共享变量进行操作。

​ 互斥:

​ 管称是互斥进入的

​ 同步:

​ 在管程中设置条件变量等待唤醒操作解决同步问题,让一个线程在不满足条件变量时进行等待,释放CPU使用权,其它线程执行时发现条件满足,再对它进行唤醒

​ 举个栗子,管程现在保护资源A,线程1来了发现条件不满足,于是执行到了一半sleep,让出CPU使用权,然后线程2执行了一半,发现条件满足了,对线程1进行唤醒,这时就有三种方案:

​ 线程2等待线程1执行完再继续执行

​ 线程1等待线程2执行完再执行

​ 规定唤醒为管程中最后一个可执行的操作

​ 以上三种方案就是管程的三种模型:Horae、Mesa、Hansen。Java语言中的管程使用的是第二种方案。

在字节码指令中,管程是通过monitor实现的。

总结

​ 信号量与管程都是同步互斥的解决方案,信号量使用场景更广泛一点,设计也更巧妙一点。如果计数器=1,就可以用信号量实现互斥,如果计数器>1,就可以用作线程之间的同步(协作)。但是信号量的使用难度也大一点,如果PV方法使用不当,就可能造成等待队列中的线程永远不会被唤醒。

​ 管程则更适合需要临界区的执行需要满足一定条件的情况,管程模型有两个队列,一个是等待访问临界区的等待队列,一个是等待满足条件的条件队列。线程想访问临界区,首先要去等待队列等候,轮到它访问临界区了,那么它执行的时候还需要看它是否需要满足一定条件,如果不满足,就去等待队列等候,等候别的线程执行的时候发现满足条件了再唤醒它。

​ 在Java中,有两种管程实现方案:synchronized与wait、notify;lock+condition,Java的管程实现采用了Mesa模型,也就是线程1不满足条件进入等待状态,线程2唤醒线程1后,线程1重新去等待队列排队,并且线程2执行完才能轮到下一个线程去访问临界区。


java中的管程实现

synchronized与wait、notify

​ 被synchronized保护的资源的模型如下:

在这里插入图片描述


测试一:
测试目的

​ 证明线程1进入wait阻塞后,线程2唤醒线程1,线程2会继续执行完之后线程1才可以继续执行

测试代码
public class MonitorTest1 {
    private static Object ob1 = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (ob1) {
                System.out.println(Thread.currentThread().getName() +
                        "获取到ob1资源啦=======");
                try {
                    ob1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +
                        "执行完释放ob1资源啦=======");
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (ob1) {
                System.out.println(Thread.currentThread().getName() +
                        "获取到ob1资源啦=======");
                System.out.println(Thread.currentThread().getName() +
                        "条件满足,我去唤醒其它线程=======");
                ob1.notifyAll();
                System.out.println(Thread.currentThread().getName() +
                        "执行完释放ob1资源啦=======");
            }
        });
        thread1.start();
        Thread.sleep(100);
        thread2.start();
    }
}
测试结果

在这里插入图片描述

结果分析

​ 可以看到Thread-0执行到一半发现条件不满足,跑到等待队列去等待并让出CPU资源,这个时候Thread-1进来,发现条件满足了,可以唤醒其它线程,这时Thread-0线程被唤醒,但是Thread-1依旧继续执行,直到Thread-1执行完毕让出CPU资源,Thread-0线程才可以继续执行,证明Java的管程模型确实是Mesa模型。


lock+condition

​ 简单的写栗子演示下:

测试代码
public class MonitorTest3 {
   

    public static void main(String[] args) throws InterruptedException {
   
        ConditionTest conditionTest=new ConditionTest();
        Thread thread = new Thread(() -> {
   
            try {
   
                Thread.sleep(1000);
            } catch (InterruptedException e) {
   
                e.printStackTrace();
            }
            conditionTest.method2();
        });
        thread.start();
        conditionTest.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值