第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 方法的签名{
方法体
}
过多的同步可能造成死锁。
五、生产者消费者模式
六、任务调度