一起Talk Android吧(第二十四回:Java多线程编程二)

99 阅读5分钟

各位看官们,大家好,上一回中咱们说的是Java多线程编程的例子,这一回咱们继续说该例子。闲话休提, 言归正转。让我们一起Talk Android吧!

看官们,我们在上一回中介绍了如何创建和使用线程。这一回中主要介绍线程同步相关的知识。关于线程同步的概念,我们在C栗子中介绍过,我们假定大家明白这个概念,这里我们主要介绍如何使用Java来实现线程同步。Java提供了synchronized关键字来实现线程同步,接下来,我们通过伪代码来演示一下:

    方法一:
synchronized(object){
    // do some thing
} 
    方法二:
synchronized void fun(){
     //do some thing
  }

方法一中的内容叫做同步块,方法二中的内容叫做同步方法。这两种方法都可以实现线程同步的效果,使用哪一种都可以,没有特别的推荐,不过同步块中的object我们推荐使用this对象。因为该关键字使用了锁变量的方法来实现线程同步,object可以看作是一种锁变量,使用this后就可以把当前的对象当作锁变量,这样就能避免把锁加在不同对象上。这时有两位看官提问了:那么同步方法如何使用锁变量呢?为什么要使用同步呢?

看官莫急,我们先来回答第一位看官的问题,同步方法的语法中没有指定锁变量,不过它默认使用方法当前的对象做为锁变量,也就是this。关于第二位看官的问题,使用同步是为了解决访问共享资源的问题,或者说解决访问临界资源的问题。这么说可能让大家觉得有点抽象,接下来我们通过具体的例子来进行说明。

我们还是使用前一章回的代码,因为该代码有潜在的错误,所以我们可以通过这一回的知识来分析和修改其中的错误。

        public void run() {
            for(int i=0; i<5; ++i){
                setData(i+1);               
                System.out.println(Thread.currentThread().getName()+ " Data :"+ getData());
            }
        }

这是run方法中的内容,我们在该方法中修改了data变量的值,然后把它修改后的值打印出来。每个线程都会执行该内容,试想一下如果线程A执行完setData这行语句后还没有没执行下一行语句,这时线程B也执行了setData这行语句,那么线程A使用getData读取数据时读取到的是线程B修改后的数据,而不是线程A修改后的数据。这种情况是完全有可能发生的,我们在上一个章回中之所以没有发现,就是程序运行比较快,我们添加点代码让程序运行慢一些,方便我们发现其中的错误。

    public void run() {
            for(int i=0; i<5; ++i){
                setData(i+1);               

                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                        e.printStackTrace();
                }               
                System.out.println(Thread.currentThread().getName()+ " Data :"+ getData());
            }
        }

在上面的代码中,我们使用了sleep函数,让程序在setData完成后等待1秒钟,然后再通过getData方法读取数据。 下面是程序的运行结果:

Thread-1 Data :1
Thread-0 Data :1
Thread-1 Data :2
Thread-0 Data :3
Thread-1 Data :3
Thread-0 Data :4
Thread-1 Date :4
Thread-0 Data :5
Thread-1 Data :5
Thread-0 Data :5

从上面的程序运行结果中可以看到程序运行时发生了错误:

    Thread-0的数据不是从15递增的,而是1-3-4-5-5;
    Thread-1的数据是从15递增的,1-2-3-4-5

这两个线程运行同样的代码,但是是却得到了不同的结果,这显然存在错误。错误的原因我们已经分析过了,接下来我们看看如何修改这种错误,这时候synchronized关键字就派上用场了。在上面的程序中数据data可以看作是共享资源或者叫临界资源,我们可以把访问临界资源的操作封装成一个临界区,然后使用同步块来控制该临界区,这样就可以放心地访问临界资源了。这是整体的思路,我们通过代码来演示一下具体的操作。

    public void run() {
            for(int i=0; i<5; ++i){
                synchronized (this) {                       
                    setData(i+1);               

                    try{
                    Thread.sleep(1000);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }               
                    System.out.println(Thread.currentThread().getName()+ " Data :"+ getData());
                }
            }
        }

在代码中我们使用同步块,同步块中使用的锁变量是this对象。它能不能解决程序中潜在的错误呢?那么我们来验证一下,运行程序后,得到以下结果:

Thread-0 Data :1
Thread-1 Data :1
Thread-1 Data :2
Thread-1 Data :3
Thread-0 Data :2
Thread-0 Data :3
Thread-0 Data :4
Thread-0 Data :5
Thread-1 Data :4
Thread-1 Data :5

从上面的程序运行结果中可以看到程序运行结果是正确的。

    TThread-0的数据从15递增:1-2-3-4-5;
    Thread-1的数据也是从15递增的:1-2-3-4-5

这两个线程得到了相同的运行结果。这就验证了我们的猜想:使用同步块可以解决访问临界资源的问题。 看官们,完整的代码我就不再列出了,大家只需要使用同步块的代码替换掉原来的旧代码就可以。至于程序的运行结果也是不同的,这取决于线程的调度,我们在前面章回中分析过其中的原因,不过有一点是可以保证的,那就是每个线程都会把数据从1递增到5,如果不是这样的运行结果,那么赶快检查一下是不是同步块使用不当呢。

看官们,使用同步块会损失一些性能,不过它保证了程序结果的正确。为此Java除了对虚拟机进行优化外,还提供了其它的锁变量,此外,Java还提供了其它的同步方法,比如信号量。这些内容和同步块的原理类似,如果大家感兴趣的话,可以自己去学习,相信大家可以举一反三,很快地掌握这些线程同步方法。

各位看官,关于Java多线程编程的例子咱们就介绍到这里,欲知后面还有什么例子,且听下回分解!