线程

并行与并发

  • 并发:指的是两个或者多个事件(任务)在同一时间段内发生的。
  • 并行:指的是两个或者多个事件(任务)在同一时刻发生(同时发生)。
    图解如下:
    在这里插入图片描述
    线程与进程
  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个线程;进程也是程序的一次执行过程,是系统 运行程序的基本单位;系统运行一个程序就是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的运行,一个程序中至少有一个线程。一个进程可以有多个线程,这个应用程序也可以称之为多线程程序。
  • 简而言之,一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
    备注:单核处理器的计算机肯定不能并行的处理多个任务的,只能是多个任务在单个cpu上并发的执行。同理,线程也是一样的,从宏观的角度上理解线程是一种并行运行的,但是从微观上分析并行运行不可能,即需要一个一个线程的去执行,当系统只有一个cpu的时候,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
    线程调度:
  • 分时调度:所有的线程轮流使用CPU的使用权,平均分配给每个线程占用CPUP的时间
  • 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机一个线程执行,java使用的就是抢占式调度方式来运行程序。
创建线程类
  • java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或者Thread类的子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流,java使用线程执行体来代表这段程序流。
  • java中通过继承Thread类来创建并启动多线程,步骤如下:
    1、创建一个Thread类的子类
    2、在Thread类的子类当中重写Thread类的run方法,设置线程任务(开启线程需要你做什么事情?)
    3、创建Thread类的子类对象
    4、调用Thread类中的方法start方法,开启新线程,执行run方法
  •   void start()使线程开始执行;java虚拟机调用该线程的run方法。
    
  •   结果是两个线程并发地运行;当前线程(从调用返回给start方法)和另一个线程(执行其run方法)。
    
  •   多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
    
  • java程序属于抢占式调度,哪个线程的优先级高,哪个线程就有就优先执行,同一个优先级的线程,系统就随机一个线程执行
    示例代码如下:
    示例一:
public class Student {

	private String name;

