Java简单基础的复习(十六)线程

多线程的概述

  1. 什么是线程
    • 线程是程序执行的一条路径, 一个进程中可以包含多条线程
    • 多线程并发执行可以提高程序的效率, 可以同时完成多项工作
  2. 多线程并行和并发的区别
    • 并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
    • 并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
  3. Java程序运行原理和JVM的启动是多线程的吗
    • Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。
    • JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的

多线程的实现

  1. 继承Thread
    • 定义类继承Thread
    • 重写run方法
    • 把新线程要做的事写在run方法中
    • 创建线程对象
    • 开启新线程, 内部会自动执行run方法
class MyThread extends Thread {				//1,定义类继承Thread
				public void run() {														
					//2,重写run方法
					for(int i = 0; i < 3000; i++) {	
						//3,将要执行的代码,写在run方法中
					}
				}
}				
  1. 实现Runnable
    • 定义类实现Runnable接口
    • 实现run方法
    • 把新线程要做的事写在run方法中
    • 创建自定义的Runnable的子类对象
    • 创建Thread对象, 传入Runnable
    • 调用start()开启新线程, 内部会自动调用Runnable的run()方法
	public class Demo3_Runnable {
		/**
		 * @param args
		 */
		public static void main(String[] args) {
			MyRunnable mr = new MyRunnable();//4,创建自定义类对象						
			//Runnable target = new MyRunnable();
			Thread t = new Thread(mr);//5,将其当作参数传递给Thread的构造函数
			t.start();//6,开启线程			
			for(int i = 0; i < 3000; i++) {
				System.out.println("bb");
			}
		}
	}
	
	class MyRunnable implements Runnable {//1,自定义类实现Runnable接口
		@Override
		public void run() {	//2,重写run方法
			for(int i = 0; i < 3000; i++) {	//3,将要执行的代码,写在run方法中
				System.out.println("aaaa");
			}
		}
		
	}
  1. 实现Runnable的原理
    • 看Thread类的构造函数,传递了Runnable接口的引用
    • 通过init()方法找到传递的target给成员变量的target赋值
    • 查看run方法,发现run方法中有判断,如果target不为null就会调用Runnable接口子类对象的run方法
  2. 两种方式的区别
    • 继承Thread : 由于子类重写了Thread类的run(), 当调用start()时, 直接找子类的run()方法
      • 好处是:可以直接使用Thread类中的方法,代码简单
      • 弊端是:如果已经有了父类,就不能用这种方法
    • 实现Runnable : 构造函数中传入了Runnable的引用, 成员变量记住了它, start()调用run()方法时内部判断成员变量Runnable的引用是否为空, 不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法
      • 好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的
      • 弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂
  3. 匿名内部类实现
  • 继承Thread类
new Thread() {//1,new 类(){}继承这个类
			public void run() {	//2,重写run方法
				//3,将要执行的代码,写在run方法中
				for(int i = 0; i < 3000; i++) {
					System.out.println("aaaaaaa");
				}
			}
		}.start();
  • 实现Runnable接口
new Thread(new Runnable(){//1,new 接口(){}实现这个接口
		public void run() {//2,重写run方法
			//3,将要执行的代码,写在run方法中
			for(int i = 0; i < 3000; i++) {
				System.out.println("bb");
			}
		}
	}).start(); 

Thread类

  1. 构造方法:
    • public Thread() :分配一个新的线程对象。
    • public Thread(String name) :分配一个指定名字的新的线程对象。
    • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
    • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

Thread类的常用方法

  1. 获取名字
    • 通过getName()方法获取线程对象的名字
			new Thread("lwb") {
				public void run() {
					for(int i = 0; i < 1000; i++) {
						System.out.println(this.getName());
					}
				}
			}.start();
			
			new Thread("zjf") {
				public void run() {
					for(int i = 0; i < 1000; i++) {
						System.out.println(this.getName());
					}
				}
			}.start(); 
  1. 设置名字
    • 通过构造函数可以传入String类型的名字
Thread t1 = new Thread() {
				public void run() {
					for(int i = 0; i < 1000; i++) {
						System.out.println(this.getName());
					}
				}
			};
			
			Thread t2 = new Thread() {
				public void run() {
					for(int i = 0; i < 1000; i++) {
						System.out.println(this.getName());
					}
				}
			};
			t1.setName("lwb");
			t2.setName("zjf");
			
			t1.start();
			t2.start();
  1. 获得当前线程的对象
    • Thread.currentThread(), 主线程也可以获取
new Thread(new Runnable() {
				public void run() {
					for(int i = 0; i < 1000; i++) {
						System.out.println(Thread.currentThread().getName());
					}
				}
			}).start();
			
			new Thread(new Runnable() {
				public void run() {
					for(int i = 0; i < 1000; i++) {
						System.out.println(Thread.currentThread().getName());
					}
				}
			}).start();
			Thread.currentThread().setName("我是主线程");//获取主函数线程的引用,并改名字
			System.out.println(Thread.currentThread().getName());//获取主函数线程的引用,并获取名字
  1. 休眠线程
    • Thread.sleep(毫秒,纳秒), 控制当前线程休眠若干毫秒1秒= 1000毫秒 1秒 = 1000 * 1000 * 1000纳秒 1000000000
