解决Java多线程数据同步问题
8755字,阅读需时30分钟

文章导读

应用多线程技术可以提高应用程序的响应能力,充分利用计算机的CPU资源,为用户提供更好的应用体验。但也会存在因多个线程竞争使用并修改同一数据而产生数据同步的问题。本文通过一个多线程案例说明多线程数据同步问题发生的原因及解决方案。


文章分成两个小节。第一小节通过协同创作图书案例,说明当多个作者在同一时间修改同一图书内容时,就会发生数据同步问题;第二个小节提出解决数据同步问题的解决方案。

第一小节 一个协同创作图书案例引发的数据同步问题

当多个线程在同一时间修改同一数据时,就会发生数据同步的问题。举一个例子来说明这个问题,考虑一个编程任务场景,一个协同写作系统允许多个作者共同编写图书,任务要求是每个作者可以添加内容到图书,但不能修改和删除别人的内容,作者对内容编辑完成后,需要将自己编辑的内容添加到图书中。

要完成该编程任务,需要定义一个Book类,用于存储图书内容;定义一个Author线程类,用于编辑内容并将编辑后的内容添加到Book对象。

(1)定义一个Book类,设置属性content,存储图书内容,代码如下:

package com.milihua.bookwriter;
public class Book {
    String content;
    public Book()
    {
     content = "";
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
 }

(2)定义一个Author线程类,设置属性name、docBook,name存储作者名称,docBook对象为传入的图书对象。在run方法中,让线程休眠1000毫秒,模拟创作过程。代码如下:

package com.milihua.bookwriter;
public class Author implements Runnable{
    Book docBook;
    //作者姓名
    String  name;
    public Author(Book inBook,String inName)
    {
     docBook = inBook;
     name = inName;
    }
   
    public void setDocBook(Book inBook)
    {
     docBook = inBook;
    }
   
