Java知识(五)

Author Avatar
子语 2018 - 01 - 20
  • 在其它设备中阅读本文章

线程的同步与死锁

同步问题引出

1、同步是多个线程访问同一资源时必须要考虑到的问题。

范例:观察非同步下的操作

import java.util.concurrent.*;

class SellTicket implements Runnable {
    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (this.ticket > 0) {
                System.out.println(Thread.currentThread().getName() +
                        "买票,ticket = " + this.ticket--);
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        // 创建一个4线程的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        SellTicket sellTicket = new SellTicket();
        // 启动线程
        executorService.execute(sellTicket);
        executorService.execute(sellTicket);
        executorService.execute(sellTicket);
        executorService.execute(sellTicket);
        executorService.shutdown();
    }
}

上述代码中,4个线程一共只能卖5张票,此时运行程序未发现问题,是因为程序是在一个JVM进程下运行,且没有受到其他影响。

2、为了发现问题,此时在线程中加入延迟,如下面代码:

class SellTicket implements Runnable {
  private int ticket = 5;
  
  @Override
  public void run() {
      for (int i = 0; i < 20; i++) {
          if (this.ticket > 0) {
              // 加入线程等待,时间为0.1秒
              try {
                  Thread.sleep(100);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() +
                      "买票,ticket = " + this.ticket--);
          }
      }
  }
}

此时结果中,出现票数为负数。卖票分为两步:
(1) 判断票数是否为负数;
(2) 减少票数。
假设当前票数为1,当一个线程进来,发现还有票,便要进行第二步。但此时该线程休眠,没有修改票数;结果第二个进程进入时,因为票数还未被修改,也进入第二步,于是两个进程休眠后,都对票进行了修改,于是便出现了负数。由于程序未进行同步,导致了数据出错。

同步处理

1、上述程序存在的问题是:票数判断和票数修改是分开完成的,正确的思路应该是将票数判断和票数修改包装起来,当一个线程进行操作时,其他线程要等待。 同步指的是在同一时间点只能有一个线程进行,其他线程要等待该线程完成后才可以继续操作。

2、在Java中使用synchronized关键字实现线程的同步,该关键字使用形式有两种:
(1) 同步代码块;
(2) 同步方法。
范例:同步块

class SellTicket implements Runnable {
  private int ticket = 5;

  @Override
  public void run() {
      for (int i = 0; i < 20; i++) {
          // 同步块
          synchronized (this){ // 当前操作只允许一个对象进入
              if (this.ticket > 0) {
              // 加入线程等待,时间为0.1秒
                 try {
                    Thread.sleep(100);
                 } catch (InterruptedException e) {
                    e.printStackTrace();
                 }
                 System.out.println(Thread.currentThread().getName() +
                        "买票,ticket = " + this.ticket--);
              }
          }
      }
  }
}

范例:同步方法

class SellTicket implements Runnable {
  private int ticket = 5;
  
  @Override
  public void run() {
      for (int i = 0; i < 20; i++) {
          this.sale();
      }
  }

  // 同步方法
  public synchronized void sale() {
      if (this.ticket > 0) {
          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() +
                  "买票,ticket = " + this.ticket--);
      }
  }
}

异步操作的执行速度高于同步操作,但是同步操作时,数据安全性较高,属于安全的线程操作。

死锁

通过上述例子,可知同步指的是一个线程对象需要等待其前一个线程对象执行完毕后才能执行。当线程同步时,可能出现以下问题:
范例:观察死锁

public class Demo implements Runnable{
  public String username;
  public Object lock1 = new Object();
  public Object lock2 = new Object();