	// 定义一个方法run
	public void run() {
		//定义一个循环,循环20次,分别打印循环的次数
		for (int i = 0; i <20; i++) {
			System.out.println(name + "---->" + i);
		}
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Student(String name) {
		super();
		this.name = name;
	}
	
	
	public Student() {
		super();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Student other = (Student) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + "]";
	}
/*
 * 主线程:执行主(main)方法的线程
 * 
 * 单线程程序:java程序中只有一个线程,执行从main方法开始,程序是从上到下依次执行。
 * 
 * JVM执行main方法,main方法会进入到栈内存当中
 * JVM会调用操作系统开辟一条main方法通向cpu的路径
 * cpu就可以通过这个路径来执行main方法
 * 而这个路径有一个名字,叫主线程(main线程)  Thread(main)
 * 
 */
public class ThreadDemo01 {
	public static void main(String[] args) {
		Student s1 = new Student("小孙");
		s1.run();
		System.out.println(0 / 0);// Exception in thread "main" ArithmeticException
		Student s2 = new Student("小王");
		s2.run();
	}
}

示例二:

//创建一个Thread类的子类
public class MyOneThread extends Thread{
	@Override
	public void run() {
		//循环20次,打印循环次数
		for (int i = 0; i < 20; i++) {
			System.out.println("run:------->"+i);
		}
	}
/**
 * 创建多线程程序的第一种方式,创建Thread类的子类
 * java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
 * 
 * 实现步骤:
 * 		1、创建一个Thread类的子类
 * 		2、在Thread类的子类当中重写Thread类的run方法,设置线程任务(开启线程需要你做什么事情?)
		3、创建Thread类的子类对象
		4、调用Thread类的方法start方法,开启新线程,执行run方法
			void start() 使该线程开始执行,开启新线程,执行run方法
			结果是两个线程并发的运行,当前线程(从调用返回给start方法)和另一个线程(执行其run方法)。
			多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动
		java程序属于抢占式调度,哪个线程的优先级高,哪个线程就有就优先执行,同一个优先级的线程,系统就随机一个线程执行
 */
public class Demo01Thread {
	public static void main(String[] args) {
		MyOneThread myOneThread = new MyOneThread();
		myOneThread.start();
		//在主线程中循环20次 打印
		for (int i = 0; i < 20; i++) {
			System.out.println("main:---->"+i);
		}
		System.out.println("-------------------------");
		//再次开启线程
		myOneThread.start();//报错信息 java.lang.IllegalThreadStateException
	}
多线程的原理

多线程的执行流程原理图:
在这里插入图片描述

  • 程序启动运行main时候,java虚拟机启动一个进程,主线程main在main调用的时候被创建。随着调用 oneThread对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程环境下运行着。
  • 通过上面一张图可以发现多线程在内存当中的执行流程。
  • 多个线程执行时,在栈内存当中,其实每一个线程都有一片属于自己的栈内存空间,进行方法的压栈和弹栈。
  • 当执行线程的任务结束了,线程自动在栈内存当中释放了。当所有的执行线程都结束了,那么进程也就结束了。
Thread类

API帮助文档中定义了有关线程的一些方法,具体如下:
构造方法:

  • public Thread():分配一个新的线程对象
  • public Thread(String name):分配一个指定名字的新的线程对象
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象
  • public Thread(Runnable target,String name):分配一个带有指定目标的新的线程对象并且带有指定名字的。
    常用的方法:
  • public String getName():获取当前线程的名称
  • public void start():让此线程开始执行,java虚拟机会调用此线程的run方法
  • public void run():此线程要执行的任务在此方法内定义。
  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(临时性暂停线程的执行)
  • public static Thread currentThread():获取当前正在执行的线程对象的引用。
    通过翻阅API的得知,创建线程有两种方式,一种是继承Thread类,一种是实现Runable接口,接下来讲解第二种方式.
    代码示例如下:
/*
 * 设置线程的名称:
 *     1.使用Thread类的方法setName(名字)
 *       void setName(String name) 修改线程的名称
 *     2.使用Thread类的带参构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给予子类线程起一个名字
 *       	Thread(String name) 分配一个指定名字的新的Thread对象
 */
// 1.
public class MyThread extends Thread{

	public MyThread(){}
	
	public MyThread(String name) {
		
		super(name);//把线程名称传递给父类,让父类(Thread)给予子线程一个名字
	}
	
	// 2.
	@Override
	public void run() {
		// 获取线程的名称
		System.out.println(Thread.currentThread().getName() + "在执行任务");
	}
	public static void main(String[] args) {
		// 3. 开启新线程
		new MyThread("小强").start();;
		
		// 3. 开启新线程
		MyThread two = new MyThread();
		two.setName("小孙");
		two.start();
		
	}
创建线程方式二
  • 采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。
    步骤如下:
    1. 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
    2. 创建Runnable接口实现类的实例,并以此实例作为Thread类的target来创建Thread类的对象,该Thread类的对象才是真正的线程对象。
    3. 调用线程对象的start()方法来启动新线程。
      实现Runnable接口:
  • java.lang.Runnable:
  • Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
  • lang.lang.Thread:
  • Thread(Runnable target): 分配新的Thread类的对象
  • Thread(Runnable target,String name):分配新的Thread类的对象
    实现步骤:
    1.定义一个Runnable接口的实现类
    2.在实现类中重写Runnable接口当中的run方法,设置线程任务。
    3.创建Runnable接口实现类的对象
    4.构建Thread类的对象,在构造方法中传递Runnable接口的实现类对象
    5.调用Thread类中的start方法,开启新线程执行run方法
    实现Runnable接口创建多线程程序的好处:
    1.避免了单继承的局限性
  •   一个类只能直接继承一个父类,类继承了Thread类就不能继承其他的类
    
  •  实现Runnable接口,还可以继承其他的类,还可以实现其他的接口
    
    2.增强了程序的扩展性,降低了程序的耦合性(解耦)
  • 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
  • 实现类中,重写了run方法:用来设置线程的任务
  • 创建Thread类的对象,调用start方法:用来开启新的线程
    代码如下:
// 1.定义一个Runnable接口的实现类
public class Demo02RunnableImpl implements Runnable{
	// 2.在实现类中重写Runnable接口当中的run方法,设置线程任务。
	@Override
	public void run() {
		// 循环20次,打印循环的次数
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + "--->" + i);
		}
		
	}
}
public class Demo01Runnable {

	public static void main(String[] args) {
		//3. 创建Runnable接口实现类的对象
		Runnable target = new Demo02RunnableImpl();// 多态写法
		//4. 构建Thread类的对象,在构造方法中传递Runnable接口的实现类对象
		Thread thread = new Thread(target);
		//5. 调用Thread类中的start方法,开启新线程执行run方法
		thread.start();
		
		Thread thread2 = new Thread(new Demo03RunnableImpl());
		thread2.start();
		
		
		
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + "--->" + i);
		}
		
	}
}
public class Demo03RunnableImpl implements Runnable{

