首页 > 编程语言 > Java多线程与优先级详细解读
2022
05-24

Java多线程与优先级详细解读

1、多线程

要使用多线程必须有一个前提,有一个线程的执行主类。从多线程开始,Java正式进入到应用部分,而对于多线程的开发,从JavaEE上表现的并不是特别多,但是在Android开发之中使用较多,并且需要提醒的是,鄙视面试的过程之中,多线程所问道的问题是最多的。

1.1 多线程的基本概念

如果要想解释多线程,那么首先应该从单线程开始讲起,最早的DOS系统有一个最大的特征:一旦电脑出现了病毒,电脑会立刻死机,因为传统DOS系统属于单进程的处理方式,即:在同一个时间段上只能有一个程序执行,后来到了windows时代,电脑即使(非致命)存在了病毒,那么也可以正常使用,只是满了一些而已,因为windows属于多进程的处理操作,但是这个时候的资源依然只有一块,所以在同一时间段上会有多个程序共同执行,而在一个时间点上只能有一个程序在执行,多线程实在一个进程基础之上的进一步划分,因为进程的启动所消耗的时间是非常长的,所以在进程之上的进一步划分就变得非常重要,而且性能也会有所提高。

所有的线程一定要依附于进程才能够存在,那么进程一旦消失了,线程也一定会消失,但反过来不一定,而Java是为数不多的支持多线程的开发语言之一。

1.2 多线程的实现

在Java之中,如果要想实现多线程的程序,就必须依靠一个线程的主体类(叫好比主类的概念一样,表示的是一个线程的主类),但是这个线程的主体类在定义的时候也需要有一些特殊的要求,这个类可以继承Thread类或实现Runnable接口来完成定义。

image-20210816122306265

1.3 继承Thread类实现多线程

Java.lang.Thread是一个线程操作的核心类负责线程操作的类,任何类只需要继承了Thread类就可以成为一个线程的主类,但是既然是主类必须有它的使用方法,而线程启动的主方法是需要覆写Thread类中的run()方法才可以(相当于线程的主方法)。

package com.day12.demo;
class MyThread extends Thread{
	private String title;
	public MyThread(String title){
		this.title = title;
	}
	public void run(){
		for (int i = 0; i < 5; i++) {
			System.out.println(this.title + ",i = " + i);
		}
	}
}
public class ThreadDemo {
	@SuppressWarnings("unused")
	public static void main(String[] args) {
		MyThread thread1 = new MyThread("线程1");
		MyThread thread2 = new MyThread("线程2");
		MyThread thread3 = new MyThread("线程3");
		thread1.run();
		thread2.run();
		thread3.run();
	}
}

我们只是做了一个顺序打印操作,而和多线程没有关系。多线程的执行和进程是相似的,而是多个程序交替执行,而不是说各自执行各自的。正确启动多线程的方式应该是调用Thread类中的start()方法。

启动多线程只有一个方法:public void start(); 调用此方法会调用run()

正确启动多线程