    @Override
    public void run() {
        // TODO Auto-generated method stub
        //模拟创作,让线程等待1000毫秒
        try {
            Thread.sleep(1000);
            //编辑内容并添加到docBook对象
            String str = docBook.getContent() + "\n" + name + ":我的创作内容";
            docBook.setContent(str);
           
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

(3)建立主线程类,创建5个作者线程,开始模拟图书内容创作。代码如下:

package com.milihua.bookwriter;
public class {
    public static void main(String[] args) {
        Book  docBook = new Book();
        //创建5个作者
        Thread t1 = new Thread(new Author(docBook,"李某"));
        t1.start();
        Thread t2 = new Thread(new Author(docBook,"王新"));
        t2.start();
        Thread t3 = new Thread(new Author(docBook,"张某"));
        t3.start();
        Thread t4 = new Thread(new Author(docBook,"赵三"));
        t4.start();
        Thread t5 = new Thread(new Author(docBook,"李四"));
        t5.start();
        try {
            Thread.sleep(10000);
            System.out.println("共同创作的图书内容:\n" + docBook.getContent());
 
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行程序,在控制台得到输出结果如下图所示:

blob.png

图1  BookWriter示例程序输出结果

观察输出结果,发现程序输出有些问题,少了一个作者的创作内容,多次运行程序,发现缺少哪个作者并没有规律。这显然不符合程序要求,检查程序也没有发现其它问题。这个隐藏的问题就是多线程数据同步的问题,多个作者线程试图同时更新图书对象内容,导致部分作者创作内容丢失。

问题应该出在Author线程类编辑内容和添加内容的代码上,编辑内容和添加内容的代码如下:

try {
            Thread.sleep(1000);
            //编辑内容并添加到docBook对象
            String str = docBook.getContent() + "\n" + name + ":我的创作内容";
            docBook.setContent(str);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

假设有A、B两个Author线程,A 线程先于B线程执行了docBook.getContent()语句,该语句执行完成后,A线程获取了当前图书内容,当A线程要执行docBook.setContent(str)语句时,A线程的运行被中断了(JVM分配给它的时间片用完了)。这时,假设B线程被执行,并且顺利完成了docBook.getContent()和docBook.setContent(str)语句,B线程编辑的内容已经存储到Book对象中。当A线程再次被执行时,A线程的docBook.setContent(str)语句将重写Book对象的内容,因为A线程已经在B线程之前获取了Book对象的内容,将会导致B线程写入到Book对象的内容被覆盖掉。

要解决Book对象同步的问题,就需要用到Java提供的线程同步方法,下面将利用synchronized方法解决本节案例遇到的问题。

第二小节  解决数据同步存在的问题

前面的案例提出了多线程数据同步所存在的问题,当多个Author线程修改同一个共享数据时,会发生数据覆盖或丢失的问题。

为了解决多个线程修改同一数据而发生数据覆盖或丢失的问题,Java提供了synchronized关键字来加保护伞,以保证数据的安全。synchronized关键字对共享数据的保护有两种方式,一种是用于修饰代码块,一种是用于修饰方法。

修饰代码块是把线程体内执行的方法中会涉及到修改共享数据的代码块,通过{}封装起来,然后用synchronized关键字修饰这个代码块。代码如下:

package com.milihua.bookwriter;
public class Author implements Runnable {
    Book docBook;
    // 作者姓名
    String name;
 
    public Author(Book inBook, String inName) {
        docBook = inBook;
        name = inName;
    }
    public void setDocBook(Book inBook) {
        docBook = inBook;
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        // 模拟创作,让线程等待1000毫秒
        // 添加synchronized关键字
     try {
                Thread.sleep(1000);
                // 编辑内容并添加到docBook对象
        } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
        }
        synchronized (this) {
            String str = docBook.getContent() + "\n" + name + ":我的创作内容";
            docBook.setContent(str);
        }
    }
}

上面的代码把获取和修改docBook对象内容的语句封装在{}内,并用synchronized关键字修饰,synchronized关键字要求传入对象的引用。需要注意的是,一定要传入需要保护的数据所在的对象。在本案例中,保护的数据为Book对象,Author线程类的docBook属性值是由主线程类实例化Author线程类时传入的对象引用,因此synchronized关键字后面的对象参数应传入主线程对象引用。

package com.milihua.bookwriter;
public class BookWriter {
    public static void main(String[] args) {
        BookWriter  book = new BookWriter();
        Book  docBook = new Book();
        //创建5个作者
        Thread t1 = new Thread(new Author(docBook,"李某",book));
        t1.start();
        Thread t2 = new Thread(new Author(docBook,"王新",book));
        t2.start();
        Thread t3 = new Thread(new Author(docBook,"张某",book));
        t3.start();
        Thread t4 = new Thread(new Author(docBook,"赵三",book));
        t4.start();
        Thread t5 = new Thread(new Author(docBook,"李四",book));
        t5.start();
        try {
            Thread.sleep(10000);
            System.out.println("共同创作的图书内容:\n" + docBook.getContent());
 
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

在主线程类中,实例化了BookWriter对象,并在实例化Author线程类时,将实例化的BookWriter对象传入到Author线程类。Author线程类的synchronized关键字将使用传入的BookWriter对象。

程序经过改造后,输出结果如下图所示:

blob.png 

图2 使用synchronized关键字解决数据同步问题输出结果

从输出结果可以看出,程序输出了正确的结果。其原理是当A和B两个并发Author线程执行synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。也就是说A和B是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

synchronized关键字对共享数据的保护,实际是利用了锁技术。锁技术又分为对象锁和内置锁。

Java内置锁是一个互斥锁,最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

对象锁和内置锁在概念上是一致的。区别是内置锁用于对象内部的代码块和方法,而对象锁用于静态方法或者一个类的实例化对象。

文章小结

1、在多线程应用程序中,如果多个线程对共享数据仅是使用,则无需对共享数据做同步保护。当线程要对共享数据进行修改时,必须要对修改的共享数据进行同步保护,否则会出现莫名其妙的问题,程序出现的BUG也不好定位。

2、在java中,每一个实例对象有且仅有一个同步锁。当用synchronized关键字修饰代码块时,要求传入一个对象引用,synchronized关键字为这个传入的对象加同步锁。如果修饰一个方法,synchronized关键字为当前对象加同步锁。

思考与练习

模拟在线售票问题。购票者可以通过售票网站在线购买车票,车票以列表方式存储,车票数量有限,只有100张,请创建120个线程模拟购票者,当购票者线程购买车票时,需要锁定存储车票的列表,直至释放,其它线程才能访问。

请编程实现上述任务要求。

我要评论
全部评论
郎宏林
授课老师
授课老师简介
项目经理,系统分析和架构师,从事多年中文信息处理技术。熟悉项目管理、擅长项目需求分析和设计、精通Java、C#、Python等编程语言。
下载APP

手机、电脑同步学

用微信或手机浏览器扫描二维码,即可下载APP。

  • 备案号:鲁ICP备15001146号
  • @1997-2018 潍坊米粒花网络技术有限公司版权所有