  @Override
  public void run() {
      if ("a".equals(username)) {
          synchronized (lock1) {
              try {
                  System.out.println("username = " + username);
                  Thread.sleep(3000);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              synchronized (lock2) {
                  System.out.println("按lock1-->lock2顺序执行");
              }
          }
      }
      if ("b".equals(username)) {
          synchronized (lock2) {
              try {
                  System.out.println("username = " + username);
                  Thread.sleep(3000);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              synchronized (lock1) {
                  System.out.println("按lock2-->lock1顺序执行");
              }
          }
      }
  }

  public void setFlag(String username) {
      this.username = username;
  }

  public static void main(String[] args) {
      Demo dt1 = new Demo();
      dt1.setFlag("a");;
      Thread t1 = new Thread(dt1);
      t1.start();
    
      try {
          Thread.sleep(2000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
    
      dt1.setFlag("b");;
      Thread t2 = new Thread(dt1);
      t2.start();
  }
}

上述代码的结果是username = a username = b。死锁是程序开发时由于某种逻辑错误造成的,不是简单地就能发现的。我们使用JDK自带的工具Jconsole观察死锁。在CMD中输入jconsole启动该工具,连接到需要查看的线程:

无法加载

点击线程检查死锁:

无法加载

无法加载

此时我们可以看到Thread-1想申请java.lang.Object@7234b7be,但是这个被Thread-0拥有了,所以堵塞了;
Thread-0想申请 java.lang.Object@3a6ff1f5,但是这个被Thread-1拥有了,所以堵塞了。两者互相等待,一直僵持下去,就造成了死锁。

小结:

请解释多个线程访问同一资源时需要考虑到哪些问题?有可能带来哪些问题?

答案:· 多个线程访问同一资源时一定要处理好同步,可以使用同步代码块或同步方法来解决;

​ |- 同步代码块:synchronized(锁定对象){代码}

​ |- 同步方法:public synchronized 返回值 方法名(){代码}

· 但是过多地使用同步,有可能造成死锁。

生产者与消费者

综合实战:问题引出

生产者消费者是两个不同的线程类对象,它们操作同一资源。具体的操作流程如下:
(1) 生产者负责生产数据,消费者负责取走数据;
(2) 生产者每生产完一组数据,消费者就要取走一组数据;
现在假设要生产的数据如下:
(1) 第一组数据:title = 书,content = “Java开发”
(2) 第二组数据:title = 笔,content = “派克钢笔”
操作的结构图如下:

无法加载

范例:程序基本模型

// 数据类
public class Info {
    private String title;
    private String content;
    
    public String getTitle() {
        return title;
    }
    
    public void setTitle(String title) {
        this.title = title;
    }
    
    public String getContent() {
        return content;
    }
    
    public void setContent(String content) {
        this.content = content;
    }
}

// 生产者
public class Productor implements Runnable{
    private  Info info;

    public Productor(Info info) {
        this.info = info;
    }
    
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            if (x % 2 == 0) {
                this.info.setTitle("书");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.info.setContent("Java开发");
            } else {
                this.info.setTitle("笔");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.info.setContent("派克钢笔");
            }
        }
    }
}

// 消费者
public class Customer implements Runnable {
    private Info info;

    public Customer(Info info) {
        this.info = info;
    }
    
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.info.getTitle() + "-" +
            this.info.getContent());
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        Info info = new Info();
 
        new Thread(new Productor(info)).start();
        new Thread(new Customer(info)).start();
    }
}

运行程序,发现两个问题:
· 数据错位,不是我们所需要的完整数据(书-派克钢笔、笔-Java开发);
· 数据重复取出,数据重复设置。

综合实战:同步处理

上述程序出现错误,是异步操作造成的,因此应进行同步处理。因为取和设置是不同功能,要进行同步控制,就需要将其定义在一个类中。

// 数据类
public class Info {
    private String title;
    private String content;
    
    public synchronized void set(String title, String content) {
        this.title = title;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.content = content;
    }
    
    public synchronized void get() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.title + "-" + this.content);
    }
}

// 生产者
public class Productor implements Runnable{
    private  Info info;

    public Productor(Info info) {
        this.info = info;
    }
    
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            if (x % 2 == 0) {
                this.info.set("书", "Java开发");
            } else {
                this.info.set("笔", "派克钢笔");
            }
        }
    }
}

// 消费者
public class Customer implements Runnable {
    private Info info;

    public Customer(Info info) {
        this.info = info;
    }
    
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            this.info.get();
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        Info info = new Info();
        new Thread(new Productor(info)).start();
        new Thread(new Customer(info)).start();
    }
}

上述代码解决了数据错位问题,但是重复操作问题更加严重了。

Object类

如果要想完成上述需要,必须加入等待机制与唤醒机制。Object类中提供有方法:
· 等待:public final void wait() throws InterruptedException;
· 唤醒第一个等待线程:public final void notify();
· 唤醒全部等待线程,优先级高的先执行:public final void notifyAll()。
范例:修改数据类

// 数据类
public class Info {
    private String title;
    private String content;
    private boolean flag = true;
    // flag=true时,只能生产,不能取
    // flag=false时,只能取,不能生产
    
    public synchronized void set(String title, String content) {
        if (!this.flag) {
            try {
                super.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.title = title;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.content = content;
        this.flag = false; // 修改线程标记
        super.notify(); // 唤醒其他线程
    }
    
    public synchronized void get() {
        if (this.flag) {
            try {
                super.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.title + "-" + this.content);
        this.flag = true;
        super.notify();
    }
}

小结: sleep()与wait()的区别
答案:· sleep()是Thread类方法,wait()是Object类方法;
​ · sleep()可以设置休眠时间,时间一到自动结束休眠;wait()必须等待notify()唤醒。

This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接:http://yov.oschina.io/article/Java/Java/Java知识(五)/