java程序员从笨鸟到菜鸟之(四十二)线程再涉

线程的引出

1 现在有一个需求:

某电影院目前正在上映贺岁大片(国产凌凌漆,大话西游),共有1000张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

 分析:

(1)3个窗口售票是开启了三个线程,完成各自的任务

(2)3个窗口要多1000张票进行操作(卖票)

实例1

线程类

 

package 卖票;


public class SellTicket extends Thread {
	//这1000张票应该被三个线程共用,所以用static修饰
	//这1000张票不让外界修改数据,所以用private修饰
	private static int ticket=1000;
	
	public SellTicket(String name){
		super(name);
	}
	
	
	@Override
	public void run() {
		//卖票
		while(ticket>0){
			
			System.out.println(Thread.currentThread().getName()+"卖第"+(ticket--)+"张票");
		}
		
	}
}

测试类

 

 

package 卖票;

/**
 * @author Orange
 * @version 1.8
 */
public class SellTicketDemo {
	
	public static void main(String[] args) {
		//创建三个卖票的线程对象
		SellTicket st1 = new SellTicket("售票窗口1");
		SellTicket st2 = new SellTicket("售票窗口2");
		SellTicket st3 = new SellTicket("售票窗口3");
		//开启三个线程
		st1.start();
		st2.start();
		st3.start();
	}
}

 

执行结果:在多线程的环境下,三个线程彼此互不影响,相互抢占CPU来执行该线程的代码;

发现问题:卖票不是逐渐递减的去卖;先卖第1000张,然后卖第998张,与我们的需求有一定的差异

分析原因:每个线程都有一个独立的程序计数器和方法调用栈

程序计数器:称为PC计数器,当线程执行一个方法时,程序计数器指向方法中下一条要执行的字节码文件

说明:只有线程抢到了CPU,程序计数器才会执行指向的代码(其实是字节码文件);换句话说没有抢到就暂时中断,停留在此位置,直到抢到再从此位置开始执行字节码文件。

方法调用栈:简称方法栈,跟踪线程运行过程中的一系列的方法调用过程,每当线程调用一个方法(常见的是在run()方法中调用其他方法),就向方法栈压入一个新桢(栈)

栈桢的组成部分:

(1)局部变量区:存放(调用方法中)局部变量和方法参数

(2)操作数栈:线程工作区,存放当前线程运算过程中的临时数据

(3)栈数据区:为线程指令提供相关的信息。例如:定位位于方法区(回顾方法区的的内容)和堆区的特定数据、正常退出或异常退出的方法

操作数栈:线程的工作区,存放当前线程运算过程中的生成的临时数据

针对本问题:比如说线程1准备卖第400张票时(准备要执行输出这条指令时),但是由于线程2抢到了CPU所以会先输出第399张票,然后第398张票,然后被线程1重新抢到了CPU的执行权,从上一次程序计数器执行的指令开始执行(上次线程1暂停的位置执行,保存第400张票的信息)

捡到的知识:java命令开启了java虚拟机进程时,JVM会创建一个主线程(操作系统完成的),该线程从程序入口main()方法开始执行

 需求2:为了模拟电影院卖票更真实的场景:,每一个窗口卖票应该延迟操作在接口自实现类中,在run()方法中让每一个线程执行睡眠0.1秒

实例2

线程类变化了,其它不变

 

package 卖票;


public class SellTicket extends Thread {
	//这1000张票应该被三个线程共用,所以用static修饰
	//这1000张票不让外界修改数据,所以用private修饰
	private static int ticket=100;
	
	public SellTicket(String name){
		super(name);
	}
	
	/* 模拟该电影院售票情况---无延时
	 * 为了模拟电影院卖票更真实的场景,每一个窗口卖票应该延迟操作
	 * 在接口自实现类中,在run()方法中让每一个线程执行睡眠0.1秒
	 * 
	 * */
	public void run() {
		//卖票
		while(ticket>0){
			try {
				Thread.sleep(100);//加了一个这个
			} catch (InterruptedException e) {
				System.out.println("异常中断");
			}
			//一张票可能会被卖多次,(加入延迟操作之后)---sleep()--一会看看情况
			System.out.println(Thread.currentThread().getName()+"卖第"+(ticket--)+"张票");
		}
		
	}
}

 

出现问题

