目录
1.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();
}
}
这个时候发现操作的数据之中出现了负数,这个就可以理解为不同步问题。
如果现在要想增加这个锁,在程序之中就可以通过两种方式完成:一种是同步代码块,另外一种就是同步方法。
实现一:同步代码块,使用synchronized关键字定义的代码块就称为同步代码块,但是在进行同步的操作之中必须设置一个要同步的对象,而这个对象应该理解为当前对象:this。
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();
}
}
方式二:同步方法
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();
}
}
加入同步之后明显比不加入同步慢许多,所以同步的代码性能会很低,但是数据的安全性会高。
1.1.2、死锁
同步就是指一个线程要等待另外一个线程执行完毕才会继续执行的一种操作形式,但是如果在一个操作之中都是在互相等着的话,那么就会出现死锁问题。
范例:下面简单的模拟一个死锁程序的样式
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) ;
}
}
面试题:请问多个线程操作同一资源的时候要考虑到那些,会带来那些问题?
多个线程访问同一资源的时候一定要考虑到同步的问题,但是过多的同步会带来死锁。
1.2、生产者和消费者的程序
在多线程的开发之中存在一种称为“生产者和消费者的程序”,这个程序的主要功能是生产者负责生产一些内容,每当生产完成之后,会由消费者取走全部内容,那么现在假设要生产的是如下两种数据:
· 数据一:title = 2012.12.21日,content = 世界末日;
· 数据二:title = 付谢,content = 打扫卫生迎接末日。
现在对于这样的程序,可以使用如下的一些基本模型实现。
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() ;
}
}
运行结果:
2012.12.21 --> null
付谢 --> 世界末日
付谢 --> 世界末日
付谢 --> 世界末日
2012.12.21 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
2012.12.21 --> 打扫卫生迎接末日
付谢 --> 世界末日
付谢 --> 世界末日
......
但是,以上的代码模型出现了如下的两严重问题:
· 数据错位了;
· 出现了重复取出和重复设置的问题。
1.2.1、解决数据错位问题:依靠同步就可以解决
只要对设置和取得加上同步应用,就可以解决数据的错位的操作问题,下面,对代码进行修改。
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() ;
}
}
这个时候的确解决了数据的错位的问题,但同时新的问题又来了:发现数据的重复问题更严重了。
运行结果2012.12.21 --> 世界末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
付谢 --> 打扫卫生迎接末日
2012.12.21 --> 世界末日
......
1.2.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()之后才结束等待。
本文探讨了线程同步在卖票程序中的应用,如何避免数据错位和死锁,以及生产者消费者模型中遇到的问题。通过实例解析了synchronized关键字和wait/notify机制在解决重复设置和取出问题上的作用。
3096

被折叠的 条评论
为什么被折叠?