	@Override
	public void run() {
		
		for (int i = 0; i < 20; i++) {
			System.out.println("hello World" + i);
		}
		
	}

}

Thread类和Runnable接口的区别
  • 如果一个类继承了Thread类,则不适合资源的共享。但是如果实现了Runnable接口的话,则很容易实现资源共享。
    实现Runnable接口比继承Thread类的所具有的优势:
    1. 适合多个相同的程序代码的线程去共享同一个资源
    2. 可以避免java中单继承的局限性
    3. 增加了程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程可以实现分离。
    4. 线程池只能放入实现Runnable或者Callable类的线程,不能直接放入继承Thread的类。
  • 备注:在java中,每次程序运行至少启动两个线程,一个是main线程,一个垃圾收集线程。因为每当使用java命令去执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实都是在操作系统中启动了一个进程。
匿名内部类方式实现多线程程序的创建
  • 使用线程的匿名内部类方式,可以很方便的实现每个线程执行不同的线程任务操作。、

  • 使用匿名内部类方式实现Runnable接口的run方法。
    示例代码如下:

public static void main(String[] args) {
		// 创建线程对象
		//new Thread().start();
		new Thread() {
			// 重写run方法
			@Override
			public void run() {
				// 循环20次,打印循环的次数
				for (int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName() + "--->" + i);
				}
			}
		}.start();
		// 线程的接口Runnable
	    Runnable run = new Runnable() {
	    	// 重写run方法
	    	@Override
	    	public void run() {
	    		// 循环20次,打印循环的次数
				for (int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName() + "--->" + i);
				}
	    	}
	    };
		new Thread(run).start();
		// 简化接口的方式
		new Thread(new Runnable() {
			// 重写run方法
			@Override
			public void run() {
				// 循环20次,打印循环的次数
				for (int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName() + "--->" + i);
				}
			}
		}).start();
线程安全
  • 如果有多个线程在同时的运行,而这些线程可能同时在运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的值是一样的,就是线程安全的。
    代码示例如下:
// 实现卖票案例
public class RunnableImpl implements Runnable {
	// 定义一个多线程共享的资源 票
	private int ticket = 100;
	
	// 设置线程的任务:卖票  此时窗口--->线程
	@Override
	public void run() {
		// 先判断票是否存在
		while(true){
			if (ticket > 0) {
				// 提高卖票的体验感 ,让程序睡眠下
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// 票存在,卖出第ticket张票
				System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");
				ticket--;
			}
		}
	}
}

/*
 * 模拟卖票
 * 创建3个线程(窗口),同时开启,对共享的票进行售卖
 * 
 */
public class SaleTicketDemo {

	public static void main(String[] args) {
		// 创建Runnable接口的实现类对象
		Runnable run = new RunnableImpl();// 多态
		/*Runnable run02 = new RunnableImpl();// 多态
		Runnable run03 = new RunnableImpl();// 多态
*/		
		// 创建Thread类的对象,构造方法中传递Runnable接口的实现类对象
		Thread t1 = new Thread(run);
		Thread t2 = new Thread(run);
		Thread t3 = new Thread(run);
		synchronized (t3) {
			
		}
		// 调用start方法开启新的线程
		t1.start();
		t2.start();
		t3.start();
		
	}
	
}
  • 通过卖票买票小案例发现,当多个线程去共享同一个资源的时候出现了线程的不安全的问题。
    ​ 1.相同的票数,被卖了多次
    ​ 2.不存在的票,也被卖出去了,比如说0和-1
  • 这种问题,几个窗口(线程)票数不同步,这种问题我们称之为线程不安全
  • 备注:线程安全问题一般都是由全局变量或者静态变量引起的,若每个线程中对全局变量、静态变量只有读操作,而无写的操作,这样的话,这个全局变量就是线程安全的;若有多个线程同时执行写操作,一般就需要考虑线程的同步,否则的话就很可能会引发线程的安全问题。
