Java多线程(2)
3.4、线程的同步与死锁(理解)
3.4.1 、同步问题
所谓的同步问题指的是多个线程操作同一资源时所带来的信息的安全性问题,例如,下面模拟一个简单的卖票程序,要求有5个线程,卖6张票。
package cn.mldn.demo; class MyThread implements Runnable { // 线程的主体类 private int ticket = 6; @Override public void run() { // 线程的主方法 for (int x = 0; x < 10; x++) { if (this.ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--); } } } } public class TestDemo { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); new Thread(mt, "票贩子D").start(); new Thread(mt, "票贩子E").start(); } } |
这个时候发现操作的数据之中出现了负数,这个就可以理解为不同步问题。
如果说现在只剩下最后一张票了,一个线程判断条件满足,但是再它还没有修改票数之后,其他线程也同时通过了if判断,所以最终修改票数的时候就变成了负数。
如果现在要想增加这个锁,在程序之中就可以通过两种方式完成:一种是同步代码块,另外一种就是同步方法。
实现一:同步代码块,使用synchronized关键字定义的代码块就称为同步代码块,但是在进行同步的操作之中必须设置一个要同步的对象,而这个对象应该理解为当前对象:this。
package cn.mldn.demo; class MyThread implements Runnable { // 线程的主体类 private int ticket = 6; @Override public void run() { // 线程的主方法 for (int x = 0; x < 10; x++) { synchronized (this) { // 同步代码块 if (this.ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--); } } } } } public class TestDemo { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); new Thread(mt, "票贩子D").start(); new Thread(mt, "票贩子E").start(); } } |
方式二:同步方法
package cn.mldn.demo; class MyThread implements Runnable { // 线程的主体类 private int ticket = 6; @Override public void run() { // 线程的主方法 for (int x = 0; x < 10; x++) { this.sale() ; } } public synchronized void sale() { if (this.ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--); } } } public class TestDemo { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); new Thread(mt, "票贩子D").start(); new Thread(mt, "票贩子E").start(); } } |
但是在此处需要说明的一个问题:加入同步之后明显比不加入同步慢许多,所以同步的代码性能会很低,但是数据的安全性会高。
3.4.2 、死锁
同步就是指一个线程要等待另外一个线程执行完毕才会继续执行的一种操作形式,但是如果在一个操作之中都是在互相等着的话,那么就会出现死锁问题。
范例:下面简单的模拟一个死锁程序的样式
package cn.mldn.demo; class YuShi { public synchronized void say(FuXie f) { System.out.println("玉史:给我30亿欧圆,放了你儿子。"); f.get() ; } public synchronized void get() { System.out.println("玉史终于得到了赎金,放了儿子,为了下次继续绑架。"); } } class FuXie { public synchronized void say(YuShi y) { System.out.println("付谢:放了我儿子,我给你30亿欧圆,不见人不给钱。") ; y.get() ; } public synchronized void get() { System.out.println("付谢救回了自己的儿子,于是开始哭那30亿。"); } } public class DeadLock implements Runnable { static YuShi ys = new YuShi() ; static FuXie fx = new FuXie() ; public static void main(String[] args) { new DeadLock() ; } public DeadLock() { new Thread(this).start() ; ys.say(fx) ; } @Override public void run() { fx.say(ys) ; } } |
死锁是在日后多线程程序开发之中经常会遇见的问题,而以上的代码并没有任何的实际意义,大概可以理解死锁的操作形式就可以了,不用去研究程序。
面试题:请问多个线程操作同一资源的时候要考虑到那些,会带来那些问题?
多个线程访问同一资源的时候一定要考虑到同步的问题,但是过多的同步会带来死锁。
3.5、线程间的经典操作案例(理解)
在多线程的开发之中存在一种称为“生产者和消费者的程序”,这个程序的主要功能是生产者负责生产一些内容,每当生产完成之后,会由消费者取走全部内容,那么现在假设要生产的是如下两种数据:
· 数据一:title = 2012.12.21日,content = 世界末日;
· 数据二:title = 付谢,content = 打扫卫生迎接末日。
现在对于这样的程序,可以使用如下的一些基本模型实现。
package cn.mldn.demo; class Message { private String title ; private String content ; public void setTitle(String title) { this.title = title; } public void setContent(String content) { this.content = content; } public String getTitle() { return title; } public String getContent() { return content; } } class Productor implements Runnable { private Message msg = null ; public Productor(Message msg) { this.msg = msg ; } @Override public void run() { for (int x = 0; x < 50; x++) { if (x % 2 == 0) { this.msg.setTitle("2012.12.21") ; try { Thread.sleep(100) ; } catch (InterruptedException e) { e.printStackTrace(); } this.msg.setContent("世界末日") ; } else { this.msg.setTitle("付谢") ; try { Thread.sleep(100) ; } catch (InterruptedException e) { e.printStackTrace(); } this.msg.setContent("打扫卫生迎接末日") ; } } } } class Customer implements Runnable { private Message msg = null ; public Customer(Message msg) { this.msg = msg ; } @Override public void run() { for (int x = 0; x < 50; x++) { try { Thread.sleep(100) ; } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.msg.getTitle() + " --> " + this.msg.getContent()); } } } public class TestDemo { public static void main(String[] args) throws Exception { Message msg = new Message() ; new Thread(new Productor(msg)).start() ; new Thread(new Customer(msg)).start() ; } } |
但是,以上的代码模型出现了如下的两严重问题:
· 数据错位了;
· 出现了重复取出和重复设置的问题。
3.5.1 、解决数据错位问题:依靠同步就可以解决
只要对设置和取得加上同步应用,就可以解决数据的错位的操作问题,下面,对代码进行修改。
package cn.mldn.demo; class Message { private String title ; private String content ; public synchronized void set(String title,String content) { this.title = title ; try { Thread.sleep(200) ; } catch (InterruptedException e) { e.printStackTrace(); } this.content = content ; } public synchronized void get() { try { Thread.sleep(100) ; } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.title + " --> " + this.content); } } class Productor implements Runnable { private Message msg = null ; public Productor(Message msg) { this.msg = msg ; } @Override public void run() { for (int x = 0; x < 50; x++) { if (x % 2 == 0) { this.msg.set("2012.12.21", "世界末日"); } else { this.msg.set("付谢", "打扫卫生迎接末日"); } } } } class Customer implements Runnable { private Message msg = null ; public Customer(Message msg) { this.msg = msg ; } @Override public void run() { for (int x = 0; x < 50; x++) { this.msg.get() ; } } } public class TestDemo { public static void main(String[] args) throws Exception { Message msg = new Message() ; new Thread(new Productor(msg)).start() ; new Thread(new Customer(msg)).start() ; } } |
这个时候的确解决了数据的错位的问题,但同时新的问题又来了:发现数据的重复问题更严重了。
3.5.2 、解决数据的重复设置和重复取出
要想解决重复的问题需要等待及唤醒机制,而这一机制的实现只能依靠Object类完成,在Object类之中定义了以下的三个方法完成线程的操作:
· 等待:public final void wait() throws InterruptedException;
· 唤醒第一个等待线程:public final void notify();
· 唤醒全部等待线程:public final void notifyAll()。
对于唤醒的两个操作:notify()是按照等待顺序进行了唤醒,而使用了notifyAll()则表示所有等待的线程都会被唤醒,那个线程的优先级高,那个线程就先执行。
范例:修改Message类,解决数据的重复设置和重复取出的操作
class Message { private String title ; private String content ; private boolean flag = true ; // flag == true:表示可以生产,但是不能取走 // flag == false:表示可以取走,但是不能生产 public synchronized void set(String title,String content) { if (this.flag == false) { // 已经生产过了,不能生产 try { super.wait() ; } catch (InterruptedException e) { e.printStackTrace(); } } this.title = title ; try { Thread.sleep(200) ; } catch (InterruptedException e) { e.printStackTrace(); } this.content = content ; this.flag = false ; super.notify() ; } public synchronized void get() { if (this.flag == true) { // 不能取走 try { super.wait() ; } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100) ; } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.title + " --> " + this.content); this.flag = true ; // 已经取走了,可以继续生产 super.notify() ; } } |
面试题:请解释sleep()和wait()的区别?
· sleep()是Thread类定义的static方法,表示线程休眠,休眠到一定时间后自动唤醒;
· wait()是Object类定义的方法,表示线程等待,一直到执行了notify()或notifyAll()之后才结束等待。
4、总结
1、 多线程的两种实现方式及区别;
2、 理解同步与死锁的概念;
3、 Object类对多线程支持的三个方法。
5、预习任务
StringBuffer类、Date类、SimpleDateFormat、System、Runtime、Math、Random、BigInteger、BigDecimal、比较器、正则表达式。
6、学习问题解决
1、 对象比较假设类都是同一个类。主方法及输出结果如下
public class Hello{ public static void main(String args[]){ Emp emp1 = new Emp(22,"李四","程序员") ; Emp emp2 = new Emp(22,"李四","程序员") ; if(emp1.equals(emp2)){ System.out.println("不是同一个人") ; }else{ System.out.println("是同一个人") ; } } |
public class Hello{ public static void main(String args[]){ Emp emp1 = new Emp(22,"李四","程序员") ; Emp emp2 = new Emp(22,"李四","程序员") ; if(emp1.equals(emp2)){ System.out.println("不是同一个人") ; }else{ System.out.println("是同一个人") ; } } |
是同一个人 |
不是同一个人 |
不明白的是,既然是对象比较那么主方法中的两个输出语句位置应该无所谓先后。而且结果都是同一个人才对,那么所有的属性都是相同的情况下,为什么会出现两次输出结果不一样呢?
2、 数据类型的转换是转型吗?
对象多态性:向上转型和向下转型;
数据类型转换:String向基本型转换,或者是基本型向String转换,称为类型转换。
3、 public class Test{
public static void main(String args[]){
String str1 = "hello";
System.out.println("fun()方法调用之前:"+str1);
fun(str1);
System.out.println("fun()方法调用之后:"+str1);
}
public static void fun(String str){
str="mldn";
}
}
为什么输出的是 hello。
核心:如果看不懂图,把String基本数据类型那样,数值传递(就仿佛是数值拷贝一样)。
4、 关于 equals的问题:
|--equals()用于字符串比较:
String str1 = "Hello" ;
String str2 = new String("Hello") ;
System.out.println(str1.equals(str2)) ;--->true;
|--equals()用于对象比较:
Person per1 = new Person("张三",20) ;
Person per2 = new Person("张三",20) ;
System.out.println(per1.equals(per2)) ; --->false;
问题来了,String类既然也是继承自Object类,字符串比较之前也没有对Object的equals()进行覆写,怎么就相当于内容的比较了呢??
String类覆写了Object类的equals()方法。
5、 匿名内部类不是很懂?
匿名内部类根本就没用,2个月之后用,匿名内部类就是指一个接口或抽象类的子类只使用一次的情况下所采用的技术。
package cn.mldn.demo; public class TestDemo { public static void main(String[] args) throws Exception { new Thread(new Runnable() { @Override publicvoid run() { for (int x = 0; x < 10; x++) { System.out.println(Thread.currentThread().getName() + ",x = " + x); } } }).start(); } } |
复习:复习核心代码,并根据代码推导概念。
重点的概念认真复习,凡是明确给出日后再使用的技术,肯定到时候一用就会了。
之前也强调过了,自己编写的代码之中,现阶段是不去考虑内部类问题的。
7、测试题讲解
1、 请写出String类对象两种实例化方式的区别?
2、 请写出字符串比较的两种方式及区别?
3、 编写一个程序,判断一个字符串是否全部由数字所组成;、
4、 〖SQL〗查询出公司每个工资等级的人数、平均工资、最高和最低工资;
5、 〖SQL〗显示出emp表的5~10行记录。