package com.day12.demo;
class MyThread extends Thread{
	private String title;
	public MyThread(String title){
		this.title = title;
	}
	public void run(){
		for (int i = 0; i < 5; i++) {
			System.out.println(this.title + ",i = " + i);
		}
	}
}
public class ThreadDemo {
	@SuppressWarnings("unused")
	public static void main(String[] args) {
		MyThread thread1 = new MyThread("线程1");
		MyThread thread2 = new MyThread("线程2");
		MyThread thread3 = new MyThread("线程3");
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

此时再次执行代码发现所有所有的线程对象变为交替执行。所以得出结论:要想启动线程必须依靠Thread类的start()方法执行,线程启动之后会默认调用了run()方法。

问题:为什么线程启动的时候必须调用start()而不是直接调用run()?

如果想要了解必须打开Java源代码进行浏览(在JDK按照目录下)

public synchronized void start() {
    /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
    if (threadStatus != 0)
        //这个异常的产生只有在你重复启动线程的时候才会发生
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        //只声明未实现的方法,同时使用native关键字定义,native调用本机的原生系统函数
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

image-20210816125455104

多线程的实现一定需要操作系统的支持,那么异常的start0()方法实际上就和抽象方法很类似没有方法体,而这个方法体交给JVM去实现,即:在windows下的JVM可能使用A方法实现了start0(),而在Linux下的JVM可能使用了B方法实现了start0(),凡是在调用的时候并不会去关心集体是何方式实现了start0()方法,只会关心最终的操作结果,交给JVM去匹配了不同的操作系统。

​ 所以多线程操作之中,使用start()方法启动多线程的操作是需要进行操作系统函数调用的。

1.4 Runnable接口实现多线程

Thread类的核心功能就是进行线程的启动,但是如果一个类直接继承Threa类就会造成单继承的局限,在Java中又提供了有另外一种实现Runable接口。

public interface Runnable{
	public void run();
}

分享:如何区分新老接口?

​ 在JDK之中个,由于其发展的时间较长,那么会出现一些新的接口和老的接口,这两者有一个最大的明显特征:所有最早提供的接口方法里面都不加上public,所有的新接口里面都有public。

通过Runnable接口实现多线程

package com.day12.demo;
class MyThread implements Runnable{
	private String title;
	public MyThread(String title){
		this.title = title;
	}
	public void run(){
		for (int i = 0; i < 5; i++) {
			System.out.println(this.title + ",i = " + i);
		}
	}
}

这个时候和之前的继承Thread类区别不大,但是唯一的好处就是避免了单继承局限,不过现在问题也就来了,刚刚解释过,如果要想启动多线程依靠Thread类的start()方法完成,之前继承Thread()类的时候可以将此方法直接继承过来使用,但现在实现的是Runnable接口,没有这个方法可以继承了,为了解决这个问题,还是需要依靠Thread类完成,在Thread类中定义一个构造方法:public Thread(Runnable target),接收Runnable接口对象。

利用Thread类启动

public class ThreadDemo {
	@SuppressWarnings("unused")
	public static void main(String[] args) {
		MyThread thread1 = new MyThread("线程1");
		MyThread thread2 = new MyThread("线程2");
		MyThread thread3 = new MyThread("线程3");
		new Thread(thread1).start();
		new Thread(thread2).start();
		new Thread(thread3).start();
	}
}

这个时候就实现了多线程的启动,而且没有了单继承局限。

1.5 Thread类和Runnable接口实现多线程的区别

现在Thread类Runnable接口都可以作为同一功能的方式来实现多线程,那么这两者如果从Java的十年开发而言,肯定使用Ruanable接口,因为可以有效的避免单继承的局限,那么除了这些之外,这两种方式是否还有其他联系呢?

为了解释这两种方式的联系,下面可以打开Thread类的定义:

Public class Thread Object implements Runnable

发现Thread类也是Runnable接口的子类,而如果真的是这样,那么之前程序的结构就变为了一下形式。所以说多线程非常类似于代理模式。

image-20210816131643041

这个时候表现出来的代码模式非常类似于代理设计模式,但是它不是严格意义上的代理设计模式,因为从严格意义上来讲代理设计模式之中,代理主体所能够使用的方法依然是接口中定义的run()方法,而此处代理主题调用的是start()方法,所以只能够说形式上类似于代理设计模式,但本质上还是有差别的。

但是除了以上的联系之外,对于Runnable和Thread类还有一个不太好区分的区别:使用Runnable接口可以更加方便的表示出数据共享的概念。

买票程序

package com.day12.demo;
class MyTicket extends Thread{
	private int ticket = 10;
	public void run(){
		for (int i = 0; i < 20; i++) {
			if(ticket > 0)
			System.out.println("买票 =" + this.ticket--);
		}
	}
}
public class TicketDemo {
	public static void main(String[] args) {
		new MyTicket().start();
		new MyTicket().start();
		new MyTicket().start();
	}
}

image-20210816132621510

运行后发现,数据没有共享。

package com.day12.demo;
class MyTicket extends Thread{
	private int ticket = 10;
	public void run(){
		for (int i = 0; i < 20; i++) {
			if(ticket > 0)
			System.out.println("买票 =" + this.ticket--);
		}
	}
}
public class TicketDemo {
	public static void main(String[] args) {
		MyTicket mt = new MyTicket();
		new Thread(mt).start();
		new Thread(mt).start();
		new Thread(mt).start();
		
	}
}

经过改进之后,发现数据进行共享,但是对于逻辑是解释是不好理解的,MyTicket类继承了Thread类,自己拥有了start()方法但是不执行自己的start()方法,而是通过匿名方法共用MyTicket()实例化对象mt调用匿名方法的start()方法。

再次经过改进之后

package com.day12.demo;
class MyTicket implements Runnable{
	private int ticket = 10;
	public void run(){
		for (int i = 0; i < 20; i++) {
			if(ticket > 0)
			System.out.println("买票 =" + this.ticket--);
		}
	}
}
public class TicketDemo {
	public static void main(String[] args) {
		MyTicket mt = new MyTicket();
		new Thread(mt).start();
		new Thread(mt).start();
		new Thread(mt).start();
		
	}
}

通过MyTicket类实现Runnable接口来进行改进,原因是因为Runnable接口里面只有一个自己的run()方法,而此处的start()的方法,是通过匿名类进行调用start()方法来实现线程的启动。

image-20210816135933824

面试题:请解释多线程的两种实现方式区别?分别编写程序验证两种实现。

多线程的两种实现方式都需要一个线程的主类,而这个类可以实现Runnable接口或继承Thread,不管使用何种方式都必须在子类之中覆写run()方法,此方法为线程的主方法;

Thread类是Runnable接口的子类,而且使用Runnable接口可以避免单继承局限,以及更加方便的实现数据共享的概念。

Runnable 接口:

class MyThread implements Runnable{
	private int ticket=5;
	public void run(){
		for(int x=0;x<50;x++)
			if(this.ticket>0){
				System.out.println(this.ticket--);
			}
	}
}

MyThread mt = new MyThread();
new Thread(mt).start();

Thread 类:

class MyThread extends Thread{
	private int ticket=5;
	public void run(){
		for(int x=0;x<50;x++)
			if(this.ticket>0){
				System.out.println(this.ticket--);
			}
	}
}

MyThread mt = new MyThread();
mt.start();

1.6 线程的操作状态

image-20210816140312921

当我们多线程调用start方法之后不会立刻执行,而是进入就绪状态,等待进行调度后执行,需要将资源分配给你运行后,才可以执行多线程的代码run()中的代码当执行一段时间之后,你需要让出资源,让其他线程来执行,这个时候run()方法可能还没有执行完成,只执行了一半,那么我么我们就需要让资源,随后重新进入就绪状态,重新等待分配新资源继续执行。当线程执行完毕后才会进入终止状态。总结就一句话:线程执行需要分配资源,资源不能独占,执行一会让出资源给其他程序执行。

1.7 Callable实现多线程

jdk增加新的工具类java.util.concurrent

@FunctionalInterface
public interface Callable<V>

Runnable中的run()方法虽然也是线程的主方法,但是其没有返回值,因为它的设计遵循了我们主方法的设计原则:线程开始就别回头。但是在很多时候需要一些返回值,例如:当某些线程执行完成后可能带来一些返回结果,这种情况下我们就只能通过Callabale来实现多线程。

通过分析源代码可以找到一些关系

image-20210816142619937

Callable定义线程主方法启动并取得多线程执行的总结果

package com.day12.demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyTicket1 implements Callable<String>{	
	@Override
	public String call() throws Exception {
		// TODO Auto-generated method stub
		for (int i = 0; i < 20; i++) {
			System.out.println("买票 ,x = " + i);
		}
		return "票卖完了,下次吧";
	}
}
public class CallableDemo {
	@SuppressWarnings({ "unused", "rawtypes", "unchecked" })
	public static void main(String[] args) throws Exception {
		FutureTask<String> task = new FutureTask<>(new MyTicket1());
		new Thread(task).start();
		System.out.println(task.get());
	}
}

这种形式主要就是返回我们的操作结果。

1.8 线程命名和取得

线程本身是属于不可见的运行状态的,即:每次操作的时候是无法预料的,所以如果要想在程序之中操作线程,唯一依靠的就是线程的名称,而要想取得和设置线程的名称可以使用以下的方法:

方法名称 类型 描述
public Thread(Runnable target,String name); 构造 声明参数
public final void setName(String name); 普通 设置线程名称
public final String getName() 普通 取得线程名称

但是由于线程的状态不确定,所以每次可以操作的线程都是正在执行run()方法的线程,那么取得当前线程对象的方法:public static Thread currentThread()。

线程名称的取得

package com.day12.demo;

class MyThread implements Runnable{
	public void run(){
		System.out.println(Thread.currentThread().getName());
			}
}
public class Test {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread mt = new MyThread();
		new Thread(mt).start();//Thread-0
		new Thread(mt).start();//Thread-1
		new Thread(mt).start();//Thread-2
		new Thread(mt,"A").start();//A
		new Thread(mt,"B").start();//B
		new Thread(mt,"C").start();//C
	}
}

如果说现在为线程设置名字的话,那么会使用用户定义的名字,而如果没有设置线程名称,会自动分配一个名称这一点操作和之前讲解的static命名类似。

package com.day12.demo;
class MyThread2 implements Runnable{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName());
	}
	
}
public class RenameThreadDemo {
	public static void main(String[] args) {
		MyThread2 mt = new MyThread2();
		mt.run();//直接通过对象调用方法
		new Thread(mt).start();
	}
}