(1)一张票可能被卖多次(同票)
(2)可能出现负票:0、-1
 原因:延迟操作和线程随机性导致

深入剖析:一种可能的情景;线程1先读取了第1000张准备卖出,但由于睡眠,被线程2抢到了CPU(线程1虽然没有卖出,但保存着第1000张票的信息,一旦线程1被唤醒(timeout)抢占到CPU还是要卖第1000张票),线程2准备卖第1000张票但由于睡眠被线程3抢到,线程3同理;最后线程1先输出第1000张票,此时ticket本应变为999,但是底层尚未在方法区内变为999(底层指令,基本不会出现:大多数情况下变量的改变指令优于其它),线程2还是按照1000张票来输出,此时ticket变为999;

关于负票:一种可能的情景:线程1准备卖第1张票,此时线程2抢到了CPU的执行权,进入了判断的方法(ticket>0);然后线程3也抢到了进入了判断的方法(ticket>0),但是线程1又抢到了,输出票的信息(此时票变成0了),线程2结束,此时线程1执行输出票0,线程3输出票-1

当前线程已经进入了判断的方法(ticket>0),但是被其他线程抢到了,其她线程结束后,而变量也随之改变

----------------------------------------------------------------------------------------

线程的职责是执行一些操作,而大多数的操作都涉及数据的处理;

那么出现上述问题的真正原因是:多个线程对共享的数据(ticket)进行操作,线程1在修改ticket的过程中,线程2也会修改变量ticket,ticket的变量仅仅代表一个临时的结果,变量ticket的值处于不确定的状态

出现的问题:专业术语(线程不安全),原子操作没有封装

原子操作:多条语句操作共享资源的代码的操作(很重要!!!  很重要!!! 很重要!!!)

检验多线程安全问题的标准(以后在判断一个多线程序是否有安全问题的标准)

(1)当前是否处于多线程的环境

(2)多线程环境是否有共享资源

(3)是否有多条语句操作共享资源

解决方案:线程1在操作共享数据的过程,让线程2先等待,操作完后再让线程2在线程1的基础上操作;也即一个线程在操作原子操作的期间,采取措施使其它线程不能操作共享资源(处于阻塞状态)

涉及到同步机制问题了:Java中的同步是指人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确性;通常使用synchronized关键字的方法或者synchronized同步代码块----同步是指得是一个线程完整的执行原子操作(其它线程靠边)

同步机制解决问题:解决共享资源竞争---一个线程在操作共享资源时,其他线程只能望眼欲穿的等待,只有当已经操作完成共享资源的线程执行完同步代码块时,其他线程才有机会操作共享资源----类比上厕所排队(除了忍受等待还有面临抢厕所的问题)

补充:线程的安全优先级(同步)高于性能(并发),所以在保证安全的情况下(结果准确),提高性能;在开发中常见的措施是:在同步代码块中尽量包含较少的原子操作(共享数据的操作),使得一个线程尽快释放锁,减少其他线程等待锁的时间,二者兼顾

有时间了补充一个打水的问题——安全与性能兼顾(二者是一对此消彼长的矛盾)

真正解决上面的问题:将多条语句对共享数据进行操作(原子操作)的代码用同步代码块封装起来
  Java的同步机制的代码形式
  使用同步代码块:synchronized(同步锁对象){
  多条语句对共享数据的操作;
  }
 实例3

线程类

 

package 卖票2;


public class SellTicket implements Runnable {

	private   int ticket=100;
	private Object obj = new Object() ;
	
public void run() {
		
		// 模拟电影院一直有票
		while (true) {
			
			synchronized(obj){//t1进来,门关了,t2和t3线程都不会进来
				
				if (ticket> 0) {
					try {
						Thread.sleep(100) ;
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "正在出售第"
							+ (ticket--) + "张票");
					//窗口1正在出售第100张
					//窗口正在出售第99张
				}
			}//代码执行完毕,t1线程出来,意味着门开了...  
			  
			
			
		}
	}
}

 

测试类

 

package 卖票2;

/**
 * @author Orange
 * @version 1.8
 */
public class SellTicketDemo {

