Day015--java中的多线程

Day015--java中的多线程,第1张

多线程的概述 

我们之前所写的代码(程序)都是单线程的,但是很多时候我们程序往往处理的任务不止一个,而是两个及两个以上。就好比一个司机正在高速公路上开着车,突然,他的手机响了,没有一点点交通安全常识(或总认为自己是独一无二的那个,不会出事情)就拿出来了他的手机接听,于是在这个时候他的大脑就不得不同时做两件事情,开车和接听电话。众所周知,边开车边接电话是不安全的,因为在这个时候人的注意力是在车子和电话之间进行转换的,有些时候如果接电话入迷了,没有注意道路情况就会不可避免的发生交通事故。这样子是不是很危险?是的,这样无疑是很危险的,因此不要去做。----此案例中的司机就是一个进程,而他所做的事情(执行的任务)就称为线程,我们可以看到他在同一时间做的不是一件事(个任务)而是两件事(个任务)---并发,因此他是一个多线程的并发进程。

java中是支持多线程机制的,而什么是线程呢?这得从程序讲起。 

  • 程序:为完成特定任务,用某种语言编写的一组指令的集合(我们写的代码)
  • 进程:运行中的程序, *** 作系统会为该进程分配内存空间,有产生和消亡的动态过程。一个进程内部可以执行多个任务,进程内部的任务就是我们的线程。
  • 线程:由进程产生,是进程的一个实体。(可以将一个结构化的“程序”看作做是一个线程,而线程实际上只是一个完整程序下(进程)的某个执行流程。是“执行环境”或“轻量级程序”)。线程必须拥有父进程。系统没有为线程分配资源,它与进程的其他线程共享该进程的系统资源。

值得注意的是:java中的线程并不是同时进行的,只是cpu在利用不同的时间片段去执行每个线程。因为线程之间的执行控制权转换很快,就造成了“同时运行的错觉”。

就这么光理论上的说说可能有点枯燥,那么我们先使用代码来看看线程的大概运行流程:

(这里涉及的代码就是让大家对多线程有个大概的轮廓)

 我们先通过实现Runnable接口,然后每隔100毫秒打印输出能够被10整除的数

大家看到上面的问题和实现是不是会有一种疑问,我们明明可以直接在main方法中写一个判断输出语句即可,为什么还要多此一举的实现Runable接口,并且还要使用start方法来启动它,以及使用sleep方法来让它休眠?大家想到的是不是下面这种方法,直接就是一个循环判断语句块?

但是相信大家也发现了,这样子的输出是很快的,并且我们在控制台是看不到最开始的数字的。如果我们使用线程,不仅可以让输出减缓,而且也可以编写另外的好几条循环语句,来让他们并发执行。如果我们还是之前的单纯类,没有继承Thread或者是实现Runnable的话,就只能是执行单线程的任务,不能多个语句块同时执行。

接下来我们在线程里面实现我们的任务:为了方便使用start()可以直接让我们的类继承Thread类。

在循环输出能够被10整除的数时,我们再创建一个线程用于循环输出质数

因为我们的系统资源足够,线程优先级没有体现出来。与之类似的还有yield()---礼让。从上面的运行效果我们可以看到两个线程的运行是交替着来的。