观察以上程序我们发现,线程的启动都是通过主线程创建并启动的,主方法就是一个线程。

进程在哪里?

实际上每当使用了java命令去解释程序的时候,都表示启动了一个新的JVM进程。而主方法只是这个进程上的一个线程而已。

1.9 线程的休眠

线程的休眠指的是让程序休息一会等时间到了在进行执行。方法:public static void sleep(long millis) throws InterruptedException,设置的休眠单位是毫秒。

package com.day12.demo;
class MyThread implements Runnable{
	public void run(){
		for(int x=0;x<100;x++){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"x="+x);
		}
	}
}
public class Test {
	public static void main(String[] args) throws Exception{
		// TODO Auto-generated method stub
		MyThread mt = new MyThread();
		new Thread(mt,"线程A").start();
		new Thread(mt,"线程B").start();
	}
}

1.10 线程的优先级

从理论上讲,线程的优先级越高,越有可能先执行。如果要想操作线程的优先级有如下两个方法:

设置线程优先级:public final void setPriority(int newPriority);

取得线程优先级:public final int getPriority();

发现设置取得优先级的时候都是利用一个int型数据的操作,而这个int型数据有三种取值:

​ **最高优先级:**public static final int MAX_PRIORITY,10;

​ **中等优先级:**public static final int NORM_PRIORITY,5;

