第六十六条 同步访问共享的可变数据

平时开发中,单线程的逻辑一般都好控制,但是并发情况下,多线程同时操作一个数据,可控性的难度就增加了,因为并发的情况不确定,并且除了问题也不好复现。因此,并发是一个重点,也是一个难点,并发中对数据的控制操作,就成为了并发的重点。同步关键字 synchronized ,可以保证同一时刻,只有一个线程可以执行某个方法。同步是为了线程安全,对此,需要满足两个特性,原子性和可见性。 同步的意思是,当一个对象被一个线程修改时,可以阻止另外一个线程观察到对象内部不一致的情况,同时,如果有多个线程同时访问它,它就会被锁定,因此同步可以保证没有任何方法可以获取对象不一致的状态。另外,java中的原子性,举个例子,是指变量是 double 或 long,对于它们,即使是多线程操作,也能保证值不会出错。

为了提高性能,在读写原子数据时,应避免同步。这个建议是很危险并且错误的。因为读写原子数据是原子操作,但不保证一个线程的写入的值对另外一个线程一定是可见的,如果另外一个线程无法操作这个数据,那么,就很容易出问题。线程与线程间的通讯,是建立在互斥和同步的基础上。下面举个例子,暂时跟着例子的思维走即可。(补充,这个例子在 jdk1.6 以下是正确的,但1.8版本及以上,还有在android手机上, 主线程是可以访问子线程,子线程对主线程是可见的)。

如果需要停止一个线程,可以使用Thread.stop方法,但这个方法很久以前就不提倡使用了,因为不安全——使用它会使数据遭到破坏。因此,普遍做法是,让一个线程轮询一个boolean域,另一个线程设置这个boolean域即可:

    private boolean stopRequested;

    private void test() {
        try {
            Thread backgroundThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (!stopRequested) {
                        i++;
                    }
                }
            });
            backgroundThread.start();
            TimeUnit.MILLISECONDS.sleep(1000);
            stopRequested = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

设计的思路很好,我们开了一个子线程,在子线程中开启一个循环,然后让 int 类型值 i不停的增长,我们期待一秒后,把 stopRequested 值变为true,然后 while() 条件不成立,终止循环,i 不再增加。这个设计的愿望很好,但现实很骨感。因为这是两个不同的线程,由于线程限制,导致外面的线程改变了 stopRequested 的值,但 backgroundThread 线程访问不到 stopRequested 改变后的值,这就相当于把

    while (!stopRequested) {
            i++;
        }            
替换成了

    if(!stopRequested){
            while (true) {
                i++;
            }
        }

所以,问题就出现了,导致线程 backgroundThread 中的预想逻辑失效。那么,我们知道问题的原因了,就可以对症下药了。起因是线程间的互斥问题,就用到了开头提到的同步锁,synchronized 。之前是直接调用 stopRequested ,此时,我们把它的赋值和取值封装成方法,同时使用 synchronized 修饰方法,这样,各个线程就同步了。

    private void test1() {

        try {
            Thread backgroundThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (!stopRequested()) {
                        i++;
                    }
                }
            });
            backgroundThread.start();
            TimeUnit.MILLISECONDS.sleep(1000);
            requestStop();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    private synchronized void requestStop() {
        stopRequested = true;
    }

    private synchronized boolean stopRequested() {
        return stopRequested;
    }

赋值和取值的方法都被同步了,我们的功能也就实现了,这是通过同步锁实现的,我们也可以用另一种方式,就是 volatile ,例如下面的写法,没有对 stopRequested 的set和get方法进行同步锁

    private volatile boolean stopRequested;
    private void test2() {

        try {
            Thread backgroundThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (!stopRequested) {
                        i++;
                    }
                }
            });
            backgroundThread.start();
            TimeUnit.MILLISECONDS.sleep(1000);
            stopRequested = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

效果和上面一样。但我们要注意, volatile 修饰也是有限制的,例如

    private volatile int nextSerialNumber = 0;

    public int generateSerialNumber() {
        return nextSerialNumber++;
    }

nextSerialNumber++,是两步操作,先是对自身加1,然后再把值赋给自己,所以如果是并发情况调用 generateSerialNumber() ,还可能一个线程正在执行加1操作,还没有赋值,另外一个已经把它值给取到,所以会造成线程安全问题。解决方法是,加入synchronized并去掉volatile。或者直接使用系统的 AtomicInteger 、 AtomicLong类,自带线程安全。

    private final AtomicLong nextSerialNumber = new AtomicLong(0);

    public Long generateSerialNumber() {
        return nextSerialNumber.incrementAndGet();
    }

注意看上面括号中的话,jdk 1.8 上, 主线程是可以访问子线程的,我特意打印了一下,估计是jvm或者最新版做了修正。有了解的大神请留言。

    private boolean stopRequested;

    private void test3() {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                    System.out.println(" test i: " + i);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        backgroundThread.start();
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stopRequested = true;

    }

因为每此睡眠10毫秒,所以100毫秒内应该执行10次左右,结果确实打印了1到10。

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/84398358
今日推荐