第10次文章:深入线程

2019-09-27 12:20:28 浏览数 (1)

时间真的是快,经不起浪费啊!加油!

一、同步

当多个线程访问同一个资源时,由于每个线程访问同一份资源的时候,会有时间差。所以很有可能多个线程同时进入同一份资源,然后使得资源的自身信息没有及时得到更新,造成错误输出的情况出现,这就是所谓的线程不安全。为了确保资源的安全,也就是确保线程安全,我们使用关键字synchronized,对需要确保安全的代码进行同步处理。

使用synchronized的基本原理是:当已经有线程进入资源时,此时计算机会给当前资源一把锁,锁住当前资源,其他的线程只能在外部进行等待,线程被阻塞挂起。当访问该资源的线程结束访问的时候,系统会将该锁释放,整个程序进入运行状态,这样就避免了多个进程同时访问同一份资源的问题。

有两种方法可确保线程的同步:

方法1、同步方法:

synchronized

方法2、同步块:

synchronized(引用类型 | this | 类.class){

}

代码语言:javascript复制
public class SynDemo01 {
  public static void main(String[] args) {
    //新建实体对象
    Web12306 web = new Web12306();
    //创建代理
    Thread t1 = new Thread(web,"黄牛1");
    Thread t2 = new Thread(web,"黄牛2");
    Thread t3 = new Thread(web,"黄牛3");
    //启动线程
    t1.start();
    t2.start();
    t3.start();
  }
}
class Web12306 implements Runnable{
  private int num = 10;
  private boolean flag = true;  
  @Override
  public void run() {
    while(flag) {
      test3();
    }
  }
  //线程不安全
  private void test1() {
    if (0 >= num) {
      this.flag = false;//跳出循环
      return;
    }
    try {
      Thread.sleep(100);//模拟网络延时
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() "抢到了第" num-- "张票");
  }
  //线程安全
  private synchronized void test2() {
    if (0 >= num) {
      this.flag = false;//跳出循环
      return;
    }
    try {
      Thread.sleep(500);//模拟网络延时
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() "抢到了第" num-- "张票");
  }
  //线程安全  资源锁定正确
  private  void test3() {
    synchronized(this) {
      if (0 >= num) {
        this.flag = false;//跳出循环
        return;
      }
      try {
        Thread.sleep(500);//模拟网络延时
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() "抢到了第" num-- "张票");
    }
  }
}

解析:test1方法中,没有加入synchronized关键字进行同步。使用此方法的时候,会导致线程"黄牛1","黄牛2","黄牛3"同时进入引用类对象"Web12306"当中访问剩余的票数,所以输出的结果会有:“黄牛1抢到了第-1张票”,这种明显错误的结果,就是因为未同步而造成的。

test2方法中,使用的是利用synchronized关键字锁定整个方法,也就是我们上面介绍的方法1:同步方法。将整个方法进行同步处理。但是正如我们所讲述的原理一样,同步方法的关键就在于阻塞线程,所以阻塞的内容越多,整体的运行速度会明显下降。最终造成低效率的结果。

test3方法中,使用的是我们介绍的方法2:同步块。在此方法中我们可以根据自己的分析,判断哪一个地方最有可能出现安全隐患,然后加入同步块,这样就可以适当的减少相应的阻塞内容,在一定的程度上提高代码运行效率。

二、死锁

在我们使用多个同步的时候,假如我们的多线程访问的资源相互同步,然后每个线程都不释放自己的锁,那么就很容易造成死锁的情况。此时,所有的线程都会被挂起,然后相互等待,一直到系统奔溃。所以过多的同步容易造成死锁。

解决死锁的一种方式:生产者与消费者模式

当生产者进行生产操作的时候,消费者被挂起,停止消费;当消费者在消费的时候,生产者被挂起,消费者进行消费。可以使用一种信号灯法进行操作。

信号灯法:

1、wait():等待,释放锁

2、notify()/notifyAll():唤醒

与synchronized一起使用

第一步:我们创建一个电影院场景,其中包含有play(生产者)和watch(消费者)