	public static void main(String[] args) {
		//创建线程对象
		SellTicket st = new SellTicket();
		//多个线程对共享数据:st的操作
		Thread thread1 = new Thread(st);
		thread1.setName("售票窗口1");
		Thread thread2 = new Thread(st);
		thread2.setName("售票窗口2");
		Thread thread3 = new Thread(st);
		thread3.setName("售票窗口3");
		//开启线程
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

 

注意:while(true)和synchronized不同的位置时分析(很重要,暂时没有搞懂)

同步锁对象的说明:

(1)同步锁对象可以是什么样的对象?
          可以是Object类型,任意的Java类(自定义)

(2)对象的同步锁是概念上的锁,也称之为以一个以对象为标记的锁

线程之间保持同步的理解:不同的线程在执行同一对象的同步代码块,因此要获得每个对象的锁而相互牵制

同步代码块与同步方法的等价形式(二者的转换,this)----有时间了补充

如果一个方法被synchronized修饰特点:每次只能有一个线程执行此类中的同步方法,只有一个线程哟!!!;只有线程退出同步方法,其它线程才能执行同步方法

线程同步的特征:

(1)同步代码块和非同步代码块操作共享资源,依然会对共享资源竞争。通俗一点:一个线程执行同步代码块,另一个线程可以执行非同步代码块对共享资源竞争

(2)每个对象都有唯一的同步锁

(3)在静态方法前面使用synchronized修饰符,是在类的层面(每个被加载到java虚拟机的方法区的类也有唯一的同步锁),通俗理解:同一时间只有一个线程可以调用它们中的任意方法(静态方法),也即:如果线程1调用它们中的一个方法,另一个线程2试图调用它们当中的方法,线程2挂起,直到线程1执行完静态代码块,线程2被唤醒,执行相应的静态方法

(4)synchronized声明不会被继承;子类重写父类用synchronized修饰的方法,如果子类方法不用synchronized修饰将不会同步,否则你懂的

问题:非静态代码块和静态代码块的区别?

(1)静态方法(被synchronized修饰):在类范围是同步的(理解上:原来所有的实例都可以不通过对象直接访问方法;现在所有的实例在该方法没有去锁的情况,不能去访问其它的静态方法);同一时间内只能有一个线程可以调用它们的任意一个方法,也就是说:如果一个线程访问任意的静态方法,如果有另一个线程中访问(试图)任意的静态方法时,另一个线程会挂起(处于类的锁池中),直到一个线程的静态同步代码块执行完,才会唤醒,执行另一个线程的静态方法-----等于是为类中所有的静态同步方法加了一把锁(静态同步方法共进退)

(2)非静态的方法(被synchronized修饰):在同一个对象是同步的。也就是说这个类的某一个确定对象,线程1调用该对象的同步方法时,线程2试图在该对象上调用某个同步的非静态方法,线程2会挂起(位于对象的锁池中),直到线程1执行完毕同步非静态方法后,线程2被唤醒,执行相应的非静态方法(存在竞争吗?)通俗讲:在一个对象上试图有多个线程调用该方法,只有一个线程会进入该方法,其余线程必须挂起。注意:不同对象调用各自的非静态方法,不会挂起

 

何时使用同步方法?

(1)方法修改关键属性值的时候

(2)方法访问多个属性的值,并根据多个属性的值进行逻辑判断时候

(3)其它情况,视需要而定

万事有利有弊,同步机制弊端
 (1)同步虽然保证了线程的安全性,但同时意味着执行效率低(每一个线程在抢占到CPU的执行权,会去将(门)关闭,别的线程进不来)
 (2)容易出现死锁现象

死锁

概念:当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁,就会发生死锁。通俗讲:相互等待彼此释放锁对象

说明:Jva虚拟机不检测也不试图避免这种情况,所以就成了我们的责任,哎!!!

 

补充:开启了两个线程,两个线程分别持有不同对象的锁,然后一个线程在锁对象的同步代码块中试图访问另一个锁对象造成的

实例5

锁对象

 

package org.westos_02;

public class MyLock {
	//创建两把锁对象
	public static final Object objA = new Object() ;
	public static final Object objB = new Object() ;
}

死锁的线程

 

 

package org.westos_02;

public class DieLock extends Thread {
	
	//定义一个成员变量
	private boolean flag ;
	
	public DieLock(boolean flag){
		this.flag = flag ;
	}
	
	//重写run()方法
	
	@Override
	public void run() {
		//dl1,dl2线程
		if(flag){
			synchronized(MyLock.objA){
				System.out.println("if objA");
				synchronized (MyLock.objB) {
					System.out.println("if objB");
				}
			}//代码执行完毕,objA锁相当于才能被释放掉
		}else {
			//dl2
			synchronized (MyLock.objB) {
				System.out.println("else objB");
				synchronized(MyLock.objA){
					System.out.println("else objA");
				}
			}
		}
		
	}
}

测试类

 

 

package org.westos_02;

public class DieLockDemo {
	
	public static void main(String[] args) {
		
		//创建线程类对象
		DieLock dl1 = new DieLock(true) ;
		DieLock dl2 = new DieLock(false);
		
		//启动线程
		dl1.start() ;
		dl2.start() ;
	}
	
}

 

造成的原因:线程中不通信(处于阻塞状态);由此引出了线程通信的问题(都不主动,总得有个中间人---和事佬来协调--调解)---下面会讲到

问题:如何尽量避免死锁现象?

经验之谈:当几个线程都要访问共享资源A、B、C时,保证每个线程都按照同样的顺序去访问它们,比如先访问A,再访问B,再访问C(不要坏了规矩)

Thread类容易死锁方法

简要说明:这些方法已经被废弃(jdk1.2被废除,但可以使用,不推荐)

(1)suspend()

说明:非静态方法,使运行中线程放弃CPU,暂停运行,但是并不会释放锁对象,死锁根源。

(2)resume()

说明:非静态方法,使暂停的线程恢复运行

这两个方法废除原因(这两个方法的危险性):

        1)容易导致死锁