new Thread("java") {
				public void run() {
					for(int i = 0; i < 10; i++) {
						System.out.println(getName());
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}.start();
			
			new Thread("c++") {
				public void run() {
					for(int i = 0; i < 10; i++) {
						System.out.println(getName());
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}.start();
  1. 守护线程
    • setDaemon(), 设置一个线程为守护线程, 该线程不会单独执行, 当其他非守护线程都执行结束后, 自动退出
Thread t1 = new Thread("守护线程") {
				public void run() {
					for(int i = 0; i < 50; i++) {
						System.out.println(getName());
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			};
			
			Thread t2 = new Thread("非守护线程") {
				public void run() {
					for(int i = 0; i < 5; i++) {
						System.out.println(getName());
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			};
			
			t1.setDaemon(true);						//将t1设置为守护线程
			
			t1.start();
			t2.start();
  1. 加入线程
    • join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续
    • join(int), 可以等待指定的毫秒之后继续
final Thread t1 = new Thread("最初线程") {
				public void run() {
					for(int i = 0; i < 50; i++) {
						System.out.println(getName());
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			};
			
			Thread t2 = new Thread("加入线程") {
				public void run() {
					for(int i = 0; i < 50; i++) {
						if(i == 2) {
							try {
								//t1.join();//插队,加入
								t1.join(30);//加入,有固定的时间,过了固定时间,继续交替执行
								Thread.sleep(10);
							} catch (InterruptedException e) {
								
								e.printStackTrace();
							}
						}
						System.out.println(getName());
					
					}
				}
			};
			
			t1.start();
			t2.start();
  1. 礼让线程
    • yield让出cpu
  2. 设置线程优先级
    • setPriority()设置线程的优先级

同步代码块和同步方法

  1. 同步代码块
    • 何时需要使用同步代码块:
      • 当多线程并发, 有多段代码同时执行时, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步.
      • 如果两段代码是同步的, 那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码.
    • 同步代码块:
      • 使用synchronized关键字加上一个锁对象(可以是任意的)来定义一段代码, 这就叫同步代码块
      • 多个同步代码块如果使用相同的锁对象, 那么他们就是同步的
class Printer {
				Object o = new Object();
				public static void print1() {
					synchronized(o){//锁对象可以是任意对象,但是被锁的代码需要保证是同一把锁,不能用匿名对象
						System.out.print("a");
						System.out.print("a");
						System.out.print("\r\n");
					}
				}
	
				public static void print2() {	
					synchronized(o){	
						System.out.print("b");
						System.out.print("b");
						System.out.print("\r\n");
					}
				}
			}
  1. 同步的方法:
    • 使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的
class Printer {
			public static void print1() {
				synchronized(Printer.class){//锁对象可以是任意对象,但是被锁的代码需要保证是同一把锁,不能用匿名对象
					System.out.print("a");
					System.out.print("a");
					System.out.print("\r\n");
				}
			}
			/*
			 * 非静态同步函数的锁是:this
			 * 静态的同步函数的锁是:字节码对象
			 */
			public static synchronized void print2() {	
				System.out.print("b");
				System.out.print("b");
				System.out.print("\r\n");
			}
		}
  1. 线程安全问题
    • 多线程并发操作同一数据时, 就有可能出现线程安全问题
    • 使用同步技术可以解决这种问题, 把操作数据的代码进行同步, 不要多个线程一起操作
			public class Demo2_Synchronized {

				/**
				 * @param args
				 * 需求:铁路售票,一共100张,通过四个窗口卖完.
				 */
				public static void main(String[] args) {
					TicketsSeller t1 = new TicketsSeller();
					TicketsSeller t2 = new TicketsSeller();
					TicketsSeller t3 = new TicketsSeller();
					TicketsSeller t4 = new TicketsSeller();
					
					t1.setName("窗口1");
					t2.setName("窗口2");
					t3.setName("窗口3");
					t4.setName("窗口4");
					t1.start();
					t2.start();
					t3.start();
					t4.start();
				}
			
			}
			
			class TicketsSeller extends Thread {
				private static int tickets = 100;
				static Object obj = new Object();
				public TicketsSeller() {
					super();
					
				}
				public TicketsSeller(String name) {
					super(name);
				}
				public void run() {
					while(true) {
						synchronized(obj) {
							if(tickets <= 0) 
								break;
							try {
								Thread.sleep(10);//线程1睡,线程2睡,线程3睡,线程4睡
							} catch (InterruptedException e) {
								
								e.printStackTrace();
							}
							System.out.println(getName() + "...这是第" + tickets-- + "号票");
						}
					}
				}
			}

Runtime类

  1. Runtime类是一个单例类
Runtime r = Runtime.getRuntime();
//r.exec("shutdown -s -t 300");		//300秒后关机
r.exec("shutdown -a");				//取消关机

Timer类

  1. Timer类:计时器
public class Demo5_Timer {
				/**
				 * @param args
				 * 计时器
				 * @throws InterruptedException 
				 */
				public static void main(String[] args) throws InterruptedException {
					Timer t = new Timer();
					t.schedule(new MyTimerTask(), new Date(114,9,15,10,54,20),3000);
					
					while(true) {
						System.out.println(new Date());
						Thread.sleep(1000);
					}
				}
			}
			class MyTimerTask extends TimerTask {
				@Override
				public void run() {
					System.out.println("起床敲代码");
				}
				
			}

线程状态

  1. 线程的六种状态
    在这里插入图片描述
  2. 状态的相互转换
    在这里插入图片描述

线程通信

  1. 概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
  2. 为什么要处理线程间通信:
    • 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
  3. 如何保证线程间通信有效利用资源:
    • 多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制

等待唤醒机制

  1. 什么是等待唤醒机制
    • 这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
    • 就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
    • wait/notify 就是线程间的一种协作机制。
  2. 等待唤醒中的方法
    • 等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
      • wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象
        上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
      • notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
      • notifyAll:则释放所通知对象的 wait set 上的全部线程。
    • 注意:
      • 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
    • 总结如下:
      • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成BLOCKED 状态
  3. 调用wait和notify方法需要注意的细节
    • wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
    • wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
    • wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
  4. 生产者和消费者案例(吃包子和包子铺)
  • 思路分析
    在这里插入图片描述
  • 创建一个包子的类
public class Baozi {
	public String pi;
	public String xian;
	public boolean flag = false;//判断是否有包子

}
  • 创建一个包子铺用来生产包子
public class Baozipu extends Thread {
	private Baozi bz;
	
	public Baozipu(String name,Baozi bz) {
		super(name);
		this.bz=bz;
	}

	@Override
	public void run() {
		int count = 0;
		//制造包子
		while(true) {
			//同步
			synchronized (bz) {
				//如果有包子
				if(bz.flag == true) {
					//等待包子卖出去
					try {
						bz.wait();
					} catch (InterruptedException e) {
						// TODO 自动生成的 catch 块
						e.printStackTrace();
					}
				}
				//如果没有包子
				System.out.println("包子铺正在准备包子");
				//制造包子
				bz.pi = "薄皮";
				bz.xian = "牛肉馅";
				count++;
				//改变包子的属性
				bz.flag = true;
				System.out.println("包子做好了:"+bz.pi+bz.xian+"的包子");
				System.out.println("快来买新鲜的包子!!");
				//等待线程被唤醒
				bz.notify();
			}
		}
	}
	
	

}
  • 创建一个路人来吃包子
public class Luren extends Thread {
	private Baozi bz;

	public Luren(String name,Baozi bz) {
		super(name);
		this.bz = bz;
	}

	@Override
	public void run() {
		//使用死循环模拟
		while(true) {
			//同步代码块
			synchronized (bz) {
				//如果没有包子
				if(bz.flag == false) {
					//就让路人等待,让包子铺去制作包子
					try {
						bz.wait();
					} catch (InterruptedException e) {
						// TODO 自动生成的 catch 块
						e.printStackTrace();
					}
				}
				System.out.println(getName()+"正在吃"+bz.pi+bz.xian+"的包子");
				try {
					sleep(1000);
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
				System.out.println("---------------------------------");
				//包子吃完了,让包子铺做
				bz.flag=false;
				bz.notify();
			}
		}
	}
	
	
	
	

}

  • 最后创建一个测试类测试一下
public class test {

	public static void main(String[] args) {
		Baozi bz = new Baozi();
		
		Baozipu bzp = new Baozipu("包子铺", bz);
		Luren lr = new Luren("lwb", bz);
		
		bzp.start();
		lr.start();

	}

}
  • 结果
    在这里插入图片描述

线程池

  1. 线程池创建的原因
    • 我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
      • 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
    • 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
      • 在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。
  2. 线程池概念
    • 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
      在这里插入图片描述
    • 合理利用线程池能够带来三个好处:
      1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
      2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
      3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
  3. 线程池使用
    • Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
    • 要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
    • Executors类中有个创建线程池的方法如下:
      • public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
      • 获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
        • public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
    • 使用线程池中线程对象的步骤:
      1. 使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
      2. 创建一个类,实现Runnable接口,重写run方法,设置线程任务
      3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
      4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
public class ThreadPool {
    public static void main(String[] args) {
        //1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
        //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
        es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程执行

        //4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
        es.shutdown();

        es.submit(new RunnableImpl());//抛异常,线程池都没有了,就不能获取线程了
    }

}
/*
    2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
 */
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值