代码语言:javascript复制
public class Movie {
  private String pic;
  //信号灯
  //flag---->T 生产者生产,消费者等待,生产完成后通知消费
  //flag---->F 消费者消费,生产者等待,消费完成后通知生产
  private boolean flag = true;  
  /**
   * 播放,相当于生产者
   * @param pic
   */
  public synchronized void play(String pic) {
    if(!flag) {//即:生产者等待
      try {
        this.wait();
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
    //生产者生产
    try {
      Thread.sleep(500);//模拟生产了500毫秒
    } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    //生产结束
    this.pic = pic;
    System.out.println("生产了:" pic);
    //生产者停下
    this.flag = false;
    //通知消费者
    this.notifyAll();
  }
  /**
   * 观看,相当于消费者
   */
  public synchronized void watch() {
    if(flag) {//生产者在生产,消费者等待
      try {
        this.wait();
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
    //消费者消费
    try {
      Thread.sleep(200);//假设消费200毫秒就停下
    } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    //消费结束
    System.out.println("消费了:" pic);
    //消费者停下
    this.flag = true;
    //通知生产者生产
    this.notifyAll();

  }
}

第二步:我们创建相应的生产者player和消费者watcher,对同一份资源movie进行访问。

代码语言:javascript复制
public class Player implements Runnable {
  private Movie m ;
  public Player(Movie m) {
    super();
    this.m = m;
  }
  @Override
  public void run() {
    for(int i = 0; i < 20 ;i  ) {
      if(0 == i%2) {
        m.play("左青龙" i);
      }else {
        m.play("右白虎" i);
      }
    }    
  }
}
代码语言:javascript复制
public class Watcher implements Runnable {
  private Movie m ;
  public Watcher(Movie m) {
    super();
    this.m = m;
  }
  @Override
  public void run() {
    for(int i = 0; i < 20 ;i  ) {
      m.watch();
    }  
  }
}

第三步:对生产者和消费者进行应用

代码语言:javascript复制
public class App {
  public static void main(String[] args) {
    Movie m = new Movie();
    //共享资源
    Player p = new Player(m);
    Watcher w = new Watcher(m);
    new Thread(p).start();
    new Thread(w).start();    
  }
}

第四步:查看一下运行结果

代码语言:javascript复制
生产了:左青龙0
消费了:左青龙0
生产了:右白虎1
消费了:右白虎1
生产了:左青龙2
消费了:左青龙2
生产了:右白虎3
消费了:右白虎3
生产了:左青龙4
消费了:左青龙4
生产了:右白虎5
消费了:右白虎5
生产了:左青龙6
消费了:左青龙6
生产了:右白虎7
消费了:右白虎7
生产了:左青龙8
消费了:左青龙8
生产了:右白虎9
消费了:右白虎9
生产了:左青龙10
消费了:左青龙10
生产了:右白虎11
消费了:右白虎11
生产了:左青龙12
消费了:左青龙12
生产了:右白虎13
消费了:右白虎13
生产了:左青龙14
消费了:左青龙14
生产了:右白虎15
消费了:右白虎15
生产了:左青龙16
消费了:左青龙16
生产了:右白虎17
消费了:右白虎17
生产了:左青龙18
消费了:左青龙18
生产了:右白虎19
消费了:右白虎19

解析:对最终的结果,可以明显看出所有线程都是一种规律性的出现,不会是随机出现的结果。在线程等待的时候需要注意一点:wait是将线程进行阻塞挂起,并且释放锁。而sleep方法,仅仅是将线程挂起,不释放锁。所以当我们使用sleep的时候,将会使得整个线程阻塞相应的时间后,再重新开始运行。与此同时,其他线程的状态并不会有所改变。

三、任务调度

了解一个类:Timer()

主要用于任务在不同时间的执行情况,具体使用如下所示:

代码语言:javascript复制
public class TimeDemo01 {
  public static void main(String[] args) {
    Timer time = new Timer();    
    //语法:schedule(TimerTask task, Date firstTime, long period)
    time.schedule(new TimerTask() {//使用匿名内部类
      @Override
      public void run() {
        System.out.println("所谓的线程,也就是换一种类,然后运行run里面的代码");
        
      }},new  Date(System.currentTimeMillis() 2000),//当前时间过后两秒开始运行
        2000);//每间隔2秒运行1次
  }
}

Timer类主要是使用schedule方法,该方法主要的几条语句如下所示:

仅将线程运行一次:

schedule(TimerTask task, Date time)

schedule(TimerTask task, long delay)

间隔period时间后,再运行的语句:

schedule(TimerTask task, long delay, long period)

schedule(TimerTask task, Date firstTime, long period)

注意:在使用schedule的时候,我们涉及到了TimerTask类别,这个类别实现了Runnable接口,所以在创建该类别的时候,就可以将其当做一个实现了Runable接口的类来处理,直接新建之后,重写它的Run()方法就好了。

结合上一篇文章,我们对线程进行总结,同时结束线程的学习,进入下一个内容:

一、创建线程 重点

1、继承 Thread

2、实现 Runnable

3、实现 Callable (了解即可)

二、线程的状态

1、新生--->start--->就绪---->运行--->阻塞--->终止

2、终止线程(重点)

3、阻塞:join yield sleep(不是释放锁)

三、线程的信息

1、Therad.currentThread

2、获取名称 设置名称 设置优先级 判断状态

四、同步:多线程使用同一份资源

synchronized (引用类型变量|this|类.class){

}

修饰符 synchronized 方法的签名{

方法体

}

过多的同步可能造成死锁。

五、生产者消费者模式

六、任务调度


0 人点赞