​ **最低优先级:**public static final int MIN_PRIORITY,1;

设置优先级

package com.day12.demo;
class MyThread3 implements Runnable{

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + "i = "+ i);
		}
	}
	
}
public class PriorityDemo {
	public static void main(String[] args) {
		MyThread3 mt = new MyThread3();
		Thread thread1 = new Thread(mt,"线程A");
		Thread thread2 = new Thread(mt,"线程B");
		Thread thread3 = new Thread(mt,"线程C");
		thread1.setPriority(Thread.MIN_PRIORITY);
		thread2.setPriority(Thread.MAX_PRIORITY);
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

主方法只是一个中等优先级。

1.11 线程的同步与死锁

所谓的同步问题是指多个线程操作统一次元所带来的信息安全性问题,

下面模拟一个简单的卖票程序,要求有3个线程,卖10张票。

image-20210816180351233

package com.day12.demo;
class MyTicket implements Runnable{
	private int ticket = 10;
	public void run(){
		for (int i = 0; i < 20; i++) {
			if(ticket > 0){
				try {
					Thread.sleep(200);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "买票 =" + this.ticket--);
			}
			
		}
	}
}
public class TicketDemo {
	public static void main(String[] args) {
		MyTicket mt = new MyTicket();
		new Thread(mt,"票贩子A").start();
		new Thread(mt,"票贩子B").start();
		new Thread(mt,"票贩子C").start();
		
	}
}

运行上面程序发现,票数为0或者为负数,这种操作我们称为不同步操作。

image-20210816181053613

不同步的唯一好处就是处理速度快(多个线程并发执行),而去银行是一个业务员对应一个客户,这个速度必然很慢。数据的不同步对于访问是不安全的操作。

同步是指所有的线程不是一起进入方法中执行,而是一个一个进来执行。

image-20210816181431244

如果要写实现这把锁的功能,那么可以使用synchronized关键字进行处理,有两种处理模式:同步代码块、同步方法。

同步代码块

如果要使用这种情况必须设置一个要锁定当前对象

package com.day12.demo;
class MyTicket implements Runnable{
	private int ticket = 2000;
	public void run(){
		for (int i = 0; i < 1000; i++) {
			//在同一时刻,只允许一个线程进入并且操作,其他线程需要等待
			synchronized(this){//线程的逻辑锁
				if(ticket > 0){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "买票 =" + this.ticket--);
				}
			}
			
		}
	}
}
public class TicketDemo {
	public static void main(String[] args) {
		MyTicket mt = new MyTicket();
		Thread thread1 = new Thread(mt,"票贩子A");
		thread1.setPriority(Thread.MIN_PRIORITY);
		thread1.start();
		new Thread(mt,"票贩子B").start();
		new Thread(mt,"票贩子C").start();
		
	}
}

同步方法

package com.day12.demo;
class MyTicket implements Runnable{
	private int ticket = 2000;
	public void run(){
		for (int i = 0; i < 1000; i++) {
			//在同一时刻,只允许一个线程进入并且操作,其他线程需要等待
			synchronized(this){//线程的逻辑锁
				this.sale();
			}
		}
	}
	public synchronized void sale(){
		if(ticket > 0){
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} 
			System.out.println(Thread.currentThread().getName() + "买票 =" + this.ticket--);
		}
	}
}
public class TicketDemo {
	public static void main(String[] args) {
		MyTicket mt = new MyTicket();
		Thread thread1 = new Thread(mt,"票贩子A");
		thread1.setPriority(Thread.MIN_PRIORITY);
		thread1.start();
		new Thread(mt,"票贩子B").start();
		new Thread(mt,"票贩子C").start();
		
	}
}

