拾人牙慧系列--Synchronized的理解

前言

本系列文章,将在各路大神文章的基础上,总结提炼出自己的感悟,力求将大神的观点总结的更加凝练,希望站在巨人的肩膀上,能看得更远

本篇引用文章

Java 并发编程系列文章(这个是第一篇,里面有其余相关的文章)

内容提要

在Java的多线程并发编程里面,常常会遇到多个线程同时共同访问同一个资源的情况,本文将描述如何避免此种情况下产生的冲突问题

正文

  • 1.问题的产生

想必多数人都去银行存过钱,也都用银行卡刷过卡消费(没有的就去试试,别抬杠哈~)。那么你有没有想过,银行的系统怎么保证你在同时进行存钱和刷卡消费的情况下(就假设你拿着主卡去存钱,女朋友拿着副卡去败家),不会把你的账户余额搞错呢?我们来模拟一下,同时存钱+消费的场景

存钱消费行为模拟

它们对应以下三种过程:
存钱消费过程模拟.png

可以看到,在这个场景里面,小明(线程1)和女朋友(线程2)在他们各自的业务逻辑之内,用到了同一个账号(资源),当他们同时使用这个账号时,多数情况导致了账号余额(资源的属性)得到了错误的数据变更。那么我们希望的,当然是得到正确的数据变更,从上述过程可以发现,数据的错乱,源自于不同线程对同一资源的同时操作(具体来说是写操作),所以,我们的问题,即可归结为如何防止不同线程在同一时刻操作同一资源

  • 2.问题的解决

如标题所示,自然,我们要用Synchronized来解决这个问题(当然也有其他法子,但是不是本篇的讨论范围)

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。
摘自这里--http://www.cnblogs.com/paddix/p/5367116.html

结合上面的例子来说,相当于小明(线程1)在存钱(操作资源)之前,通过Synchronized先把账号给霸占起来了,女朋友无法操作(也就是给资源加了锁,并且线程1把锁获取到手,达到互斥的效果),当小明存钱完毕,系统将余额更新到账号里(相当于将资源副本同步到堆内的资源),小明不再霸占账号(释放锁),女朋友(线程2)再进行与小明一样的流程。而不管是小明先存钱,还是女朋友先消费,最终账号的余额都会是一个正确的数额(也就是说,线程的执行次序,并不影响资源变更的正确性,只要保证资源使用的互斥性即可)

  • 3.小小的例子

光说不练假把式,来实操一下,我们把他们的存储-消费过程,用代码实现一次
账号类

public class Account {
    String name;
    int balance;

    public Account(String name) {
        this.name = name;
        balance = 0;
    }

    public void saveMoneyNotSync(String user, int money) {
        balance += money;
        System.out.println(user + "存入" + money + "元");
    }

    public int withdrawMoneyNotSync(String user, int money) {
        balance -= money;
        System.out.println(user + "取出" + money + "元");
        return money;
    }

    /**
     * 1.这种用法,实际上是将new 出来的Account对象锁定,一旦线程获取了对象锁,那么该对象的其他非静态方法将无法被其他线程使用
     * 2.如果 synchronized 用在静态方法上,那么锁定的就不是对象,而是这个类的Class,一旦线程获取了Class锁,那么该类的其他静态方法将无法被其他线程使用
     */
    public synchronized void saveMoney(String user, int money) {
        balance += money;
        System.out.println(user + "存入" + money + "元");
    }

    public synchronized int withdrawMoney(String user, int money) {
        balance -= money;
        System.out.println(user + "取出" + money + "元");
        return money;
    }

    public int getBalance() {
        System.out.println(name + "的账户余额为:" + balance);
        return balance;
    }
}

小明线程,用于模拟一次存钱过程

public class MingThread extends Thread {
    Account account;

    public MingThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        super.run();
        account.saveMoneyNotSync("小明", 100);
    }
}

女朋友线程,用于模拟一次取钱过程

public class GirlFirThread extends Thread {
    Account account;

    public GirlFirThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        super.run();
        account.withdrawMoneyNotSync("小红", 100);
    }
}

生活线程,模拟同时进行存钱和消费,由于同时进行一次的情况,很难出现错误,因此我们来模拟存取10000次的情景,小明往账户里面存钱10000次,一次100,女朋友取钱10000次,一次100。

public class Life {
    public static void main(String[] args) throws InterruptedException {
        final Account account = new Account("爱的账号");

        Thread guangZhou = new Thread() {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < 10000; i++) {
                    MingThread mingThread = new MingThread(account);
                    mingThread.start();
                }
            }
        };

        Thread shenZhen = new Thread() {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < 10000; i++) {
                    GirlFirThread girlFirThread = new GirlFirThread(account);
                    girlFirThread.start();
                }
            }
        };

        guangZhou.start();
        shenZhen.start();

        Thread.sleep(1500);
        account.getBalance();
    }
}

如果按照理想情况,最后账户余额应该是0,我们来看看几组执行结果






