多线程程序设计题型分析

本文详细探讨了Java中的线程同步和通信,包括生产者消费者模型的实现、synchronized与wait/notify、ReentrantLock与Condition的使用。此外,还介绍了如何通过信号量、volatile和park/unpark实现线程间的交替打印,展示了各种并发控制策略在实际问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

java线程可以分为设计与执行,设计无非就是调用start()并且等待JVM为我们映射到一个操作系统线程,并执行它的run()方法,而设计层面就是布置任务。
我们一般将“任务代码”抽取出一个资源类,而不是直接写在线程类的run方法。(synchronized尽量不出现在run方法中)

class res{
    static int i;
    int j;
    synchronized void a(){}
    synchronized void b(){}
}

如果我们将资源访问代码放到一个资源类,那么我们可以创建一个资源实例,然后创建多个线程对象同步地执行该任务。

Task3_ task = new Task3_(); //资源类
new Thread(task::first).start();  //线程操作资源类
new Thread(task::second).start();
new Thread(task::third).start();

如果将syn(this)或者syn方法声明在资源类中,对象锁指代的就是内存中存在的task对象。而如果是类锁,指代的是加载入内存的唯一的Task_的Class对象。
而如果将syn关键字申明在run方法中,对象锁指代的是的“runnable”对象绑定的锁对象,没有意义。推荐使用外部传入synchronize包裹的锁对象。

同一时刻,系统中只有某一个线程正在执行同步块,而如果看向res的话,就意味着a和b方法只有一个正在被调用,请求执行另一个方法的线程被阻塞,因为未抢到对象锁。(底层是CAS修改锁对象owner失败,阻塞进锁关联的同步队列)。

但是如果a方法是syn修饰而b方法用syn static修饰就不会影响,因为此时不是同一把锁了,如果有两个对象new res()分别为res1和res2,也不影响,因为两个对象分别代表不同的锁。

总结
线程的任务代码应该单独存放在资源类中,目的是明确锁对象和实现高内聚低耦合。资源类中必不可少的代码:成员字段和操作成员字段的synchronized修饰的代码。因为同步问题解决的问题就是实现互斥地访问临界资源,所以必须能够抽象出“资源值”字段。

实现生产者和消费者通信模型

生产者和消费者模型就是一个典型的多线程操作资源类的模型,进一步简化,共享资源就是一个数字,一组线程负责增,一组线程负责减,当数字为0就不能继续减,而当数字达到一个上限则不能继续增

生产者与消费者模型的高层次实现就是使用阻塞队列。因此实现生产者消费者模型其实也是实现阻塞队列的思路

依据高内聚低耦合原则,操作资源类的代码应该定义在资源类中,而用户(线程对象)仅仅调用资源类对外暴露的接口即可。

class P implements Runnable{
    private Resource target ;