        2)即使不是死锁,一个线程(线程1)强行中断另一个运行的线程(线程2),会造成另一个线程(线程2)操作的数据停留在逻辑上不合理的状态,导致线程不安全

举例:线程1获得了对象的锁,正在执行一个同步代码块,如果线程2调用了线程1的suspend()方法,线程1会暂停,暂时放弃CPU,但是线程1不会释放锁对象;此时线程2试图访问该锁对象就会产生死锁问题。

废除了总得有替代方案吧,解决方案:

这也是为什么不在Thread类中定义这两个方法,其实Thread类本身已经有类似的两个方法(问题太多)

在Object类中使用wait()方法和notify()方法来替代suspend()和resume()

特点:前者由线程自身执行一个对象的wait()方法,确保处理的数据稳定性,再进入阻塞状态;同时会释放锁对象,避免了死锁

(3)stop()

说明:强制终止一个线程,线程会终止,同时释放锁

废弃的原因:不会造成死锁问题,但会使共享数据停留在不稳定的中间状态

举例:假设线程1获得了对象的锁,执行一个同步代码块;线程2调用线程1的stop()方法,线程1就会终止,线程1在终止之前释放它特有的锁对象;避免了前面我们提到的前两种方法的死锁问题但是如过线程2在调用线程1的stop()方法时,线程1正在执行一个原子操作,会操作共享数据,使共享数据停留在不安全的状态;为了安全起见,只有线程1本身才可以决定何时终止运行

在实际开发中:以编程的方式控制线程-----重点

通俗的讲:一般在受控制的线程中定义一个标志变量,其它线程通过改变变量的值,来控制线程的暂停、恢复运行

线程通信

不同的线程执行不同的任务,任务有联系,线程必须能够通信,协调完成任务,同步问题(锁对象相互牵制问题)

突然想到的:高山流水----并发,一唱一和---同步

Object类中有两个与线程通信有关的方法

(1)线程挂起---wait()

 wait()概述:执行该方法的线程释放锁对象,java虚拟机把该线程放到对象的等待池(阻塞状态),该线程等待其它线程将其唤醒(不会释放锁对象);释放锁对象(在保证安全的前提下,提高了并发性---性能)

(2)线程唤醒---notify()

 notify()概述:执行该方法的线程唤醒在等待池的线程,从对象的等待池中随机选择一个线程放到对象的锁池,待该线程的方法(同步)执行完毕,等待池中随机选择的线程(上帝的选择)就会执行相应的同步代码块-----排队:等待执行该方法的线程执行完毕,再执行随机线程

 注意:如果对象的等待池中没有任何线程(即:没有可以移到对象锁池中的线程),notify()方法什么也不做