线程的同步
  • 当我们使用多线程访问同一资源的时候,且这多个线程中对资源有的写的操作,就容器出现线程安全问题。
  • 要解决多线程并发访问一个资源的安全问题,java中提供了同步机制(synchronized)来解决。
    根据上一次卖票卖票分析
    窗口1线程进入操作的时候,窗口2和窗口3线程只能在外面等着,当窗口1线程操作结束,窗口1和窗口2和窗口3才有机会进入代码中去执行。也就是说某个线程修改共享资源的时候,其他线程不能修改共享资源,等待修改完毕同步后,才能去抢夺cpu的使用资源,完成对应的操作,保证了数据的同步性。解决了线程不安全的问题。
  • 有三种方式实现同步机制:
    1、同步代码块
    2、同步方法
    3、锁机制
同步代码块

同步代码块:synchronized关键字可以用于方法中的某个代码块中,表示只对这个代码块的资源实行互斥访问。
格式:
synchronized(同步锁) {
// 需要同步操作的代码。
}
同步锁

  • 同步锁是一个对象,是一个抽象的概念,可以想象成在对象上标记了一个锁。
    1. 锁对象可以是任意类型的。Object
    2. 多个线程对象,要使用同一把锁。
      注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到同步锁谁就拥有资格进入代码块中,其他线程只能在外面等待着。(Blocked阻塞状态)
      同步原理图解:
      在这里插入图片描述
      同步方法
  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面的等待着,排队
    格式:
    public synchronized void method(){
    //可能会产生线程安全问题的代码
    }
  • 备注:同步锁是谁?
    1、对于非static方法,同步锁就是this
    2、对于static方法,我们使用当前方法所在类的字节码对象(类名.class)
    例子如下:
    非静态的例子:
// 定义一个多线程共享的资源票
	private static int ticket = 100;

	// 设置线程的任务:卖票 此时窗口---->线程
	@Override
	public void run() {
		System.out.println(this + "---------->");
		// 先判断票是否存在
		while (true) {
			saleTicket();
		}
	}

	/**
	 * 定义一个同步方法 同步方法也会把方法内部的代码锁住 只让一个线程访问 同步方法中的锁对象是谁? 就是实现类对象 new RunnableImpl()
	 * 也就是this
	 */
	public synchronized void saleTicket() {
		if (ticket > 0) {
			// 提高卖票的体验感,让程序睡眠下
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {

				e.printStackTrace();
			} // 票存在卖出第ticket张票
			System.out.println(Thread.currentThread().getName() + "--------正在售卖" + ticket-- + "张票");
		}

	}

静态的例子:

// 定义一个多线程共享的资源票
	private static int ticket = 100;

	// 设置线程的任务:卖票 此时窗口---->线程
	@Override
	public void run() {
		System.out.println(this + "---------->");
		// 先判断票是否存在
		while (true) {
			saleTicket();
		}
	}
public static /* synchronized */ void saleTicket() {
		synchronized (RunnableImpl.class) {
			if (ticket > 0) {
				// 提高卖票的体验,让程序睡眠下
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				// 票存在,卖出第ticket张票
				System.out.println(Thread.currentThread().getName() + "--------->" + ticket + "张票");
				ticket--;
			}
		}

	}
Lock锁:
  • java.util.concurrent.locks.lock机制提供了比synchronized代码块和synchronize同步方法更加广泛的锁操作,同步代码块/同步方法具有的功能,Lock都有,除此之外更加强大,更能体现出面向对象特征的
  • Lock锁也称为同步锁,定义了加锁与解锁的动作,方法如下:
    1、public void lock():同步锁
    2、public void unlock():释放锁
  • 解决线程安全问题的第三种方式,使用Lock锁
  • java.util.concurrent.locks.Lock接口
  • Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作
  • Lock锁接口中实现了两个操作
    • public void lock();//获取锁
    • pupblic void unlock();//释放锁
  • java.util.concurrent.locks.ReentranLock implements Lock接口
    使用步骤:
    1、在成员的位置创建一个ReentranLock对象
    2、在可能会引发线程问题的代码前,调用Lock接口中的lock方法获取锁。
    3、在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
    示例代码如下:
