前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第10次文章:深入线程

第10次文章:深入线程

作者头像
鹏-程-万-里
发布2019-09-27 12:20:28
3130
发布2019-09-27 12:20:28
举报
文章被收录于专栏:Java小白成长之路

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

一、同步

当多个线程访问同一个资源时,由于每个线程访问同一份资源的时候,会有时间差。所以很有可能多个线程同时进入同一份资源,然后使得资源的自身信息没有及时得到更新,造成错误输出的情况出现,这就是所谓的线程不安全。为了确保资源的安全,也就是确保线程安全,我们使用关键字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 方法的签名{

方法体

}

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

五、生产者消费者模式

六、任务调度


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-03-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java小白成长之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档