    public P(Resource target) {
        this.target = target;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                target.produce();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

通过构造函数传入资源实例,任务:生产十次。消费者代码逻辑十分相似

class C implements Runnable{
    private Resource target ;
    public C(Resource target) {
        this.target = target;
    }
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                target.consume();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试类中,创建一个公共的资源类,以及两对生产者消费者。

        Resource r = new Resource(5);//指定一个上限
        new Thread(new P(r),"生产者A").start();
        new Thread(new P(r),"生产者B").start();
        new Thread(new C(r),"消费者A").start();
        new Thread(new C(r),"消费者B").start();

synchronized与wait/notify

class Resource{
    private int num;
    private int maxSize;

    public Resource(int maxSize) {
        this.maxSize = maxSize;
    }

    public synchronized void produce() throws InterruptedException {
        while (num==maxSize){
            wait(); // 这里的锁对象是当前resource实例绑定的monitor
        }
        System.out.println(Thread.currentThread().getName()+"生产后:"+num);
        num++;
        System.out.println(Thread.currentThread().getName()+"生产前:"+num);
        notifyAll();
    }
    public synchronized void consume() throws InterruptedException {
        while (num==0){
            wait();
        }
        System.out.println(Thread.currentThread().getName()+"消费前"+num);
        num--;
        System.out.println(Thread.currentThread().getName()+"消费后"+num);
        notifyAll();
    }
}

notifyAll()属于粗唤醒,一个消费者消费完毕,不但会唤醒生产者线程,还会唤醒某些消费者线程,这些被“虚假唤醒”的消费者线程醒来后,发现不满足条件将会调用wait继续睡眠,而这涉及一次线程切换(wait调用涉及系统调用),而且被虚假唤醒的线程拿到CPU什么有意义的事情也没干却成功抢占了CPU,这是低效的

reentrantLock与condition

class Resource2{
    private int num;
    private int maxSize;
    private ReentrantLock lock=new ReentrantLock();
    private Condition empty = lock.newCondition();
    private Condition full = lock.newCondition();


    public Resource2(int maxSize) {
        this.maxSize = maxSize;
    }
    public void produce() throws InterruptedException {
        lock.lock();
        try {
            while (num==maxSize){
                full.await();
            }
            num++;
            empty.await();
        }finally {
            lock.unlock();
        }
    }
    public synchronized void consume() throws InterruptedException {
        lock.lock();
        try {
            while (num==0){
                empty.await();
            }
            num--;
            full.await();
        }finally {
            lock.unlock();
        }
    }
}

基于reentrantLock的一个好处就是可以使用细粒度等待队列,减少不必要的虚假唤醒情况。

总结:消费者与生产者模型的实现和实现阻塞队列一个道理,主要就是需要处理好等待与唤醒的时机。

交替打印问题

建立三个线程ABC,各自分别打印10次A 、B和 C
实现交替打印,按照ABCABC…

synchronized实现

    private char cur = 'A';
    private int sum = 30; //打印十轮为例
    //任务打印字母,并且修改字母
    void doTask(char name){
        synchronized (this){
            while (sum>0){
                if(cur == name){
                    sum--;
                    System.out.println(Thread.currentThread().getName()+" 当前正在打印 "+name+" sum:"+sum);
                    //更新字母
                    if(name=='A')cur='B';
                    else if(name=='B')cur='C';
                    else cur='A';
                }else {
                    //唤醒另外两个字母进程
                    this.notifyAll();
                    //等到cur==name再执行任务
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            this.notifyAll();//任务完成
        }
    }

synchronized可以保证可见性,所以没有使用volatile修饰共享变量。
这道题中,前一个被打印的字母和打印次数,就是需要同步的资源。由于线程之间有序,因此离不开通信机制wait/notify。

假设某一时刻锁被B线程抢占,而此时的同步字母为A,说明这是一次无效的抢占,于是B线程notifyAll唤醒其他两个线程,然后this.wait()进入对象锁对应的阻塞队列。
直到线程A抢到了锁,成为了锁的owner。
A进程第一次执行if中的逻辑,修改字段值为B和sum–,然后执行了一次else,唤醒全部的wait进程(主要是唤醒C线程),然后进入waitSet阻塞。
(这种写法属于粗唤醒,会产生大量无效的唤醒,效率比较低)

JUC实现精确唤醒

ReentrantLock lock =new ReentrantLock();
Condition condition1 =lock.newCondition();
Condition condition2 =lock.newCondition();
Condition condition3 =lock.newCondition();

我们创建三个条件对象,分别用于三个线程,等待的原因(condition)可以分为:当前不是A、当前不是B和当前不是C

这里不再抽取出一个方法,而是定义三个方法,当前只展示A线程调用的方法first(),另外两个无非就是修改一下字母

void first(){
//锁写在循环外,避免不必要竞争,只获取一次即可
        lock.lock();
        try { 
            for (int i = 0; i < 10; i++) {
                while (syn != 'A') {//写在循环内,防止虚假唤醒
                    condition1.await();//非当前线程的任务,wait
                }
            System.out.println(Thread.currentThread().getName() + " " + i + " A");
                syn = 'B';
//执行完任务后,精确唤醒下一个线程,同时wait
                condition2.signal();//只唤醒B线程(精确唤醒)
                condition1.await();
            }
            condition2.signal();//最后一次唤醒。(最后一次比较特殊,避免死锁)
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
}

信号量实现

Semaphore(1)等价于互斥锁,而semaphore(0)可以用于实现前驱关系

private Semaphore s1 = new Semaphore(0);
private Semaphore s2 = new Semaphore(0);
private Semaphore s3 = new Semaphore(0);
private volatile int sum;
    void first(){
        while (true){
            try {
                s1.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" == "+ ++sum);
            s2.release();
            if(sum==28)return;
        }
    }
    void second(){
        while (true){
            try {
                s2.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" == "+ ++sum);
            s3.release();
            if(sum==29)return;
        }
    }

当A B线程会阻塞,而C线程会先唤醒A,然后阻塞,接着A唤醒B,B唤醒C依次类推。这里仅展示其中一个线程执行的方法

void third(){
    while (true){ 
        s1.release();
        try {
            s3.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" == "+ ++sum);
        System.out.println("================================================");
        if(sum==30)return;
    }
}

自旋读写volatile实现

三个线程并发执行,cur为A B C中任何一个,由于使用了volatile关键字保证内存可见性,某一个时刻cur和num被修改,其他线程将立刻知道

void first(){ // cur 和 num 都是共享变量,且由volatile修饰
    while (num<28){ 
        while (cur!='C');//自旋
        ++num;
     	System.out.println(Thread.currentThread().getName()+ " == A  " + num);
        cur='A';
    }
}

park/unpark实现

park/unpark都是以线程对象作为单位进行调用的。lockSupport不需要获取对象的monitor,而是给线程一个许可。
Permit的值只能是0和1(二元变量),unpark会给线程一个permit,最多是一个(这里可以理解为置位),而park()会消耗一个permit并返回,如果线程没有permit将被阻塞(试图重置为0,如果当前即为0则阻塞)

private static Thread A,B,C;
public static void main1(String[] args) {
    A = new Thread(()->{
        for (int i = 0; i < 10; i++) {
            //第一次调用park(),A进程会被阻塞
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+" 当前正在打印 A"+" 次数:"+i);
            LockSupport.unpark(B);//唤醒B进程
        }
    });
    B = new Thread(()->{
        for (int i = 0; i < 10; i++) {
            LockSupport.park();//B进程阻塞
            System.out.println(Thread.currentThread().getName()+" 当前正在打印 B"+" 次数:"+i);
            LockSupport.unpark(C);//唤醒C进程
        }
    });
    C = new Thread(()->{
        for (int i = 0; i < 10; i++) {
            LockSupport.unpark(A);//唤醒A进程
            LockSupport.park();//阻塞
            System.out.println(Thread.currentThread().getName()+" 当前正在打印 C"+" 次数:"+i);
        }
    });
    A.start();B.start();C.start();
}

由于每个线程对象的permit初始都为0,因此上面的原理其实类型semaphore(0)实现前驱关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值