/**
 * 实现买票案例 解决线程安全问题的第三种方式,使用Lock锁 java.util.concurrent.locks.Lock接口
 * 
 * Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作 Lock锁接口中实现了两个操作 public void
 * lock();//获取锁 pupblic void unlock();//释放锁
 * java.util.concurrent.locks.ReentranLock implements Lock接口
 * 
 * 使用步骤: 1、在成员的位置创建一个ReentranLock对象 2、在可能会引发线程问题的代码前,调用Lock接口中的lock方法获取锁。
 * 3、在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
 * 
 */
public class RunnableImpl implements Runnable {
	// 定义一个多线程共享的资源票
	private int ticket = 100;
	// 1、在成员的位置创建一个ReentranLock对象
	Lock Lock = new ReentrantLock();

	// 设置线程的任务:卖票 此时窗口---->线程
	@Override
	public void run() {
		// 先判断票是否存在
		while (true) {
			// 2、在可能会引发线程问题的代码前,调用Lock接口中的lock方法获取锁。
			Lock.lock();
			if (ticket > 0) {
				// 提高卖票的体验感,让程序睡眠下
				try {
					Thread.sleep(100);
					// 票存在卖出第ticket张票
					System.out.println(Thread.currentThread().getName() + "--------正在售卖" + ticket-- + "张票");
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					//无论程序出现异常,此时都会把锁释放掉
					//在finally语句块中一般用于释放资源,关闭Io流,释放lock锁,关闭数据库连接等
					//3、在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
					Lock.unlock();
				}
			}
		}
	}
}
/**
 * 模拟卖票
 * 创建3个线程(窗口),同时开启,对共享的票进行售卖
 */
