zoukankan      html  css  js  c++  java
  • Java多线程编程(同步、死锁、生产消费者问题)

    Java多线程编程(同步、死锁、生产消费):

    关于线程同步以及死锁问题:

    线程同步概念:是指若干个线程对象并行进行资源的访问时实现的资源处理保护操作;

    线程死锁概念:是指两个线程都在等待对方先完成,造成程序的停止的状态;

    先了解相应的概念,后面深入理解。

    同步:

    举个例子:还是卖票问题(经典❗)

    1. 不存在同步
    2. 开启三个线程(售票员)测试
    package com.xbhog;
    class MyThread implements Runnable {// 定义线程执行类
        private int ticket = 3;// 总票数为6张
        @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 Java多线程核心 {
        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 = 2
    售票员C卖票,ticket = 3
    售票员A卖票,ticket = 3
    售票员A卖票,ticket = 1
    售票员B卖票,ticket = -1
    ***** 票已经卖光了
    售票员C卖票,ticket = 0
    票已经卖光了
    票已经卖光了 *****
    售票员B卖票,ticket = 1
    ***** 票已经卖光了
    售票员A卖票,ticket = 3
    票已经卖光了
    售票员C卖票,ticket = 2
    票已经卖光了 *****

    存在上述原因是因为在代码中两个地方存在多线程访问时出现模糊的问题:

    1. this.ticket>0;
    2. this,ticket--;

    假设现在剩余的票数为1张;当第一个线程满足售票的条件的时候(此时还未减少票数),其他的线程也可能同时满足售票的条件,这样同时进行自减减就可能造成负数!

    解决上述问题就需要采用线程同步技术实现;

    首先需要明确,在Java中实现线程同步(synchronized)的方法有两个:

    1. 同步代码块(同步策略加在方法内部)

      package com.xbhog.多线程1;
      class MyThread implements Runnable {						// 定义线程执行类
          private int ticket = 3; 								// 总票数为6张
          @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 Java多线程同步代码块 {
          public static void main(String[] args) {
              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
      售票员B卖票,ticket = 1
      ***** 票已经卖光了 *****
      ***** 票已经卖光了 *****
      ***** 票已经卖光了 *****
      
    2. 同步方法(同步策略加在方法上)

      class MyThread implements Runnable {						// 定义线程执行类
      	private int ticket = 3; 								// 总票数为6张
      	@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 ThreadDemo {
      	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
      售票员B卖票,ticket = 1
      ***** 票已经卖光了 *****
      ***** 票已经卖光了 *****
      ***** 票已经卖光了 *****
      

    同步的本质:在同一个时间段只允许有一个线程执行资源,所以在此线程对象未执行完的过程中其他线程对象将处于等待的状态。

    同步的优点与缺点:

    1. 可以保证数据的准确性

    2. 数据线程的访问安全


    3. 程序的处理性能下降

    死锁:

    实例:

    假如现在又张三想要李四的画,李四想要张三的书,那么张三对李四说:把你的画给我,我就给你书;

    李四对张三说:把你的书给我,我就给你画;

    此时:张三在等待李四,李四在等待张三,两人一直等待下去形成死锁;

    观察线程的死锁:(实现张三李四)

    package com.xbhog.死锁;
    
    class Book {
        public synchronized void tell(Painting paint) {		// 同步方法
            System.out.println("张三对李四说:把你的画给我,我就给你书,不给画不给书!");
            paint.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 DeadLock implements Runnable{
        private Book book = new Book();
        private Painting paint = new Painting();
        public DeadLock() {
            new Thread(this).start();
            book.tell(paint);
        }
        @Override
        public void run() {
            paint.tell(book);
        }
        public static void main(String[] args) {
            new DeadLock() ;
        }
    }
    

    由于死锁是不可预测发生的,该代码有可能在一次运行中展示不出效果来,需要多次运行观察效果;

    效果图:

    image-20210422104049966

    由此引申出了生产者与消费者模型。

    生产者与消费者问题:

    首先需要明确生产者与消费者为两个线程对象,是对同一资源进行数据的保存与读取;

    基本操作是:生产者生产一个资源,消费者则取走一个资源,一一对应。

    对应类关系图:

    image-20210421234308037

    我们需要设想一个问题,如果不加任何操作的话,会出现什么问题?

    1. 数据错位:当生产者线程只是开辟了一个栈空间保存信息名称,在想存数据但是还没存数据的时候切换到了消费者线程上,那么消费者线程将会把这个信息名称与上个信息的内容进行结合联系,这样就造成了数据的错位。
    2. 重复数据:当生产者放了若干次的数据,消费者才开始取数据,或者消费者取完,但生产者还没生产新数据时又取了直接已经取过得数据。

    解决以上两个问题需要涉及到以下两个知识点:

    1. 设置同步代码块或设置同步方法>>>解决数据错误问题

    2. Object线程等待与唤醒>>>解决数据重复设置以及重复取出的问题

    增加数据同步方法或同步代码块:

    在本程序中,生产者与消费者代表的都是线程对象,所以同步操作只能在Message类中,可以将set与get方法设置为单独的同步方法。

    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("xbhog","22") ;	// 设置属性
    			} else {
    				this.msg.set("xbhog","www.cnblog.cn/xbhog") ;	// 设置属性
    			}
    		}
    	}
    }
    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 ThreadDemo {
    	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() ;		// 取得消费者线程
    	}
    }
    

    Object线程等待与唤醒机制:

    线程的等待与唤醒只能依靠Object来完成,如果想要让生产者与消费者一个一个拿,一个一个取,那么需要加入标志位来确定线程的当前状态;

    由图所示:

    image-20210422111820013

    当生产者线程与消费者线程进入时,判断当前的标志位是否为true,

    1. true:表示生产者可以生产资源,但是消费者不能取走资源

    2. false:表示生产者不能生产资源,但是消费者需要取走资源

    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(100);
    		} 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(); 								// 唤醒等待线程
    		}
    	}
    }
    

    在本程序中追加一个数据产生与消费的控制逻辑成员属性,通过此程序的值控制实现线程的等待与唤醒处理操作,从而解决线程重复操作的问题。

  • 相关阅读:
    JDBC 删除数据两种方式,PreparedStatement表示预编译的 SQL 语句的对象,防止sql注入
    MySQL 主键外键
    JDBC 增删改查
    MySQL数据库语句
    反射
    如何安装、配置、登陆MySQL
    如何干净卸除MySQL数据库
    IO流总结
    字符流五种读写 字符流 BufferedWriter BufferedReader 带缓冲区的字符流
    单例模式
  • 原文地址:https://www.cnblogs.com/xbhog/p/14689101.html
Copyright © 2011-2022 走看看