同步虽然可以保证数据的完整性(线程安全操作),但是其执行的速度很慢。

1.12 死锁

一个线程执行完毕后才可以继续执行,但是如果现在相关的几个线程,彼此几个线程都在等待(同步),那么就会造成死锁。

image-20210816185434363

模拟死锁程序

package com.day12.demo;
class JieFei{
	public synchronized void say(Person Person){
		System.out.println("给钱放人");
		Person.get();
	}
	public synchronized void get(){
		System.out.println("得到钱");
	}
}
class Person{
	public synchronized void say(JieFei jiefei){
		System.out.println("放人就给钱");
		jiefei.get();
	}
	public synchronized void get(){
		System.out.println("得到人");
	}
}
public class DeadLock implements Runnable {
	JieFei jie = new JieFei();
	Person person = new Person();
	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		new DeadLock(); 
	}
	public DeadLock(){
		new Thread(this).start();
		jie.say(person);
	}
	@Override
	public void run() {
		// TODO 自动生成的方法存根
		person.say(jie);
	}
}

死锁实在日后多线程程序开发之中经常会遇见问题,而以上的代码并没有任何实际意义,大概可以理解死锁的操作形式就可以了,不用去研究程序。记住一句话:数据要想完整操作必须使用同步,但是过多的同步会造成死锁。

面试题:请问多线程操作统一资源的时候要考虑到那些,会带来的问题?

多线程访问统一资源的时候一定要考虑同步的问题,但是过多的同步会带来死锁。

综合案例

生产者和消费者是一道最为经典的供求案例:provider、consumer。不管是之后的分布式开发还是其他开发都被大量采用。

生产者只是负责生产数据,而生产者每生产一个完整的数据,消费者就把这个数据拿走。假设生产如下数据 title = 生产,note = 出库;title=拿货, note = 消费

image-20210816190031191

package com.day12.demo;

class Message{
	private String title;
	private String note;
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getNote() {
		return note;
	}
	public void setNote(String note) {
		this.note = note;
	}
	
}
class Productor implements Runnable{
	Message msg = null;
	public Productor(Message msg){
		this.msg=msg;
	}

	@Override
	public void run() {
		// TODO 自动生成的方法存根
		for(int x = 0;x<50;x++){
			if(x%2==0){
				try {
					msg.setTitle("生产");
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
				msg.setNote("出库");
			}else{
				msg.setNote("拿货");
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
				msg.setNote("消费");
			}
		}
	}
}
class Consumer implements Runnable{
	Message msg = null;
	public Consumer(Message msg){
		this.msg=msg;
	}
	@Override
	public void run() {
		// TODO 自动生成的方法存根
		for(int x= 0;x<50;x++){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
			System.out.println(this.msg.getTitle()+"--->"+this.msg.getNote());
		}
	}
}	
public class PCDemo {
	public static void main(String args[]){
		// TODO 自动生成的方法存根
		Message msg = new Message();
		Productor pro = new Productor(msg);
		Consumer con = new Consumer(msg);
		new Thread(pro).start();
		new Thread(con).start();
	}
}

但是异常的代码模型出现了如下的两个严重的问题:

数据错位了出现了重复取出和重复设置的问题

1.解决数据错位问题:依靠同步解决

package com.day12.demo;

class Message {
	private String title;
	private String note;