(3)线程唤醒---notifyAll()

 notifyAll()方法概述:会把对象等待池中所有的线程转移到对象的锁池中(不太常用)---此举等于让这些线程竞争

  注意:对象的等待池中没有线程,则什么也不做

假设问题:线程1和线程2操作同一个对象t,两个线程通过对象t的wait()和notify()方法进行通信

常见的模型:生产者和消费者模型、银行存储模型

实例4   生产者和消费者模型----"贩卖人口"

Person类

 

package 贩卖人口;

/**
 * @author Orange
 * @version 1.8
 * 说明一点:这两个锁对象都是:同一个对象
 */
public class Person {

	//人的属性:名字和年龄
	private String name;
	private int age;
	private boolean flag;//线程通信的关键
	
	//对人的操作----其它线程类对人执行的任务
	//得到人---生产(人)
	public synchronized void set(String name,int age){
		//首先判断是否有数据,如果有数据该怎么办?没有数据怎么办?
		//如果有数据,通知人贩子来买(让人贩子线程唤醒,此线程睡眠),生产线程处于挂起状态(直到检测到没有数据,则重新生产)
		if(flag){
			try {
				this.wait();//自己暂时停止生产,通知其他线程来消费
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//没有数据,则开始生产数据,同时标识位改变(通知消费者线程消费)
		this.name=name;
		this.age=age;
		flag=true;
		this.notify();
	}
	
	public synchronized void get(){
		//判断是否已经生产了数据
		//(1)没有数据,消费者线程则等待
		if(!flag){
			try {
				this.wait();//没有可消费的数据了,通知生产
			} catch (InterruptedException e) {
				
			}
		}
		//否则消费
		System.out.println("恭喜你购买了年龄为"+age+"的"+name);
		//消费完了,该通知生产了
		flag=false;
		this.notify();
	}
}

生产者线程

package 贩卖人口;

/**
 * @author Orange
 * @version 1.8
 */
public class SetPerson extends Thread {

	private Person student;

	public SetPerson() {

	}

	public SetPerson(Person student) {
		this.student = student;
	}
    private int i;
	@Override
	public void run() {
		//自己没有加while(true)导致执行了一次
		//始终在等待生产数据
		while(true){
			if(i%3==0){
				student.set("高圆圆", 18);
			}else if(i%3==1){
				student.set("陈圆圆", 16);
			}else{
				student.set("宋圆圆", 17);
			}
			i++;
		}
	}
}

消费者线程

 

 

package 贩卖人口;

/**
 * @author Orange
 * @version 1.8
 */
public class GetPerson extends Thread {
	
	private Person student;

	public GetPerson() {

	}

	public GetPerson(Person student) {
		this.student = student;
	}

	@Override
	public void run() {
		//始终在等待获取数据
		while(true){
		  student.get();
		}
	}
}

测试类

 

 

package 贩卖人口;

/**
 * @author Orange
 * @version 1.8
 */
public class Test {

	public static void main(String[] args) {
		//创建两个线程要操作的同一个对象
		Person person = new Person();
		//创建两个线程,来操作对象
		SetPerson sp1 = new SetPerson(person);
		GetPerson sp2 = new GetPerson(person);
		//开启两个线程
		sp1.start();
		sp2.start();
	}
}

 

看懂了,没看懂的话整个流程再过一遍。首先生产者和消费者线程会抢占CPU,我们也不知道谁先抢到;假设消费者线程先抢到,由于判断发现没有数据就会处于静默状态,生产者线程抢到了,生产者线程通过判断发现还没生产数据,就开始生产数据,生产数据后通过改变标志位以及notify()通知消费者线程启动消费线程的任务,消费者线程消费完了修改标志位以及notify()通知生产者开始生产,循环下去。。。;关于生产者线程先抢到,大家可以自行推理,人人都是福尔摩斯

发现问题:在编写代码 过程中,发现没有while(true)只执行了一次,线程就Gameover了;此语句的作用是时刻等着生产和消费

通俗理解(强调n边不为过):两个人共同拥有一把锁(对此锁进行操作),所以同一时刻只有一个人可以拥有锁,其他人只能等待

友情提示:本代码纯属娱乐,不以标题制造舆论,俺是合法公民!!!

实例5 银行存储模型(有时间了补充)

下一章节的内容---涉及到高级属性

线程组

Lock(外部锁)

Callable接口(第三种实现方式)

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值