显然,虽然也有正确的时候,但是多数情况下,无法得到正确的数值。你可以再去试试加了synchronized的方法,无论你怎么调用,总能得到正确的答案(懒得截图了,一试便知)

衍生问题

既然讨论了Synchronized,必然要再讨论一下跟线程同步相关的一些问题。我们来着重讨论以下几个方法:
wait() notify() sleep() join()

wait()

wait()是Object的方法,作用是使当前线程进入等待,来看看官方文档:

Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0).

散装翻译:使当前线程进入等待(我的理解是哪个线程调用这个方法,就使哪个线程进入等待),直到其他的线程通过notify()唤醒该线程,或者使用notifyAll()唤醒全部线程

The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake up either through a call to the notify method or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.

散装翻译:调用wait()的线程,必须拥有对应Object对象的监视器,因为wait()会释放这个监视器(如果你都没有,你释放个啥?),然后如果这个线程被唤醒,会阻塞等待获取监视器的所有权,再继续执行

As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:

散装翻译:和wait(time)方法一样,等待状态可能被打断,或者也可能有虚假唤醒(不太懂,可能是线程自己就醒了,但是却没有得到监视器的状态?),所以wait()方法应该在一个队列里面使用。如下:

   synchronized (obj) {
         while ()
             obj.wait();
         ... // Perform action appropriate to condition
     }

我们来讨论一下wait()的应用场景,假设有一个线程(ThreadA)对某个对象(Object)进行处理,但是Object的某一个属性需要从另一个线程(ThreadB)之中获取,ThreadB将数据给到Object之后,再通知ThreadA进行后续处理,所以当ThreadA需要暂停自己的操作,等待其他线程的时候,wait()就派上用场了

这实际上有点像工厂组装产品,假设有两个车间共同完成一个产品的组装流程,它们不一定谁先拿到产品(Product),但是Product必须先由车间一组装好零件,车间二才可以继续。我们可以用代码来模拟这一过程

Product主要代码