	public synchronized void set(String title, String note) {
		try {
			Thread.sleep(50);
		} catch (InterruptedException e) {
			// TODO 自动生成的 catch 块
			e.printStackTrace();
		}
		this.title = title;

		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO 自动生成的 catch 块
			e.printStackTrace();
		}
		this.note = note;
	}

	public synchronized void get() {
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO 自动生成的 catch 块
			e.printStackTrace();
		}
		System.out.println(this.title + "--->" + this.note);
	}
}

class Productor implements Runnable {
	Message msg;

	public Productor(Message msg) {
		this.msg = msg;
	}

	@Override
	public void run() {
		// TODO 自动生成的方法存根
		for (int x = 0; x < 50; x++) {
			if (x % 2 == 0) {
				msg.set("生产", "出库");
			} else {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
				msg.set("拿货", "消费");
			}
		}
	}
}

class Consumer implements Runnable {
	Message msg = null;

	public Consumer(Message msg) {
		this.msg = msg;
	}

	@Override
	public void run() {
		// TODO 自动生成的方法存根
		for (int x = 0; x < 50; x++) {
			msg.get();
		}
	}
}

public class PCDemo {
	public static void main(String args[]) {
		// TODO 自动生成的方法存根
		Message msg = new Message();
		Productor pro = new Productor(msg);
		Consumer con = new Consumer(msg);
		new Thread(pro).start();
		new Thread(con).start();
	}
}

虽然解决了错位的问题,但是重复设置重复取出更加严重了。

2.解决数据的重复设置和重复取出

要想解决重复的问题需要等待及唤醒机制,而这一机制的实现只能依靠Object类完成,在Object类之中定义了以下的三个方法完成线程操作:

方法名称 类型 描述
public final void wait() throws InterruptedException 普通 等待、死等
public final void notify() 普通 唤醒第一个等待线程
public final void notifyAll() 普通 唤醒全部等待线程

通过等待与唤醒机制来解决数据的重复操作问题

package com.day12.demo;

class Message {
	private String title;
	private String note;
	//flag = true  允许生产但是不允许消费者取走
	//flag = false 生产完毕 允许消费者取走,但是不能够生产
	private boolean flag = false;
	public synchronized void set(String title, String note) {
		if(flag == true){//生产完毕 允许消费者取走,但是不能够生产
			try {
				super.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		this.title = title;
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO 自动生成的 catch 块
			e.printStackTrace();
		}
		this.note = note;
		flag = true;
		super.notify();
	}

	public synchronized void get() {
		if(flag == false){ //允许生产但是不允许消费者取走
			try {
				super.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		try {
			Thread.sleep(50);
		} catch (InterruptedException e) {
			// TODO 自动生成的 catch 块
			e.printStackTrace();
		}
		System.out.println(this.title + "--->" + this.note);
		flag = false;
		super.notify();
	}
}

class Productor implements Runnable {
	Message msg;

	public Productor(Message msg) {
		this.msg = msg;
	}

	@Override
	public void run() {
		// TODO 自动生成的方法存根
		for (int x = 0; x < 50; x++) {
			if (x % 2 == 0) {
				msg.set("生产", "出库");
			} else {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
				msg.set("拿货", "消费");
			}
		}
	}
}

class Consumer implements Runnable {
	Message msg = null;

	public Consumer(Message msg) {
		this.msg = msg;
	}

	@Override
	public void run() {
		// TODO 自动生成的方法存根
		for (int x = 0; x < 50; x++) {
			msg.get();
		}
	}
}

public class PCDemo {
	public static void main(String args[]) {
		// TODO 自动生成的方法存根
		Message msg = new Message();
		Productor pro = new Productor(msg);
		Consumer con = new Consumer(msg);
		new Thread(pro).start();
		new Thread(con).start();
	}
}

面试题:请解释sleep()和wait()的区别?

  • sleep()是Thread类中定义的方法,到了一定的时间后该休眠的线程自动唤醒,自动唤醒。
  • wait()是Object类中定义的方法,如果要想唤醒,必须使用notify()、notifyAll()才能唤醒,手动唤醒。

到此这篇关于Day12基础不牢地动山摇-Java基础的文章就介绍到这了,更多相关Java基础内容请搜索自学编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持自学编程网!

编程技巧