多线程(2)

1、线程安全

1.1、线程安全问题与线程同步

多线程编程是有趣且复杂的事情,它常常容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性。即使程序在运行过程中偶尔会出现问题,那也是由于我们的代码有问题导致的。当多个线程访问同一个数据时,非常容易出现线程安全问题。

1.2、线程安全问题

关于线程安全问题,有一个经典的问题:卖票问题。卖票的基本流程很简单:看是否还有票,如果有就可以卖。

现在开启多个窗口同时卖票:

  package com.bdit.part03;
   
  import java.util.ArrayList;
   
  public class
  TicketService {
      private ArrayList<String> list;
      public TicketService(){
           list = new ArrayList<String>();
           list.add("01车01A");
           list.add("01车01B");
           list.add("01车01C");
           list.add("01车01D");
           list.add("01车01E");
           list.add("01车02A");
           list.add("01车02B");
           list.add("01车02C");
           list.add("01车02D");
           list.add("01车02E");
      }
 public boolean hasTicket(){
           return list.size()>0;
      }
      
      public String buy(){
           try {
               return list.remove(0);
           } catch (IndexOutOfBoundsException e) {
  		throw new RuntimeException("票卖超了");
           }
      }
  }

现在有两个窗口同时卖票。

  package com.bdit.part03;
   
  public class
  Saler extends Thread{
      private TicketService ts;
   
      public Saler(TicketService ts) {
           super();
           this.ts = ts;
      }
  public void run(){
           while(ts.hasTicket()){
               try {
                    Thread.sleep(100);
  //这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
               try {
                    String buy = ts.buy();
                    System.out.println("购买票:" + buy);
               } catch (Exception e) {
                    System.err.println(e.getMessage());
               }
           }
           System.out.println("没有票了");
      }
  }
  package com.bdit.part03;
  
   public class TestSaler{
      public static void main(String[] args) {
           TicketService ts = new TicketService();
           Saler s1 = new Saler(ts);
           Saler s2 = new Saler(ts);
           
           s1.start();
           s2.start();
      }
  }

运行结果:
在这里插入图片描述

这里就发生了线程安全问题,当多个线程多条语句(size()和remove())访问共享数据(这里TicketService的list存票的集合)时,就会发生线程安全问题。

解释1:“票卖超了”如何发生的,不是先判断是否有票,才买的吗?
在这里插入图片描述

解释2:重票01车02D又是怎么回事?

在这里插入图片描述

同理重票01车01A和01车01B的重票也是类似的情况,只不过发生线程切换的代码位置在System.arraycopy()方法中,线程1把[1]赋值给[0]后发生切换。这里就不再累述。

2、同步代码块

【线程同步也就是线程安全】

【多线程只要不牵扯操作成员变量的问题,就不会引发线程安全问题】

如何解决线程安全问题呢?

解决思路:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
在这里插入图片描述

为了解决这个问题,Java的多线程支持引入同步监视器来解决这个问题。使用同步监视器的方式有两种:同步代码块和同步方法。

同步代码块的语法格式如下:

synchronized(同步监视器对象){
	//.....
}

上面代码的含义,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得对同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器对象的锁定。

任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。

Java程序运行使用任何对象来作为同步监视器对象,只要保证共享资源的这几个线程,锁的是同一个同步监视器对象即可。

1.选择共享资源对象作为同步监视器对象
  package com.bdit.part03;
   
  public class
  Saler extends Thread{
      private TicketService ts;
   
      public Saler(TicketService ts) {
           super();
           this.ts = ts;
      }
      public void run(){
           while(true){
               synchronized (ts) {
                    if(ts.hasTicket()){
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        try {
                            String buy = ts.buy();
                            System.out.println("购买票:" + buy);
                        } catch (Exception e) {
                            System.out.println(e.getMessage());
                        }
                    }else{
                        System.out.println("没有票了");
                        break;
                    }
               }
           }
      }
  }
2.选择this对象作为同步监视器对象

如果线程是继承Thread类实现的,那么把同步监视器对象换成this,那么就没有起到作用,仍然会发生线程安全问题。因为两个线程的this对象是不同的。

但是如果线程是实现Runnable接口实现的,那么如果两个线程共用同一个Runnable接口实现类对象作为target的话,就可以把同步监视器对象换成this。

package com.bdit.part03;

   public class Window implements Runnable{
      private TicketService ts;
   
      public Window(TicketService ts) {
           super();
           this.ts = ts;
      }
      public void run(){
           while(true){
               synchronized (this) {
                    if(ts.hasTicket()){
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        try {
  			   String buy = ts.buy();
                            System.out.println("购买票:" + buy);
                        } catch (Exception e) {
                            System.out.println(e.getMessage());
                        }
                    }else{
                        System.out.println("没有票了");
                        break;
                    }
               }
           }
      }
  }
package com.bdit.part03;
   
public class TestWindow {
      public static void main(String[] args) {
           TicketService ts = new TicketService();
           Window w = new Window(ts);
           Thread t1 = new Thread(w);
           Thread t2 = new Thread(w);
           
           t1.start();
           t2.start();
      }
  }
3.继承Thread类和实现Runnable接口两种方式的区别

通过继承Thread类或实现Runnable接口都可以实现多线程,但是两种方式存在一定的差别:

采用继承Thread的方式:
优势:

(1)直接线程对象启动,比较简单。

(2)如果在线程体中要访问当前线程,直接this即可。

劣势:

(1)Java有单继承线程,继承了Thread类就不能再继承其他类了。

(2)如果多个线程要共享数据,就比较麻烦,使用static的变量共享,范围又太大。

采用Runnable的方式:

优势:

(1)避免单继承;

(2)可以多个线程共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。

劣势:

(1)启动线程需要再new Thread的对象;

(2)在线程体中要访问当前对象,需要用Thread.currentThread()先获取当前线程对象。

3、同步方法

与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显式指定同步监视器,静态方法的同步监视器对象是当前类的Class对象,非静态方法的同步监视器对象是调用当前方法的this对象。

package com.bdit.part03;
   
  import java.util.ArrayList;
   
  public class TicketService {
      private ArrayList<String> list;
      public TicketService(){
           list = new ArrayList<String>();
           list.add("01车01A");
           list.add("01车01B");
           list.add("01车01C");
           list.add("01车01D");
           list.add("01车01E");
           list.add("01车02A");
           list.add("01车02B");
           list.add("01车02C");
           list.add("01车02D");
           list.add("01车02E");
      }
  public synchronized boolean hasTicket(){
           return list.size()>0;
      }
      
      public synchronized String buy(){
           try {
               return list.remove(0);
           } catch (IndexOutOfBoundsException e) {
 		throw new RuntimeException("票卖超了");
           }
      }
  }
package com.bdit.part03;
   
  public class Saler extends Thread{
      private TicketService ts;
   
      public Saler(TicketService ts) {
           super();
           this.ts = ts;
      }
   public void run(){
           while(ts.hasTicket()){
               try {
                    Thread.sleep(100);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
               try {
                    String buy = ts.buy();
                    System.out.println("购买票:" + buy);
               } catch (Exception e) {
                    System.err.println(e.getMessage());
               }
           }
           System.out.println("没有票了");
      }
  }
package com.bdit.part03;
   
 public class TestSaler {
      public static void main(String[] args) {
           TicketService ts = new TicketService();
           Saler s1 = new Saler(ts);
           Saler s2 = new Saler(ts);
           
           s1.start();
           s2.start();
      }
  }

不要对线程安全类的所有方法都加同步,只对那些会影响竞争资源(即共享资源)的方法进行同步即可。而且也要注意同步方法的默认同步的监视器对象对于多个线程来说是否是同一个。

4、释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?

1、释放锁的操作

● 当前线程的同步方法、同步代码块执行结束。

● 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。

● 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。

●当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

2、不会释放锁的操作

●线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。

●线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。

● 应尽量避免使用suspend()和resume()这样的过时来控制线程

5、死锁

不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

【如何避免线程出现死锁?加大锁的粒度】

package com.bdit.part03;
   
public class TestDeadLock {
      public static void main(String[] args) {
           Object g = new Object();
           Object m = new Object();
           Owner s = new Owner(g,m);
           Customer c = new Customer(g,m);
           new Thread(s).start();
           new Thread(c).start();
      }
  }
  class Owner implements Runnable{
      private Object goods;
      private Object money;
   
      public Owner(Object goods, Object money) {
           super();
           this.goods = goods;
           this.money = money;
      }
   
      @Override
      public void run() {
           synchronized (goods) {
               System.out.println("先给钱");
               synchronized (money) {
                    System.out.println("发货");
               }
           }
      }
  }
  class Customer implements Runnable{
      private Object goods;
      private Object money;
   
      public Customer(Object goods, Object money) {
           super();
           this.goods = goods;
           this.money = money;
      }
   
      @Override
      public void run() {
           synchronized (money) {
               System.out.println("先发货");
               synchronized (goods) {
                    System.out.println("再给钱");
               }
           }
      }
  }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值