程序利用线程可以进行更高效的程序处理,如果没有多线程,那么一个程序在处理某些资源的时候会有主方法(主线程全部处理),但是这样的处理速度一定会比较慢,但是如果采用了多线程的处理机制,,利用主线程创建出许多的子线程,那么效率会更高。
4.1 线程同步问题引出
线程同步是指若干个线程对象并行进行资源访问时实现资源处理的保护操作,下面通过一个程序说明。
范例:卖票操作(3个线程卖3张票)
package cn.kuiba.util;
class MyThread implements Runnable{ //定义线程执行类
private int ticket=3; //总票数为3张
@Override
public void run(){
while (true){ //持续卖票
if (this.ticket>0){ //还有剩余票
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
}else {
System.out.println("**********票已经卖光了**********");
break; //跳出循环
}
}
}
}
public class Main {
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();
}
}
程序执行结果:
售票员C卖票,ticket=3
售票员A卖票,ticket=2
售票员B卖票,ticket=3
售票员A卖票,ticket=0
售票员B卖票,ticket=-1
**********票已经卖光了**********
售票员C卖票,ticket=1
**********票已经卖光了**********
**********票已经卖光了**********
在本程序中为了更好地观察到同步问题,在判断票数(this.ticket>0)和卖票(this.ticket--)操作之间追加了一个线程休眠操作以实现延迟的模拟。通过执行的结果也可以发现程序出现了不同步的问题,而造成这些问题主要是由于代码的操作结构所引起的,因为卖票操作分为两个步骤。
步骤一(this.ticket>0):判断票数是否大于0,大于0则表示还有票可以卖。
步骤二(this.ticket--):如果票数大于0,则卖票出去。
假设现在只剩最后一张票,当第一个线程满足售票条件后(此时并未减少票数),其他线程也可能同时满足售票的条件,这样同时进行自减操作时就有可能造成负数,操作如图。
4.2 线程同步处理
造成并发资源访问不同步的主要原因在于没有将若干个程序逻辑单元进行整体性的锁定,即当判断数据和修改数据时只允许一个线程进行处理,而其他线程需要等当前线程执行完毕后才可以继续执行,这样就使得在同一时间段内,只允许一个线程操作,从而实现同步的处理。
Java中提供有synchronized关键字以实现同步处理,同步的关键在于要为代码加上“锁”,而对于锁的操作程序有两种:同步代码块、同步方法。
同步代码块是指使用synchronized关键字定义的代码块,在该代码执行时往往需要设置一个同步对象,由于线程操作的不确定状态,所以这个时候的同步对象可以选择this。
范例:使用同步代码块
class MyThread implements Runnable{ //定义线程执行类
private int ticket=3; //总票数为3张
@Override
public void run(){
while (true){ //持续卖票
synchronized (this){ //同步代码块
if (this.ticket>0){ //还有剩余票
try {
Thread.sleep(100); //模拟网络延迟
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
}else {
System.out.println("**********票已经卖光了**********");
break; //跳出循环
}
}
}
}
}
public class Main {
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();
}
}
程序执行结果:
售票员A卖票,ticket=3
售票员C卖票,ticket=2
售票员C卖票,ticket=1
**********票已经卖光了**********
**********票已经卖光了**********
**********票已经卖光了**********
本程序将票数判断与票数自减的两个控制逻辑放在了一个同步代码块中,当进行多线程并发执行时,只允许有一个线程执行此部分代码,就实现了同步处理操作。
提示:线程的同步操作本质在于同一个时间段内只允许一个线程执行,所以此线程对象在未执行完的过程中其他线程对象都处于等待状态,会导致程序处理性能下降。但是同步也会带来优点:数据的线程访问安全。
同步代码块可以直接定义在某个方法之中,使得方法的部分操作进行同步处理,但是如果现在某一个方法中的全部操作都需要进行同步处理,则可以采用同步方法的形式进行定义,即在方法声明上使用synchronized关键字即可。
范例:使用同步方法
package cn.kuiba.util;
class MyThread implements Runnable{
private int ticket=3;
@Override
public void run(){
while (this.sale()){ //调用同步方法
;
}
}
public synchronized boolean sale(){ //售票操作
if (this.ticket>0){
try {
Thread.sleep(100); //模拟网络延迟
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
return true;
}else {
System.out.println("*******票已经卖光了********");
return false;
}
}
}
public class Main {
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();
}
}
程序执行结果:
售票员B卖票,ticket=3
售票员B卖票,ticket=2
售票员B卖票,ticket=1
*******票已经卖光了********
*******票已经卖光了********
*******票已经卖光了********
本程序将需要进行线程同步处理的操作封装在了sale()方法中,当多个线程并发访问时可以保证数据操作的正确性。
4.3 线程死锁
同步是指一个线程要等待另一个线程执行完毕才会继续执行的一种操作形式,虽然在一个程序中,使用同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。例如死锁。
所谓的死锁,是指两个线程都在等待对方先完成,造成程序的停滞状态。一般程序死锁都是在程序运行时出现的。
范例:观察程序死锁
package cn.kuiba.util;
class Book{
public synchronized void tell(Painting painting){ //同步方法
System.out.println("张三说:你把画给我,我给你书,不给画不给书!");
painting.get();
}
public synchronized void get(){ //同步方法
System.out.println("张三拿到了画");
}
}
class Painting{
public synchronized void tell(Book book){ //同步方法
System.out.println("李四说:你把书给我,我给你画,不给书不给画!");
book.get();
}
public synchronized void get(){ //同步方法
System.out.println("李四拿到了书");
}
}
public class Main implements Runnable {
private Book book=new Book();
private Painting painting=new Painting();
public Main(){
new Thread(this).start();
book.tell(painting);
}
@Override
public void run(){
painting.tell(book);
}
public static void main(String[] args){
new Main();
}
}
程序执行结果:
张三说:你把画给我,我给你书,不给画不给书!
李四说:你把书给我,我给你画,不给书不给画!
***程序处于相互等待的状态,后续代码不再执行******
为了更好的观察死锁带来的影响,本程序使用了大量同步处理操作,而死锁一旦出现程序将进入等待状态并且不会往下执行。实际开发中回避线程死锁的问题是设计的难点。
14.5 综合案例:生产者与消费者
在多线程操作中有一个经典案例————生产者不断生产,消费者不断取走生产者生产的产品。所以生产者和消费者分别为两个线程对象,这两个线程对象同时向公共区域进行数据的保存和读取,所以可以按照如图的类结构实现程序模型。
范例:程序基础模型
package cn.kuiba.util;
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 Producer implements Runnable{ //定义生产者
private Message msg=null;
public Producer(Message msg){
this.msg=msg;
}
@Override
public void run(){
for (int x=0;x<50;x++){ //生产50次数据
if (x%2==0){
this.msg.setTitle("JOE"); //设置title属性
try {
Thread.sleep(100); //延迟操作
}catch (InterruptedException e){
e.printStackTrace();
}
this.msg.setContent("Megalo boxer"); //设置content属性
}else {
this.msg.setTitle("魁拔"); //设置title属性
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
this.msg.setContent("www.kuiba.com"); //设置content属性
}
}
}
}
class Consumer implements Runnable{ //定义消费者
private Message msg=null;
public Consumer(Message msg){
this.msg=msg;
}
@Override
public void run(){
for (int x=0;x<50;x++){ //取走50次数据
try {
Thread.sleep(100); //延迟
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(this.msg.getTitle()+"-->"+this.msg.getContent());
}
}
}
public class Main{
public static void main(String[] args)throws Exception{
Message msg=new Message(); //定义Message对象,用于保存和取出数据
new Thread(new Producer(msg)).start(); //产生生产者线程
new Thread(new Consumer(msg)).start(); //取得消费者线程
}
}
程序执行结果(截取部分随机结果):
魁拔-->Megalo boxer
魁拔-->Megalo boxer
JOE-->www.kuiba.com
JOE-->www.kuiba.com
JOE-->Megalo boxer
魁拔-->www.kuiba.com
本程序实现了一个基础的线程交互模型,但是通过执行结果可以发现程序中存在两个问题。
1.数据错位:假设生产者线程刚向数据存储空间添加了信息的名称,还没有加入这个信息的内容,程序就切换到了消费者线程,而消费者线程把这个信息的名称和上一个信息的内容联系到了一起。
2.重复操作:生产者放了若干次的数据,消费者才开始取数据;或者是消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已经取过的数据。
5.1 解决数据同步问题
数据同步的问题只能够通过同步代码块或者同步方法完成。在本程序中,生产者和消费者代表着不同的线程对象,所以此时的同步操作应该设置在Message类中,可以将title和content属性设置定义为单独同步方法。
范例:定义同步操作
package cn.kuiba.util;
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 String get(){
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
return this.title+"-->"+this.content;
}
//setter、getter略
}
class Producer implements Runnable{ //定义生产者
private Message msg= null;
public Producer(Message msg){
this.msg=msg;
}
@Override
public void run(){
for (int x=0;x<50;x++){ //生产50次数据
if (x%2==0){
this.msg.set("JOE","Megalo boxer"); //设置属性
}else {
this.msg.set("魁拔","www.kuiba.com"); //设置属性
}
}
}
}
class Consumer implements Runnable{ //定义消费者
private Message msg=null;
public Consumer(Message msg){
this.msg=msg;
}
@Override
public void run(){
for (int x=0;x<50;x++){ //取走50次数据
System.out.println(this.msg.get()); //取走属性
}
}
}
public class Main{
public static void main(String[] args)throws Exception{
Message message=new Message(); //定义Message对象,用于保存和取出数据
new Thread(new Producer(message)).start(); //启动生产者线程
new Thread(new Consumer(message)).start(); //启动消费者线程
}
}
程序执行结果(截取部分随机结果):
JOE-->Megalo boxer
魁拔-->www.kuiba.com
JOE-->Megalo boxer
魁拔-->www.kuiba.com
JOE-->Megalo boxer
魁拔-->www.kuiba.com
JOE-->Megalo boxer
本程序在Message类中定义了两个同步处理方法,这样使得不同线程在进行公共数据区域操作时都可以保证数据的完整性,解决了数据设置错乱的问题。
5.2 Object线程等待与唤醒
重复操作问题的解决需要引入线程的等待与唤醒机制,而这一机制的实现只能依靠Object类完成。在Object类中定义了3种方法完成线程的操作,如表。
从表中可知,一个线程可以为其设置等待状态,但是唤醒的操作只有两个:notify()、notifyAll()。一般来说,所有等待的线程会按照顺序进行排列,如果使用了notify()方法,则会唤醒第一个等待的线程执行;而如果使用notifiAll()方法,则会唤醒所有的等待线程,哪个线程优先级高,就先执行。
清楚了Object类中的3个方法之后,就可以解决程序中的问题。如果想要生产者不重复生产,消费者不重复取走,则可以增加一个标志位。假设标志位为boolean型变量,如果标志位的内容位true,则表示可以生产,但是不能取走,如果此时线程执行到了消费者线程则应该等待;如果标志位内容为false,则表示可以取走,但不能生产。操作流程如图。
要想完成以上的功能,直接修改Message类即可。在Message类中加入标志位,并通过判断标志位的内容完成线程等待与唤醒操作。
范例:修改Message类,解决数据的重复设置和重复取出的操作
package cn.kuiba.util;
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 String get(){
if (this.flag==true){ //还未生产,需要等待
try {
super.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
try {
return this.title+"-"+this.content;
}finally { //不论如何都要执行
this.flag=true; //继续生产
super.notify(); //唤醒等待线程
}
}
//setter、getter略
}
在本程序中追加了一个数据产生与消费操作的控制逻辑成员属性(flag),通过此属性的值控制实现线程的等待与唤醒处理操作,从而解决了线程重复操作的问题。