菜鸟学习笔记:Java提升篇8(线程2——线程的基本信息、线程安全、死锁、生产者消费者模式、任务调度)
线程的基本信息线程同步线程安全死锁生产者消费者模式任务调度(了解)线程的基本信息
Java中为我们提供了许多方法可以用来获取线程的信息:
isAlive()判断线程是否还活着,即线程是否还未终止。getPriority获得线程的优先级数值setPriority设置线程的优先级数值setName设置线程的名称getName获取线程的名称currentThread获取当前正在运行的线程
对这些方法的演示如下:
public class MyThread implements Runnable {private boolean flag =true;private int num =0;@Overridepublic void run() {while(flag){System.out.println(Thread.currentThread().getName()+"-->"+num++);}}public void stop(){this.flag=!this.flag;}}
public static void main(String[] args) throws InterruptedException {MyThread it =new MyThread();Thread proxy =new Thread(it,"挨踢");//设置线程名proxy.setName("test");//获取线程名System.out.println(proxy.getName());//获取当前正在运行的线程名System.out.println(Thread.currentThread().getName()); //mainproxy.start();System.out.println("启动后的状态:"+proxy.isAlive());Thread.sleep(200);it.stop();Thread.sleep(100);System.out.println("停止后的状态:"+proxy.isAlive());}
这里说明一下优先级代表线程运行的概率,不是绝对的先后顺序,Thread类中定义了三个静态常两来表示优先级。
MAX_PRIORITY 10
NORM_PRIORITY 5 (默认)
MIN_PRIORITY 1
程序优先级方法演示:
public static void main(String[] args) throws InterruptedException {MyThread it =new MyThread();Thread p1 =new Thread(it,"挨踢1");MyThread it2 =new MyThread();Thread p2 =new Thread(it2,"挨踢2");p1.setPriority(Thread.MIN_PRIORITY); //设置优先级p2.setPriority(Thread.MAX_PRIORITY);//设置优先级p1.start();p2.start();Thread.sleep(100);it.stop();it2.stop();}
程序结果中p2运行次数要大于p1运行次数,大家可以拷贝代码自己尝试。
线程同步
线程安全
之前讲解中经常会提到线程安全这一概念,所谓线程安全说白了就是保证多个线程访问同一份资源时不会造成错误的结果,还是我们之前的12306例子:
public static void main(String[] args) {//真实角色Web12306 web = new Web12306();//代理Thread t1 =new Thread(web,"路人甲");Thread t2 =new Thread(web,"黄牛已");Thread t3 =new Thread(web,"攻城师");//启动线程t1.start();t2.start();t3.start();}
我们对代码做出一定的修改:
class Web12306 implements Runnable {private int num =10;private boolean flag =true;@Overridepublic void run() {while(flag){test1();}}public void test1(){if(num<=0){flag=false; //跳出循环return ;}try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"抢到了"+num--);}}
这时程序会打印:
为什么会出现0或者负数的情况那?我们分析test1,,500ms的时间足够三个线程完成一次执行,所以会出现10、9、8,7、6、5,4、3、2,同时出现。我们从num为1时开始分析,路人甲执行到Thread.sleep(500)时线程阻塞运行,判断num<=0成立,该线程进入休眠。在休眠过程中黄牛已线程运行num还是为1,黄牛已也进入休眠,攻城师也是一个道理。当路人甲休眠结束,执行抢票程序,打印1,num变为0。马上黄牛已休眠结束,由于已经执行完判断语句,所以会直接执行抢票程序,打印0,num变为-1,攻城师休眠结束后也是直接执行抢票程序,打印-1,num变为-2。这时路人甲再次进入程序进行判断,发现判断条件不成立结束程序。
以上情况就是多线程访问带来的错误,我们需要使用线程同步策略防止这个现象。
线程同步最简单的方法就是加上synchronized修饰符,它的作用相当于给方法加了一把锁,使得方法同时只允许一个线程进行访问。
加上之后的实例代码如下:
public synchronized void test2(){if(num<=0){flag=false; //跳出循环return ;}try {Thread.sleep(500); //模拟 延时} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"抢到了"+num--);}
执行之后之前异常的情况消失,但是由于单线程访问的问题使得每次执行都必须等待500ms,执行效率大大降低。
除了用synchronized方法以外还可以用同步块的思想去实现这个功能:
public void test3(){//this代表具体对象,也就是web。synchronized(this){if(num<=0){flag=false; //跳出循环return ;}try {Thread.sleep(500); //模拟 延时} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"抢到了"+num--);}}
死锁
同步虽然可以解决许多资源共享的问题,但它不仅会降低程序执行的效率,还会带来一些其他问题,死锁就是其中之一。
我们直接通过一段Java代码来解释死锁:
class Test implements Runnable{Object goods ;Object money ;public Test(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {while(true){test();}}public void test(){synchronized(goods){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized(money){}}System.out.println("一手给钱");}}
上述类中test方法中先对goods进行了锁定,然后在100ms延迟后申请money资源。
class Test2 implements Runnable{Object goods ;Object money ;public Test2(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {while(true){test();}}public void test(){synchronized(money){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized(goods){}}System.out.println("一手给货");}}
上述类中test方法中先对money进行了锁定,然后在100ms延迟后申请goods资源。
我们在主函数中这样进行调用:
public static void main(String[] args) {Object g =new Object();Object m = new Object();Test t1 =new Test(g,m);Test2 t2 = new Test2(g,m);Thread proxy = new Thread(t1);Thread proxy2 = new Thread(t2);proxy.start();proxy2.start();}
执行后发现程序卡死了,这就是死锁现象。为什么会这样那?首先proxy.start()和proxy2.start()会启动两个线程,而g和m两个资源被两个线程共享,t1线程执行Test中的run方法,run方法中循环调用test方法,执行时会对g进行锁定,并延迟100ms,在这100ms中,t2线程也执行到Test2的test方法中,这里会对m进行锁定,并延迟100ms。当t1线程等待满100ms会在锁定money的基础上申请goods资源,此时goods资源已被t2所占有,所以会继续等待,而t2线程等待100ms后会在占有goods的基础上去申请money资源,此时money资源已被t1所占有,这样双方都需要另一方所占有的资源才能释放自己所占有的资源,进而陷入了无线等待中。从而造成了死锁现象。
生产者消费者模式
为了解决死锁问题我们引入了生产者和消费者模式。
在介绍之前先讲解两个方法。
Object.wait方法:在其他线程调用此对象的notify()或者notifyAll()方法前,当前线程等待,等待时释放所占有资源(这一点与sleep方法不同)。
Object.notify方法:唤醒此对象上等待的单个线程。
有了这个基础我们用代码来演示生产者消费者问题:
首先构建一个电影类(产品):
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) {e.printStackTrace();}}//开始生产try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("生产了:"+pic);//生产完毕this.pic =pic;//通知消费this.notify();//生产者停下this.flag =false;}public synchronized void watch(){if(flag){//消费者等待try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//开始消费try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("消费了"+pic);//消费完毕//通知生产this.notify();//消费停止this.flag=true;}}
然后构建生产者:
public class Player implements Runnable {private Movie m ;public Player(Movie m) {super();this.m = m;}@Overridepublic void run() {for(int i=0;i<20;i++){if(0==i%2){m.play("左青龙");}else{m.play("右白虎");}}}}
在构建消费者:
public class Watcher implements Runnable {private Movie m ;public Watcher(Movie m) {super();this.m = m;}@Overridepublic void run() {for(int i=0;i<20;i++){m.watch();}}}
在主函数中进行启用:
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();}
执行代码发现对象的生产和消费总是对应存在的:
这是什么原理那?我们可以看到watch类中通过一个flag位来标记是否以生产,所以我们假设当flag位为false时消费者先进入watch对象的watch()方法(这时还没有生产产品pro),此时flag位为false,所以消费者线程会执行wait方法等待。等待过程中生产者进入watch对象的play方法,生产了一个pro,然后执行notify方法,这时消费者方法被唤醒,对pro进行消费。当flag为true时生产者先进入也是同样的道理,这样就保证了pro这个资源只能在生产完成后才可以消费。避免了死锁的问题。
任务调度(了解)
任务调度的意思就是对一个方法在指定时间或者间隔多长时间对其进行调用。Java中一般通过Timer类来实现,该类使用非常简单,直接通过代码演示。
public static void main(String[] args) {Timer timer =new Timer();timer.schedule(new TimerTask(){@Overridepublic void run() {System.out.println("so easy....");}}, new Date(System.currentTimeMillis()+1000), 200);//从当前时间开始1000ms后执行run方法,每间隔2000ms执行一次}
以上就是Java多线程的全部内容。
上一篇:菜鸟学习笔记:Java提升篇7(线程1——进程、程序、线程的区别,Java多线程实现,线程的状态))
下一篇:菜鸟学习笔记:Java提升篇9(网络1——网络基础、Java网络编程)
菜鸟学习笔记:Java提升篇8(线程2——线程的基本信息 线程安全 死锁 生产者消费者模式 任务调度)