第10次文章:深入线程

  • 2019 年 10 月 8 日
  • 筆記

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

一、同步

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

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

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

方法1、同步方法:

synchronized

方法2、同步块:

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

}

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(消费者)

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进行访问。

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);        }      }    }  }
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();      }    }  }

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

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();    }  }

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

生产了:左青龙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()

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

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 方法的签名{

方法体

}

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

五、生产者消费者模式

六、任务调度