public class SaleTicketDemo {
	public static void main(String[] args) {
		//创建Runnable接口的实现的对象
		Runnable run = new RunnableImpl();
		//创建Thread类的对象,构造方法中传递Runnable接口的实现类对象
		Thread t1 = new Thread(run);
		Thread t2 = new Thread(run);
		Thread t3 = new Thread(run);
		//调用start方法开启新的线程
		t1.start();
		t2.start();
		t3.start();
	}
线程状态

线程状态概述:
当线程被创建并启动后,它既不是一启动就进入到了执行状态,也不是一直处于执行状态。在线程的生命周期中有6种状态,在javaAPI帮助文档中java.lang.Thread.State这个枚举给出了线程的6中状态。

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是还没有穷,还没有调用start方法
RUNNABL(可运行)线程可以在java虚拟机中运行的状态,可以是正在运行自己的代码,也可能没有,这取决于操作系统处理器
BLOCKED(锁阻塞)当一个线程视图获取一个对象锁,而该对象所被其他线程所持有,则该线程进入到Blocked状态;当该线程持有锁时,该线程进入到Runnable状态
WAITING(无限等待)一个线程在等待另一个线程执行一个动作(新建时),该线程就进入到Waiting状态,进入这个Waiting状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒
TIMED_WAITING(计时器等待)同Waiting状态,有几个方法有超时参数,调用他们将进入Tiimed Waiting状态,这一状态将一直保持到超时期满或者是收到了唤醒通知,带有超市参数的常用方法Thread.sleep(),Object.wait()
TERMINATED(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run()方法而死亡

六中状态切换描述图解:
在这里插入图片描述
Timed Vaiting(计时等待)
Timed Waitng在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态其实当我们调用了sleep方法之后,当前正在执行的线程就进入到了计时等待状态。

  • 例子:如实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。
public class MyThread extends Thread{
	@Overried 
	public void run(){
		for(int i = 1;i <= 100;i++){
			if(i%10==0){
				System.out.println("----------------->"+ i);
			}
			System.out.println(i);
			//每个数字之间暂停1秒
			try{
				Thread.sleep(1000);
			}catch (Exception e){
				e.printStackTrace();	
			}
		}
	}
	//准备一个main函数
	public static void main(String[] args){
		new MyThread().start();
	}
}

备注:

  • 进入到Timed Waiting状态的一种常见的操作是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系
  • 为了让其他线程有机会执行到,一般建议将Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行过程中会睡眠
  • sleep与锁无关,线程睡眠到期会自动苏醒,并返回到Runnable状态。sleep()里面的参数指定的时间是线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行
    在这里插入图片描述
    Blocked锁阻塞状态
  • Blocked状态在JavaAPI中描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
  • 比如:线程A与线程B代码中使用同一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态反之线程B进入到Blocked锁阻塞状态。
    在这里插入图片描述
    Waiting无限等待状态
  • waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别(唤醒)动作的线程处于这一状态。
  • 一个调用了某个对象的Object.wait()方法的线程,会等待另外一个线程调用此对象的Object.netify()或者Object.netifyAll()方法,其实waiting状态它并不是一个线程的操作,它体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。
等待唤醒机制

线程间通信
**概念:**多个线程在处理同一个资源,但是处理的动作(线程的任务)却又不相同。
比如说,线程A用来生产一个娃哈哈饮料,线程B用来消费娃哈哈饮料,娃哈哈饮料可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通讯问题。

  • 图例如下:
    在这里插入图片描述

为什么要处理线程之间的通信:

  • 多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协调通信,以此来帮助我们达到多线程共同操作一份数据
  • 如何保证线程间通信有效利用资源:
  • 多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一变量的使用或者操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。
  • 而这种手段就是----->等待唤醒机制
    等待唤醒机制
  • 什么是等待唤醒机制呢?
  • 这是多个线程间的一种协作机制。
  • 就是一个线程进行了规定操作后,就进入到了等待状态(wait()),等待其他线程执行完它们的指定代码后再将其唤醒(nnotify())
  • 在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程。
  • wait/notify就是线程间的一种协作机制。
    等待唤醒中的方法:
  • 等待唤醒机制就是用来解决线程间通信问题的。可以使用到的方法由三个如下:
    1、wait():线程不在活动,不再参与调度,进入到waiting设置中,因此不会浪费CPU资源,也不会去竞争锁了,这时的线程状态就是WAITING。他还要等着别的线程执行一个特别的动作,就是**唤醒通知(notify)**在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中。
    2、notify():选取所通知对象的wait set中的一个线程释放。例如:餐厅有空位置后,等待就餐最久的雇客最先入座。
    3、notifyAll():释放所通知对象的wait set中的全部线程。
  • 备注:哪怕之通知了一个等待线程,被通知的线程也不能立即回复执行,因为当初中断的地方是在同步块内,而它此刻已经不在持有锁了,所以他需要再次尝试着去获取锁(很有可能民艾琳者其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。
  • 总结下:
    如果能获取到锁,线程就从WAITING状态转变成RUNNABLE状态
    否则,从wait set中,又进入set中,线程就从WAAITING状态转变成BLOCKED状态。
  • 调用wait和notify注意细节:
    1、wait方法与notify方法必须有同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
    2、wait方法与notify方法是属于Object类的方法的。因为,锁对象可以是任意对象,而任意对象的所属对象都是继承了Object类的。
    3、wait方法与notify方法必须要在同步代码块或者同步方法中使用。因为,必须通过锁对象调用这两个方法来实现等待与唤醒。
生产者与消费者问题

等待唤醒机制经典的哪里就是生产者与消费者的问题。
举一个例子:生产包子与消费包子来描述等待唤醒机制如何有效的利用资源:

在这里插入图片描述
在这里插入图片描述

代码示例如下:

/*
 * 资源类:包子类
 *    设置包子的属性
 *      皮
 *      馅
 *      包子的状态  有 true 没有 false
 */
public class Baozi {
	// 皮
	String pi;
	// 馅
	String xian;
	// 包子的状态  有 true 没有 false,设置初始值为false,没有包子
	boolean flag = false;
	
}
// 包子铺
/*
 * 生产者(包子铺):是一个线程类,继承Thread类
 * 	设置线程的任务:生产包子
 *   true:有包子
 *    包子铺调用wait方法进入等待状态
 *   false:没有包子
 *     增加一些难度:交替生产两种包子
 *     有两种状态:(i % 2 == 0)
 *    包子铺生产包子
 *    修改包子的状态为true
 *    唤醒吃货线程,让吃货去吃包子
 *    
 *  注意:
 *  	包子铺线程和吃货线程关系---->通信(互斥)
 *      必须使用同步技术保证两个线程只能有一个线程在执行
 *      锁对象必须保证唯一,可以使用包子对象作为锁对象
 *      包子铺线程和吃货线程的类需要把包子对象作为参数传递进来
 *         1.需要在成员的位置上创建一个包子变量
 *         2.使用带参构造,为这个包子变量赋值 	     
 */
public class Costs extends Thread{
	//1.需要在成员的位置上创建一个包子变量
	private Baozi baozi;
	
	//2.使用带参构造,为这个包子变量赋值 	
	public Costs(Baozi baozi) {
		this.baozi = baozi;
	}
	
	// 重写run方法
	@Override
	public void run() {
		// 设置线程任务:生产包子
		// 定义一个变量
		int count = 0;
		// 让包子铺一直生产包子
		while(true) {
			// 必须保证两个线程只能有一个线程在执行
			synchronized (baozi) {
				// 进行包子状态的判断
				if (baozi.flag) {
					//包子铺有包子,包子铺需要调用wait方法进入等待状态
					try {
						baozi.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				// 包子铺没有包子,被唤醒之后,包子铺生产包子
				// 增加一些难度:交替生产两种类型的包子
				if (count % 2 == 0) {
					//生产 三鲜馅的包子,皮是薄皮
					baozi.pi = "薄皮";
					baozi.xian= "三鲜馅";
				} else {
					// 生产  猪肉大葱馅 冰皮
					baozi.pi = "冰皮";
					baozi.xian = "猪肉大葱馅";
				}
				count++;
				System.out.println("包子铺正在生产:" + baozi.pi + baozi.xian + "包子");
				// 生产包子需要有一个过程:等待3秒钟
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// 包子铺生产好了包子
				// 修改包子的状态为true 有
				baozi.flag = true;
				// 唤醒吃货线程,让吃货线程去吃包子
				baozi.notify();
				System.out.println("包子铺已经生产好了:" + baozi.pi + baozi.xian + "包子,吃货可以开始吃了。。");
			}
		}
	}
}
/*
 * 消费者(吃货)类:是一个线程类  extends Thread
 *   设置线程的任务:吃包子
 *   对包子的状态进行判断
 *   true:有包子
 *      吃货吃包子
 *      吃货吃完包子
 *      修改包子的状态味false:没有包子
 *      吃货唤醒包子铺线程,生产包子
 *   false:没有包子
 *      吃货调用wait方法,进入到等待状态
 */
public class Foodie extends Thread{
	// 1. 需要在成员的位置上定义一个包子变量
	private Baozi baozi;
	
	//2.使用带参构造,为这个包子变量赋值
	public Foodie(Baozi baozi) {
		this.baozi = baozi;
	}
	
	//3. 重写run方法
	@Override
	public void run() {
		// 设置线程任务:吃包子
		// 使用死循环,让吃货一直吃包子
		while(true) {
			// 使用同步技术保证两个线程只有一个线程在执行
			synchronized (baozi) {
				// 对包子的状态进行判断
				if (baozi.flag == false) {
					// 让吃货线程进入到等待状态
					try {
						baozi.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				// 被唤醒后执行吃包子
				System.out.println("吃货正在吃:" + baozi.pi + baozi.xian + "包子");
				// 吃货吃完包子
				// 修改包子的状态为false 没有
				baozi.flag = false;
				// 吃货线程唤醒包子铺线程--->生产包子
				baozi.notify();
				System.out.println("吃货已经把" + baozi.pi + baozi.xian + "的包子");
				System.out.println("------------------------------------------");
			}
		}
	}
}
public class TestChihuoAndBaoziPuDemo {

	public static void main(String[] args) {
		// 创建包子对象
		Baozi baozi = new Baozi();
		// 创建包子铺线程对象
		new Costs(baozi).start();
		// 创建吃货线程对象
		new Foodie(baozi).start();
	}
	
}

线程测试结果如下:

在这里插入图片描述

  • 包子铺线程生产包子,吃货线程消费包子。当没有包子的时候(包子的状态为false),吃货线程需要等待,包子铺线程生产包子(包子的状态为true),并通知吃货线程(解除吃货等待的状态),因为已经有了包子,所以包子铺线程就需要进入到等待状态。
  • 接下来,吃货线程能否进一步执行则取决于锁的获取情况,如果吃货线程获取到锁,那么就执行吃包子的动作,包子吃完了(包子的状态为false),需要通知包子铺线程(解除包子铺线程等待状态),此时吃货线程就进入到等待状态。包子铺线程能否进一步执行则取决于锁的获取情况。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值