多线程与高并发学习笔记——第二章:临界资源问题与代码同步

1. 临界资源问题演示

我们来模拟一个场景:5个售票员同时售卖100张票,卖完为止!
代码演示:

public class ThreadTest {
    
    
    public static void main(String[] args) {
    
    
        Runnable r = new Runnable() {
    
    
            int i = 100;
            @Override
            public void run() {
    
    
                while(i>0)
                {
    
    
                    i--;
                    System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余票数:" + i);
                }
            }
        };
        Thread t1 = new Thread(r,"t1");
        Thread t2 = new Thread(r,"t2");
        Thread t3 = new Thread(r,"t3");
        Thread t4 = new Thread(r,"t4");
        Thread t5 = new Thread(r,"t5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

执行结果:

在这里插入图片描述
在这里插入图片描述
可以看到,每个售票员正常买票,并输出了剩余票数,但是在这其中时不时就会出现票数显示重复,并且剩余票的数量并不是逐一递减,这就是因为多个线程同时操作一个变量导致的临界资源问题!

2. 临界资源问题分析

临界资源定义:
多个线程共享的资源叫临界资源

问题分析:
我们在上一篇文章中介绍过,多线程执行本质上是线程抢夺CPU时间片的结果,所以就可能出现t1线程抢夺到CPU时间片,对票数做了自减操作,此时CPU时间片被t2线程抢走,正常输入完成,因为每个线程的虚拟机栈独立,所以t2线程记录的剩余票数为97,所以仍然会输出剩余97!同样,此问题也会导致剩余数目不是顺序递减的情况,比如图二,t3线程抢夺到了CPU时间片,但是随即时间片又被其他线程抢夺,此时t3线程的剩余票数始终为71,此时CPU时间片被其他线程抢夺,票数减少,t3线程重新抢夺回CPU时间片后,可能剩余票数已经很少,但是t3线程的记录仍为71,所以导致了输出票数不是逐级递减的问题!

解决方法:
既然是由于抢夺时间片问题导致的问题,那就需要保证在变量使用的过程中,每次同时只能有一个线程操作该变量,使用同步逻辑同步线程即可!

3. 同步代码块

synchronized () {
    
    }

使用方法:
在大括号内的代码可以保证线程同步,小括号内为锁对象(可以为任何对象),代码执行时,系统会给代码块加上相应对象的锁,此时如果有其他线程访问,如果锁对象相同,就必须等待其它线程执行完成释放锁对象!

同步原理:
此时,如果已经有线程执行到同步代码处,并持有相同的锁,其他线程就会在执行到此处时进入锁池等待,待到当前线程执行完毕,释放锁标记后,锁池中等待的线程就会挣钱锁标记,正抢到锁标记的线程继续执行,其他线程继续等待!

public class ThreadTest {
    
    
    public static void main(String[] args) {
    
    
        Runnable r = new Runnable() {
    
    
            int i = 100;
            @Override
            public void run() {
    
    
                while(i>0)
                {
    
    
                    synchronized ("锁") {
    
    
                        i--;
                        System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
                    }
                }
            }
        };
        Thread t1 = new Thread(r,"t1");
        Thread t2 = new Thread(r,"t2");
        Thread t3 = new Thread(r,"t3");
        Thread t4 = new Thread(r,"t4");
        Thread t5 = new Thread(r,"t5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

此时输出就会发现票数已经是依次递减:
在这里插入图片描述

注意:如果此时另一个线程持有不同的锁对象访问,是可以访问的!

synchronized ("锁"+i) {
    
    
    i--; System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
}

此时可以发现,虽然代码被上锁,但是由于持有的锁并不相同,其他线程的代码依旧可以访问,同步代码块无效!
在这里插入图片描述

回到上方,我们对代码添加了同步代码块,线程正常执行,票数逐一递减,但是翻到最后我们就会发现这样的问题:
在这里插入图片描述
这是因为:
当t4线程执行到0时,其它线程已经通过了while循环的循环条件,进入锁池等待,当t4执行完后,其它线程并不知道剩余票数已经为0,只知道逐一递减,所以造成了负数的情况,所以,多线程并发时一定要注意临界资源问题,并加以额外的判断!

解决方法:

synchronized ("锁") {
    
    
    if(i==0)//在同步代码块中额外判断
        return;
    i--;
    System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
}

在这里插入图片描述
问题成功解决!

4. 同步方法

如果我们的同步逻辑比较简单,整个方法中的逻辑都需要同步,就可以直接使用Synchronized关键字修饰整个方法,此时整个方法就会被线程同步。

注意:我们知道同步代码需要有一个锁对象

  • 静态方法:锁对象为当前类的类锁,即:类名.class
  • 非静态方法:锁对象为当前对象,即this
public class ThreadTest {
    
    
    public  static int i =100;
    public synchronized static void soild()
    {
    
    
        if(i==0)
            return;
        i--;
        System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
    }
    public static void main(String[] args) {
    
    
        ReentrantLock reentrantLock = new ReentrantLock();
        Runnable r = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                while(i>0) {
    
    
                    soild();
                }
            }
        };
        Thread t1 = new Thread(r,"t1");
        Thread t2 = new Thread(r,"t2");
        Thread t3 = new Thread(r,"t3");
        Thread t4 = new Thread(r,"t4");
        Thread t5 = new Thread(r,"t5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

5. 显式锁ReentrantLock

ReentrantLock显式锁与synchronize同步代码块使用方法大致相同,不过可以将上锁的细节交给程序员控制。

public class ThreadTest {
    
    

    public static void main(String[] args) {
    
    

        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable r = new Runnable() {
    
    
            int i = 100;
            @Override
            public void run() {
    
    
                while(i>0)
                {
    
    
                    reentrantLock.lock();
                    if(i==0)
                        return;
                    i--;
                    System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余票数:" + i);
                    reentrantLock.unlock();
                }
            }
        };

        Thread t1 = new Thread(r,"t1");
        Thread t2 = new Thread(r,"t2");
        Thread t3 = new Thread(r,"t3");
        Thread t4 = new Thread(r,"t4");
        Thread t5 = new Thread(r,"t5");

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

在这里插入图片描述
测试发现,与上方代码执行完全相同,临界资源问题解决完毕!

猜你喜欢

转载自blog.csdn.net/qq_42628989/article/details/105720085