public class Control245 extends Thread{
	//在控制台输出可以被10整除的数,每输出一个数字后,实现线程休眠100毫秒
	int startNum=1;
	public void run() {
		while(true) {
			if(startNum%10==0) {
				System.out.println(startNum);
			}
			startNum++;
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
	}
	public static void main(String[] args) {
//       new Thread(new  Control245()).start();    //使用实现Runnable接口的方式在创建对象时,外面再套个Thread的外套之后才能使用start方法
		new Zhi(6).start();
		new Control245().start(); 
		System.out.println("Control245的优先级:"+new Control245().getPriority());
		System.out.println("Zhi的优先级:"+new Zhi(6).getPriority());
	}

}
class Zhi extends Thread{
	public Zhi(int priority) {
		setPriority(priority);    
/*给线程设定优先级只是为了在系统资源紧张的情况下,
JVM会根据线程的优先级决定线程的执行顺序让优先级比较高的线程先执行。
*/
	}
	int  startNum=1;
	boolean flag=true;
	int i;
	public void run() {
		while(true) {
			for(int i=2;i<100;i++) {
				boolean flag=true;
				for(int j=2;j<=i/2;j++) {
					if(i%j==0) {
						flag=false;
						break;}
				}
			if(flag) {
				System.out.println(i+"是质数");
				try {
					Thread.sleep(100);    //线程每输出一句语句就休眠100毫秒
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			}
	}
	}}
线程的生命周期

对于线程的生命周期有几个,众说纷纭,但是说得最多的是六个或者是七个。一般而言我们的线程主要是有七个:

  1. 出生:用户在创建线程时处于的的状态,在使用start方法之前,线程都处于出生状态
  2. 就绪:用户调用start方法后,线程处于就绪状态(可执行状态)
  3. 运行:线程得到系统资源后进入运行状态
  4. 等待:处于运行状态的线程调用了Thread类中的wait方法,可使用notify方法唤醒
  5. 休眠:处于运行状态的线程调用了Thread类中的sleep方法
  6. 阻塞:如果一个线程在运行状态下发出输入/输出请求,输入/输出结束时,线程进入就绪状态。
  7. 死亡:对于阻塞的线程来说,即使系统资源空闲,线程依旧不能回到运行状态,当线程的run方法执行完毕时,线程就进入了死亡状态。

线程的创建

java中有两种方式创建线程,一种是继承Thread类,另外一种是实现Runnable接口。

  • 继承Thread类的缺点:我们都知道在java中类的继承是单继承的,如果已经继承了一个类(有了父类)是不能使用继承Thread类的方式创建的线程的。因此基本上很多时候是推荐使用Runnable来进行创建线程的。
  • 继承Thread类的优点:一般我们在练习(学习)的时候就直接使用的是继承Thread类来创建线程,因为有些时候我们自己创建的类是不需要继承除了Thread类外的其他类的,而且使用继承Thread类的话,我们用start开启一个线程就不需要使用Thread.currentThread().start(),而是可以直接使用Thread.start();即我们就不用去创建一个Thread对象使其包裹我们的类,才能去使用start方法。
 Thread类

 我们完成线程真正功能的代码是放在Thread类的run方法中的,当我们的类继承Thread类后,就可以在该类中覆盖run方法,将实现该线程功能的代码写入run方法中,然后同时调用Thread类中的start方法执行线程,也就是调用覆盖后的run方法。

案例:

创建两个线程,一个用来打印100到1000的水仙花数,另外一个用来打印2000到2022的数

 之后我们到主方法中去启动创建的两个线程

public class DoTask  {
	//创建两个线程,一个用来打印100到1000的水仙花数,另外一个用来打印2000到2022的数
	public static void main(String[] args) {
      new Thread1().start();    //启动线程
      new Thread2().start();   //启动线程
	}

}
class Thread1 extends Thread{
	//打印100到1000的水仙花数
	public  void run (){
		//一般来说线程的run方法里面会放置while死循环(无限循环),使线程一直运行下去,这里我们不需要循环输出
		for(int i=100;i<1000;i++) {
			int hundred=i/100;
			int ten=i%100/10;
			int single=i%100%10;
			if(Math.pow(hundred, 3)+Math.pow(ten, 3)+Math.pow(single, 3)==i) {
				System.out.println(i+"是水仙花数:");
			}
		}
	}}

class Thread2 extends Thread{
	//打印2000到2022的数
	public  void run (){
		for(int i=2000;i<=2022;i++) {
			if(i==2022) {
				System.out.println("现在是"+i+"年");
				continue;
			}
			System.out.println(i+"年");
			
		}
	}
}
Runnable接口

前面我们也提到过:当一个类已经继承了其他类时,就不能再去继承Thread类了,在java中提供了一种Runnable的接口,继承了其他父类的类可以通过实现接口来创建线程。实现了Runnable接口后该类还有一个极大的特点(优点):很适合多个线程共享一个资源的情况(虽然还是会出现资源调用错误,但是解决这个问题只需要后期加锁即可)。多线程共享一个资源最典型的案例就是火车站卖票问题

案例:

三个窗口售卖100张票(有且只有100张票),一旦剩余票数为0输出“票额不足”终止线程

  在上面的效果中我们可以看到卖出的票数是不正确的出现了超卖的现象,如果大家在尝试的时候出现了超卖的话不要惊讶,因为这是正常情况。后期我们可以使用同步机制对其进行完善。

public class SaleTickts {
	public static void main(String[] args) {
     Worker w1=new Worker();   //启动线程
     new Thread(w1).start();;
     new Thread(w1).start();;
     new Thread(w1).start();;
     
	}

}
class Worker implements Runnable{
	//三个窗口售卖100张票,每卖出一张休息一秒。一旦剩余票数为0输出“票额不足”终止线程
	static int tickts=100;
	int count=0;
	public void run() {
		while(true) {
			if(tickts==0) {
				System.out.println(Thread.currentThread().getName()+"票额不足");
				break;
			}
			System.out.println("窗口"+Thread.currentThread().getName()+"卖出一张票,还剩"+(--tickts));
		count++;
		
			try {
			Thread.sleep(5);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		}
		System.out.println(count);
	}
}
线程的控制

线程的控制包括线程的启动,挂起,状态检查,以及如何正确的结束线程,由于在程序中使用多线程,因此需要合理安排线程的执行顺序。

线程的挂起

当一个程序进入“非可执行状态”(即为挂起状态)时,必然存在某种原因使其不能继续运行,这些原因可能有如下几种情况:

  • 调用了sleep方法使线程进入休眠状态,线程在指定时间内不会运行。
  • 调用join方法使线程挂起,如果某个线程在另一个线程t上调用t.join,这个线程将会被挂起,直到线程t执行完毕为止。
  • 调用wait方法使线程挂起,直到线程得到了notify方法或者是notifyAll方法,线程才会进入“可执行”状态。
  • 线程在等待某个输入/输出完成
join方法 

join方法可以让一个线程实现插队的效果。当一个线程插队成功,则肯定先执行完插入的线程所有的任务才会去执行下一个线程的任务。

例如我们上面的的两个打印线程,如果我们想要先执行打印输出年份的线程那么我们可以使用join方法进行插队,如下:

public class DoTask  {
	//创建两个线程,一个用来打印100到1000的水仙花数,另外一个用来打印2000到2022的数
	public static void main(String[] args) {
     Thread2 t2= new Thread2();
     Thread1 t1= new Thread1(t2);
     t1.start();
     t2.start();
	}

}
class Thread1 extends Thread{
	private Thread2 t2;
	public Thread1(Thread2 t2) {
		this.t2=t2;
	}
	//打印100到1000的水仙花数
	public  void run (){
		try {
			System.out.println("t2开始插队运行");
			t2.join();
			System.out.println("t2结束插队退出");
			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//一般来说线程的run方法里面会放置while死循环(无限循环),使线程一直运行下去,这里我们不需要循环输出
		for(int i=100;i<1000;i++) {
			int hundred=i/100;
			int ten=i%100/10;
			int single=i%100%10;
			if(Math.pow(hundred, 3)+Math.pow(ten, 3)+Math.pow(single, 3)==i) {
				System.out.println(i+"是水仙花数:");
			}
		}
	}}

class Thread2 extends Thread{
	//打印2000到2022的数
	public  void run (){
		for(int i=2010;i<=2022;i++) {
			if(i==2022) {  //当i为2022时,修改输出语句
				System.out.println("现在是"+i+"年");
				continue;
			}
			System.out.println(i+"年");
			
		}
	}
}
状态检查 

我们可以使用Thread.currentThread().getState()方法获取到当前线程的状态

线程的优先级

线程的执行顺序其实跟它的优先级关系也不是很大,一般是在资源紧张的时候,系统才会先去执行优先级比较高的线程。 java中的线程的范围可以是整型也可以是变量整型的范围为1~10,如果我们不去设定线程的优先级的话,默认的优先级为5。在Thread类中有3个成员变量分别对应相应的优先级:

  • Thread.MIN_PRIORITY----------优先级为1
  • Thread.MAX_PRIORITY---------优先级为10
  • Thread.NORM_PRIORITY-------优先级为5

线程的守护

线程有两种:一种是用户进程,另外一种是守护进程。

  • 用户进程:也叫工作进程,当线程的任务执行完成或通知方式结束时才结束
  • 守护进程:为工作进程服务,当所有的用户线程结束,守护线程自动结束

创建两个线程,一个循环打印0到10的数字,一个循环打印100到150的数字,将循环打印100到150的数字的线程设置为守护线程

public class TestDeamon {
	//创建两个线程,一个只打印0到10的数字,一个循环打印100到120的数字,
	//将循环打印100到120的数字的线程设置为守护线程
	public static void main(String[] args) {
       Workers worker=new Workers();
       Daemoners daemoner=new Daemoners();
       daemoner.setDaemon(true);     //使用setDaemon方法将线程设置为守护进程
       daemoner.start();
       worker.start();
	}

}
class Workers extends Thread{
	public void run() {
		for(int i=0;i<=10;i++) {
			System.out.println(i);
		}
	}
}

class Daemoners extends Thread{
	public void run() {
		while(true) {
			for(int i=100;i<=150;i++) {
				System.out.println(i);
			}
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
	
}
 线程的同步机制

如果程序是单线程的,我们在执行的时候是不用担心会有其他的线程来打扰我们的线程,但是如果我们使用的是多线程的话,就会出现共享资源上的竞争。就好比有两个人都吃坏了肚子,窜稀的那种,特别急切的想要上厕所,但是,很不巧只有一个厕所,这个时候就出现了共享资源发生冲突的情况,到底谁先上呢?此时就需要进行控制,否则容易阻塞(如果一直阻塞,线程获取锁失败可能会直接导致程序死亡),于是有了厕所门上的锁。在java中也有锁的概念,我们只需要将会发生冲突的共享资源上锁即可。刚刚的那两个人中,会是谁来给厕所上锁呢?毫无疑问是跑得最快的那个。在我们的java中也一样,率先访问到资源的第一个线程来为资源上锁,其他线程如果想要使用这个资源就需要等到锁解除,第二个线程获取锁成功后就会为该资源上锁。

java中的上锁可以使用同步机制(synchronized)来完成。

注释:同步机制---指两个线程同时作用在一个对象时,应该保持对象数据的统一性和整体性

共享资源一般是文件,输入/输出端口,或者是打印机。

 通常将 *** 作共享资源的代码放入synchronized定义的区域内,这样当其他线程也获取到这个锁时,必须等待锁别释放时,才能进入该区域。java中有两种方式使用同步机制进行上锁:

  • 同步代码块:形如→synchronized(Object/this){ 要同步的代码  }
  • 同步方法:形如→public synchronized void 方法名(){要同步的代码}

案例依旧是之前我们的火车站卖票,现在我们对其使用同步机制,给需要同步的代码上锁

 使用同步代码块进行上锁  使用同步方法上锁

public class SaleTickts {
	public static void main(String[] args) {
     Worker w1=new Worker();   //启动线程
     new Thread(w1).start();;
     new Thread(w1).start();;
     new Thread(w1).start();;
     
	}

}
class Worker implements Runnable{
	//三个窗口售卖100张票,每卖出一张休息一秒。一旦剩余票数为0输出“票额不足”终止线程
	static int tickts=100;
	int count=0;
	boolean flag=false;
	public synchronized void sell() {    //使用同步方法
		if(tickts==0) {
			System.out.println(Thread.currentThread().getName()+"票额不足");
			//break;     //不能使用break
			flag=true;    //标志变量
			return;      //可以使用return来返回,但是后期需要进行处理
		
		}
		System.out.println("窗口"+Thread.currentThread().getName()+"卖出一张票,还剩"+(--tickts));
	count++;
	
		try {
		Thread.sleep(5);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
		System.out.println(count);
	}
	

	public void run() {
		while(true) {
			sell();
			if(flag) {     //判断语句,如果旗帜立起来时代表票额不足,退出循环
				break;
			}
}}}
水池排注水问题

public class Pool {       				 //创建水池类 
	static Object obj=new Object();      //在水池类里面创建超类,方便其他类来访问水池类
	static int totalWater=10;            //假设水池可以容纳10升的水
	static int haveWater=2;              //因为连续一个月的下雨,水池中现在有2升的水
	static int outWater=0;               //没有人去排水  
	public static void main(String[] args) {
		new Thread(new OutWater()).start();
		new Thread(new InWater()).start();
	}

}


class OutWater implements Runnable{         				//创建排水类
	public void outWaterMethod()  {     				//创建排水的方法
		synchronized(Pool.obj) {					//因为会有两个线程对水池的totalWater进行访问,因此我们上锁
			System.out.println("水池没水→"+isEmpty());
			if(isEmpty()) {
				try {
					Pool.obj.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("水池为空,不用排水,您可以休息。");}
			else {
				System.out.println("水池有水,请及时排掉。");
				Pool.outWater++;    //让水池开始排水
				System.out.println(Thread.currentThread().getName()+"已近排掉了"+Pool.outWater+"还剩"+(Pool.haveWater-Pool.outWater));
			
			}
		}
	}
	
	public boolean isEmpty() {						       //创建判断水池是否为空的方法
		return Pool.haveWater==Pool.outWater?true:false;   //如果水池中有多少水就排多少水,证明我们的水池是空的状态
	}
	public void run() {    									//线程的功能实现
		while(Pool.haveWater

 如果大家对于上面的代码有什么更好的意见或者是更优解的话欢迎在评论区留言哈

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/876640.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)