第十八讲 多线程(三)——多线程安全问题的解决方案——同步代码块和同步函数

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yerenyuan_pku/article/details/82670572

同步代码块

当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这个代码块被称作同步代码块,格式如下:

synchronized(对象) {
    需要被同步的代码
}

对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取CPU的执行权,也进不去,因为没有锁。火车上的卫生间就是一个经典的同步例子,对于简单的卖票程序,加入同步之后的代码为:

class SaleTicket implements Runnable
{
    private int tickets = 300;
    Object obj = new Object();
    public void run()
    {
        while (true)
        {
            synchronized (new Object())
            {
                if (tickets > 0)
                {
                    try {Thread.sleep(10);} catch (InterruptedException e) {}//让线程到这里稍微停一下。
                    System.out.println(Thread.currentThread().getName() + "...." + tickets--);
                }
            }
        }
    }
}

class TicketDemo3 
{
    public static void main(String[] args) 
    {
        //线程任务对象。
        SaleTicket t = new SaleTicket();

        //创建四个线程。通过Thread类的对象。
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

同步的前提

有可能出现这样一种情况,多线程安全问题出现后,加入了同步机制,没有想到,安全问题依旧!这时咋办呢?要想到肯定是同步出现了问题,我们只要遵守了同步的前提,就可以解决。同步的前提:

  1. 必须要有两个或者两个以上的线程;
  2. 必须是多个线程使用同一个锁。

必须保证同步中只有一个线程在运行。同步在目前的情况下保证了一次只能有一个线程在执行,其他线程进不来,这就是同步的锁机制。

同步的好处

解决了多线程的安全问题。

同步的弊端

当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

同步函数

例,银行有一个金库,有两个储户,分别存300元,每次存100元,存3次。试问如下程序是否有安全问题,如果有,该如何解决?

class Bank
{
    private int sum;
    public void add(int n)
    {
        sum = sum + n;
        try {Thread.sleep(10);} catch (InterruptedException e) {}
        System.out.println("sum = " + sum);
    }
}

class Customer implements Runnable
{
    private Bank b = new Bank();
    public void run()
    {
        for (int x = 0; x < 3; x++)
        {
            b.add(100);
        }
    }
}

class BankDemo 
{
    public static void main(String[] args) 
    {
        //1,创建任务对象。
        Customer c = new Customer();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

以上程序绝壁是有问题的,那我们又该如何找到问题?我们只要明确以下几点就能找到问题。

  • 明确哪些代码是多线程运行代码;
  • 明确共享数据;
  • 明确多线程运行代码中哪些语句是操作共享数据的。

同步代码块,咱已经很熟了,所以就没必要再用它了,不妨我们试试同步函数,同步函数其实就是在函数上加上了同步关键字进行修饰。

class Bank
{
    private int sum;
    public synchronized void add(int n)
    {
        sum = sum + n;
        try {Thread.sleep(10);} catch (InterruptedException e) {}
        System.out.println("sum = " + sum);
    }
}

class Customer implements Runnable
{
    private Bank b = new Bank();
    public void run()
    {
        for (int x = 0; x < 3; x++)
        {
            b.add(100);
        }
    }
}

class BankDemo 
{
    public static void main(String[] args) 
    {
        //1,创建任务对象。
        Customer c = new Customer();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

这里我们思考一个问题,同步函数用的是哪一个锁呢? 答案是函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。

验证同步函数使用的锁是this

需求:验证同步函数使用的锁是this。
我们可以这样做,启动两个线程,一个线程负责执行同步代码块(使用明锁),另一个线程使用同步函数(使用this锁)。两个线程执行的任务都是一样的(例如都是卖票),如果它们没有使用相同的锁,说明它们没有同步,会出现数据错误。下面我们就以卖票程序来验证该需求。

class Ticket implements Runnable {
    private int tick = 100;
    Object obj = new Object();
    //定义一个boolean标记。
    boolean flag = true;
    public void run() {
        if(flag) {
            while(true) {
                synchronized(this) {
                    if(tick > 0) {
                        try { Thread.sleep(10); } catch(Exception e) {}
                        System.out.println(Thread.currentThread().getName()+"...code:"+tick--);
                    }
                }
            }
        } else {
            while(true) 
                show();
        }
    }
    public synchronized void show() { // this
        if(tick > 0) {
            try { Thread.sleep(10); } catch(Exception e) {}
            System.out.println(Thread.currentThread().getName()+"...show...:"+tick--);
        }
    }
}
class ThisLockDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t); // 创建一个线程
        Thread t2 = new Thread(t); // 创建一个线程
        t1.start();
        // 主线程停10ms,此刻能运行的线程只有t1
        try { Thread.sleep(10); } catch(Exception e) {}
        t.flag = false;
        // 主线程醒了之后,t1和t2线程同时运行
        t2.start();
    }
}

验证static同步函数的锁是类名.class

现在我们思考如果同步函数被静态修饰后,使用的锁是什么呢?通过验证,发现不再是this,因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class,该对象的类型是Class。静态的同步方法,使用的锁是该方法所在类的字节码文件对象,即类名.class

class Ticket implements Runnable {
    private static int tick = 100;
    // Object obj = new Object();
    boolean flag = true;
    public void run() {
        if(flag) {
            while(true) {
                synchronized(Ticket.class) {
                    if(tick > 0) {
                        try { Thread.sleep(10); } catch(Exception e) {}
                        System.out.println(Thread.currentThread().getName()+"...code:"+tick--);
                    }
                }
            }
        } else {
            while(true) 
                show();
        }
    }
    public static synchronized void show() { 
        if(tick > 0) {
            try { Thread.sleep(10); } catch(Exception e) {}
            System.out.println(Thread.currentThread().getName()+"...show...:"+tick--);
        }
    }
}
class StaticMethodDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t); // 创建一个线程
        Thread t2 = new Thread(t); // 创建一个线程
        t1.start();
        // 主线程停10ms,此刻能运行的线程只有t1
        try { Thread.sleep(10); } catch(Exception e) {}
        t.flag = false;
        // 主线程醒了之后,t1和t2线程同时运行
        t2.start();
    }
}

同步函数和同步代码块的区别

  • 同步代码块使用的是任意的对象作为锁;
  • 同步函数只能使用this作为锁。

同步函数和同步代码块的应用场景

如果说一个类中只需要一个锁,这时可以考虑同步函数,使用this,写法简单。但是,一个类中如果需要多个锁,还有多个类中使用同一个锁,这时只能使用同步代码块。建议使用同步代码块。

猜你喜欢

转载自blog.csdn.net/yerenyuan_pku/article/details/82670572