public class Product {
    public void setComponents1(String components1) {
        System.out.println("产品" + name + "开始组装");
        System.out.println("零件- - -" + components1 + "- - -组装中");
        try {
            //模拟组装耗时
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.components1 = components1;
    }

    public void setComponents2(String components2) {
        System.out.println("零件- - -" + components2 + "- - -组装中");
        try {
            //模拟组装耗时
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.components2 = components2;
        System.out.println("产品" + name + "组装完成");

    }

    public boolean isComplete() {
        return components1 != null && !components1.equals("") && components2 != null && !components2.equals("");
    }
}

车间1

public class Step1 extends Step {
    @Override
    public void machining() {
        synchronized (product) {
            System.out.println("车间1开始工作");
            product.setComponents1("车轮子");
            System.out.println("车间1工作完毕");
            //组装完成,唤醒需要被唤醒的线程
            product.notify();
        }
    }
}

车间2

public class Step2 extends Step {
    @Override
    public void machining() {
        System.out.println("车间2开始工作");
        synchronized (product) {
            while (product.getComponents1() == null || product.getComponents1().equals("")) {
                try {
                    //如果第一个零件没有组装完毕,就等待
                    product.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            product.setComponents2("车身");
        }
        System.out.println("车间2工作完毕");
    }
}
执行结果

从结果可以看到,车间2开始工作之后,由于车间1还没完成组装工作,车间2进入等待,直到车间1完成组装,才被重新唤醒

sleep()

Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.

散装翻译:让线程暂停执行一定的毫秒数,暂停时间的精度取决于系统计时器和调度程序的精度和准确性。sleep()方法不会释放锁

sleep()方法算是非常常用了,wait()和sleep()一样,都会使调用的线程进入等待/暂停,wait()会把锁释放,其它线程可以使用相应的资源,但是sleep()不行,线程暂停期间,其他线程也不能使用这个资源,举个栗子

先定义一个超简单的类

public class DoSomething {
    public void doSomething(String thing) {
        System.out.println(thing);
    }
}

用wait()的线程,很简单,打印十次,每次wait(1000),注意在循环期间,doSomething是被锁住的

public class WaitThread extends Thread {
    DoSomething doSomething;
    public WaitThread(DoSomething doSomething) {
        this.doSomething = doSomething;
    }
    @Override
    public void run() {
        super.run();
        synchronized (doSomething) {
            for (int i = 0; i < 10; i++) {
                doSomething.doSomething(Thread.currentThread().getName() + "---WaitThread is working");
                try {
                    doSomething.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

用sleep()的线程,同样,打印十次,每次sleep(1000),循环期间,doSomething同样锁住

public class SleepThread extends Thread {
    DoSomething doSomething;
    public SleepThread(DoSomething doSomething) {
        this.doSomething = doSomething;
    }
    @Override
    public void run() {
        super.run();
        synchronized (doSomething) {
            for (int i = 0; i < 10; i++) {
                doSomething.doSomething(Thread.currentThread().getName() + "---SleepThread is working");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

先试试用wait()

        DoSomething doSomething = new DoSomething();
        new WaitThread(doSomething).start();
        new WaitThread(doSomething).start();
        new WaitThread(doSomething).start();

执行结果
wait执行结果

可以看到,只要一wait,doSomething对象的锁就被其他线程持有,所以它们可以交替执行

再看看用sleep()

        DoSomething doSomething = new DoSomething();
        new SleepThread(doSomething).start();
        new SleepThread(doSomething).start();
        new SleepThread(doSomething).start();

执行结果
sleep执行结果

可以看到,sleep()之后,其他线程并不能执行打印,即是说没有获取doSomething的对象锁,也就说明sleep()并不释放锁

notify()

notify()用于唤醒正在等待当前线程持有的object锁的线程,相当于一个提醒功能。打个比方,公司只有一个微波炉,当我在使用微波炉的时候,其他人只能等待,那么当我快使用完毕的时候,我可以提醒某人:“小李,微波炉我快用好了,你准备”,这个提醒,就是notify(),至于小李能不能在一众大汉之中抢到微波炉,那我就管不了了,我只负责提醒。看下文档

Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object's monitor by calling one of the wait methods.

散装翻译:任意唤醒一个正在等待当前线程持有的对象锁的线程

The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.

散装翻译:并不是线程被唤醒了,就一定能马上拿到对象锁,而是首先需要当前线程放弃这个锁,其次需要和其他等待该锁的线程进行竞争

This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:

By executing a synchronized instance method of that object.
By executing the body of a synchronized statement that synchronizes on the object.
For objects of type Class, by executing a synchronized static method of that class.
Only one thread at a time can own an object's monitor.

散装翻译:只有拥有了对象锁的线程,才能调用这个对象的notify()方法。
有三种获取锁的方法:锁方法,锁代码块,锁Class。
同一时刻,只能有一个线程拥有某个对象的锁

同样的,我们来实操一下,通过下面的例子,来展示notify()可以唤醒线程,以及notify()本身并不具备释放锁的功能

先定义一个Service类,用于进行wait和notify

public class Service {
    public void testWait(Object lock) {
        synchronized (lock) {
            System.out.println("begin wait by==" + new SimpleDateFormat("mm:ss").format(new Date()));
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end wait by==" + new SimpleDateFormat("mm:ss").format(new Date()));
        }
    }

    public void testNotify(Object lock) {
        synchronized (lock) {
            System.out.println("begin notify by==" + new SimpleDateFormat("mm:ss").format(new Date()));
            lock.notify();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                lock.wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end notify by==" + new SimpleDateFormat("mm:ss").format(new Date()));
        }
    }
}

testWait()方法很简单,获取到锁之后,打印当前时间,然后等待,被唤醒并重新获得锁之后,继续打印当前时间,这样,可以知道中间等待了多久
testNotify()复杂一些,获取到锁之后,打印当前时间,然后notify(),为了证明notify()并不释放锁,我们特地让当前线程sleep2秒,再等待(wait()也就意味着让出锁)。如果说notify()是释放锁的,那么testWait()方法里面第二次打印的时间,应该和第一次打印的时间一致;反之,应该是隔了两秒钟。一秒钟之后,testNotify()所在线程再次唤醒,继续执行sleep5秒,最后输出

试一试

public class WaitThread extends Thread {
    @Override
    public void run() {
        super.run();
        Service service = new Service();
        service.testWait(lock);
    }
}
public class NotifyThread extends Thread {
    @Override
    public void run() {
        super.run();
        Service service = new Service();
        service.testNotify(lock);
    }
}
        Object lock = new Object();
        WaitThread waitThread = new WaitThread(lock);
        NotifyThread notifyThread = new NotifyThread(lock);

        waitThread.start();
        Thread.sleep(10);
        notifyThread.start();

执行结果



可以看到,end wait这一句,确实和begin wait相差了两秒

join(long millis)

join()方法用于使当前线程等待,直到子线程执行完毕(如果设置了时间限制,则至多等待设置的时间这么久),当前线程才继续执行,看文档

Waits at most millis milliseconds for this thread to die. A timeout of 0 means to wait forever.
This implementation uses a loop of this.wait calls conditioned on this.isAlive. As a thread terminates the this.notifyAll method is invoked. It is recommended that applications not use wait, notify, or notifyAll on Thread instances.

散装翻译:等待子线程结束(在本线程消亡之前),如果设置时间n毫秒,则至多等待n毫秒(子线程如果在n毫秒前结束,则等待 join()的内部实现,是在一个循环里面,以isAlive()为判断条件,去调用wait(),当有子线程终结,子线程会调用notifyAll(),唤醒等待的线程
所以,一般情况下,不建议使用Thread本身的wait(), notify(), 或者 notifyAll()方法(因为可能会打乱Thread自身的唤醒逻辑)

简单的例子

    public class Test extends Thread {
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

不join()的情况下:

        new Test().start();
        System.out.println("main==========");
主线程自己先执行了

join()一下:

        Test test = new Test();
        test.start();
        test.join();
        System.out.println("main==========");
主线程会等待子线程结束去唤醒它

本篇内容到此结束,感谢收看~~

你可能感兴趣的:(拾人牙慧系列--Synchronized的理解)