10.20-多线程

进程:直译就是正在进行当中的程序,在操作系统中同时运行的每一个程序都是进程

线程:就是进程中的一个负责程序执行的控制单元(执行路径),一个进程中可以有多个执行路径,称之为多线程。

对于一个进程(程序)而言,在程序内部也会同时运行多个任务,那么每一个任务都是由一个线程来控制执行的,一个进程中至少要有一个线程,开启多线程是为了在同一个程序中同时运行多部分代码,每一个线程都由自己运行的内容,这个内容就是线程要执行的任务

线程的运行都是并发执行的;所谓的并发从宏观上看所有的线程都是同时运行的,但从微观上看所有的线程都是走走停停

线程调度

将CPU的时间划分为若干个时间片段,尽可能均匀的分配给每一个线程,获取CPU时间片段的线程将得以被CPU执行

创建线程的两种方式

  1. 继承Thread类(线程类),该类的每一个实例表示可以并发的线程

  2. 实现Runnable接口(用于发布线程任务的接口)

例子-继承Thread类创建

package day10_20;

public class ThreadDemo01 {
	/**
	 * 创建线程的第一种方式:继承Thread类
	 * 弊端:线程和线程要干的事情(任务)耦合到了一起
	 * 使用多线程时,不应当考虑执行先后顺序问题
	 * 执行没有先后顺序的,叫异步运行(并发运行)
	 * 执行有先后顺序的,叫同步运行
	 * @param args
	 */
	public static void main(String[] args) {
		//创建线程对象
		Thread t1 = new Person1();
		Thread t2 = new Person2();
		/*
		 * 启动线程调用的是start()方法,而并不是run()方法
		 */
		t1.start();
		t2.start();
		/*
		 * t1与t2的执行没有任何的先后顺序,
		 * 线程调度在给线程分配时间片段后,
		 * 并没有将某个线程一次性执行完毕。
		 * 并且分配的时间片段长短也不是一定均匀
		 */
	}
}
/*
 * 该类继承Thread线程类,Thread类的子类就可以作为线程类来使用
 * 该类的每一个实例都是一个可以并发的线程
 */
class Person1 extends Thread{
	@Override
	public void run() {
		for(int i=0;i<1000;i++) {
			System.out.println("Who are you");
		}
	}
}
class Person2 extends Thread{
	@Override
	public void run() {
		for(int i=0;i<1000;i++) {
			System.out.println("开门,社区查水表");
	}
}
}

例子-实现Runnable接口创建

package day10_20;

public class ThreadDemo02 {
	/**
	 * 创建线程的第二种方式(实现Runnable接口)
	 * 线程要执行的任务可以定义在该接口的实现类当中
	 * 优点:将任务与线程解耦(线程与任务分离)
	 */
	public static void main(String[] args) {
		//创建任务实例
		Runnable r1 = new Runnable1();
		Runnable r2 = new Runnable2();
		//创建线程对象时,将r1这个任务指派给t1这个线程来执行
		Thread t1 = new Thread(r1);
		Thread t2 = new Thread(r2);
		//启动线程
		t1.start();
		t2.start();
	}
	
}
/*
 * Runnable1实现了Runnable,
 * 那么该类就是任务类,用于定义线程要执行的任务
 */
class Runnable1 implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<1000;i++) {
			System.out.println("你是谁?");
		}
	}
}
class Runnable2 implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<1000;i++) {
			System.out.println("收电费!");
		}
	}
}

使用线程的注意事项

对于线程调度而言,分配的时间片段长短,具体分配给哪一个时间片段,对于程序而言都是不可控的。

线程的阻塞

线程在执行某段逻辑时,可能会发生阻塞现象

sleep阻塞:该阻塞可以指定阻塞时间,并在线程阻塞了该时间后自动返回Runnable等待状态,并不是直接进入 Running运行状态,Thread类提供了一个静态方法sleep(),该方法就是用于睡眠阻塞的

例子-sleep阻塞线程

package day10_20;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 睡眠阻塞
 *
 */
public class SleepBlockDemo {
	public static void main(String[] args) {
		//创建任务类实例
		SleepBlock sb = new SleepBlock();
		//指派给线程执行
		Thread t1 = new Thread(sb);
		//启动线程
		t1.start();
		
	}
}
/*
 * 定义任务类
 * 每间隔一秒的在控制台输出当前系统时间(HH:mm:ss)
 * 跳秒:
 * 线程阻塞时间结束之后会进入Runnable等待状态,
 * 并不是直接进入Running状态,有可能在等待时CPU恰好在执行其他线程,导致当前线程的打印任务没有被执行时就有可能出现跳秒的情况
 */
class  SleepBlock implements Runnable{
	@Override
	public void run() {
		SimpleDateFormat sdf = 
				new SimpleDateFormat("HH:mm:ss");
		while(true) {
			System.out.println(sdf.format(new Date()));
			//每输出一次系统事件后停止一秒
			try {
				Thread.sleep(1000);//毫秒
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
	}
}

线程的停止

  1. run方法正常执行完毕

  2. run方法在执行过程中抛出了中断异常InterruptedException

例子-主动中断

package day10_20;

import javax.swing.plaf.basic.BasicTreeUI.TreeCancelEditingAction;

//中断异常
public class InterruptedExceptionDemo01 {
	public static void main(String[] args) {
		/*
		 * 表演者:蔡老师
		 * 处于睡眠阻塞的线程
		 */
		Thread cxk = new Thread() {
			@Override
			public void run() {
				System.out.println("刚打完篮球,有点累歇歇");
				try {
					Thread.sleep(1145141918);
				} catch (InterruptedException e) {
						System.out.println("你干嘛~哎呦~");
				}
				
			}
		};
		
		/*
		 * 表演者:张老师
		 * 中断睡眠阻塞的线程
		 */
		Thread zs = new Thread() {
			public void run() {
				System.out.println("起来吧你!");
				for(int i=0;i<5;i++) {
					System.out.println("duang~");
					try {
						/*
						 * 当一个线程处于睡眠阻塞时,
						 * 若被其他线程调用了interrupt方法中断
						 * 则sleep方法就会抛出中断异常
						 */
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				//中断cxk的睡眠阻断
				cxk.interrupt();
				System.out.println("搞定");
			}
		};
		//启动线程
		cxk.start();
		zs.start();
	}
}

进程的停止

当一个进程中所有的前台线停止后,该进程就结束前台线程和后台线程

后台线程的特点:用法上与前台线程无异,只是当一个进程所有的前台线程都结束后,

无论线程是否还在运行当中都会强制结束,从而使进程结束

例子-线程守护

package day10_20;

public class DaemonThreadDemo {
	public static void main(String[] args) {
		//前台线程Rose
		Thread rose = new Thread() {
			public void run() {
				for(int i=0;i<5;i++) {
					System.out.println("Let me Jump!");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				System.out.println("落水");
			}
		};
		
		//后台线程Jack
		Thread jack = new Thread() {
			@Override
			public void run() {
				while(true) {
					System.out.println("You jump,I jump");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		//启动线程
		jack.setDaemon(true);
		rose.start();
		jack.start();
	}
}

优先级

线程的优先级为1-10

优先级越高,分配时间片段的机会也就更多,获得CPU执行的时间也就更多

例子-优先级设置

package day10_20;
//线程的优先级
public class ThreadPriorityDemo {
	public static void main(String[] args) {
		//最高
		Thread max = new Thread() {
			@Override
			public void run() {
				for(int i=0;i<1000;i++) {
					System.out.println("max");
				}
			}
		};
		//最低
		Thread min = new Thread() {
			public void run() {
				for(int i=0;i<1000;i++) {
					System.out.println("min");
				}
			}
		};
		//默认
		Thread def = new Thread() {
			public void run() {
				for(int i=0;i<1000;i++) {
					System.out.println("def");
				}
			}
		};
		/*
		 * void setPriority(int p)
		 * 设置当前线程的优先级
		 * 最高、最低、默认都有对应的常量表示
		 */
		max.setPriority(Thread.MAX_PRIORITY);
		min.setPriority(Thread.MIN_PRIORITY);
		def.setPriority(Thread.NORM_PRIORITY);//默认就为Norm,可以省略不写
		/*
		 * 即使设置优先级,也无法百分百控制线程调度
		 * 只是最大程度告知调度以更多的几率分配分配时间片段给优先级高的线程
		 */
	}
}

线程安全

多线程并发访问同一个资源,会产生线程安全问题

解决办法:把异步操作变为同步操作

  1. 多线程并发读写同一临界资源会发生"线程安全并发问题",如果保证多线程同步访问临界资源,就可以解决线程安全问题

  2. 常见的临界资源

    • 多线程共享的实例变量

    • 静态的公共变量

  3. 异步:执行没有先后顺序

    • 同步:执行由先后顺序

  4. 同步锁:synchronized关键字

    • synchronized可以修饰方法,当一个方法被该关键字修饰之后,这个方法就是一个同步方法,在同一个时间只能有一个线程访问该方法

synchronized块

当一个方法被synchronized修饰后,该方法就变为了同步方法,虽然保证了代码的执行安全,但是效率比较低,我们实际上只需要将方法中需要同步的代码片段加锁即可,这样可以缩小同步范围,从而提高代码的运行效率

语法

synchronized(同步监视器){
    需要同步的代码片段
}

同步监视器:就是一个对象,任何对象都可以,但是要保证一点,多线程看到的应该是"同一个"对象,通常情况下使用this就可以

线程不安全

  • StringBuilder

  • ArrayList

  • HashMap

  • HashSet

  • .....

线程安全

  • StringBuffer

  • Vector

  • HashTable

  • ......

对于集合(Collection)和Map而言,Collections集合工具类中提供了可以将给定集合转换为线程安全集合的方法

线程的协同工作

线程池

Java1.5以后提供了并发包concurrent

Executors是工厂,包含工厂用于创建Executor接口的实例

总结

创建线程的方式

  1. 继承Thread类

    1. 创建一个类继承Thread类,覆盖run()方法,提供并发运行的过程

    2. 创建这个类(子类)实例

    3. 使用start()方法启动线程

  2. 实现Runnable接口

    1. 创建一个类实现Runnable接口,覆盖run()方法,提供并发运行的过程

    2. 创建这个类(实现类/任务类)的实例,用这个实例作为Thread类的构造器参数,创建Thread类的实例

    3. 使用start()方法启动线程

  3. 匿名内部类

    1. Thread t =new Thread(){
      	public void run(){
      	并发的过程
      	}
      };
      t.start();
    2. Runnable r = new Runnable(){
          public void run(){
              //并发的过程
          }
      };
      Thread t = new Thread(r);
      t.start();

线程的状态

  1. new 新建状态,还未启动

  2. Runnable 等待状态,可以运行的状态(就绪)

  3. Running 运行状态,该线程已经获取了CPU

    • 假设线程获取了CPU,则进入Running状态,开始执行线程体(run()方法)

    • 如果系统只有一个CPU(单核心),那么在任意时间点只有一个线程处于Running状态;如果是双核心,那么任意时间点有两条线程处于Running状态;但是当线程数大于处理器数量时,依然会是多条线程在同一CPU中轮换执行

    • 当一条线程开始运行的时候,如果它不是一瞬间完成,线程在执行的过程中会被中断,目的是为了让其他线程获得被执行的机会,像这样的线程调度策略取决于底层平台;对于抢占式策略的平台来说,系统会给每一个可执行的线程一小段时间来处理任务;当时间用完时,系统会剥夺线程所占用的资源(CPU),让其他线程获得运行的机会

  4. Block 阻塞状态(挂起状态)

    • 以下情况会发生阻塞状态

      • 线程使用了sleep()方法,主动放弃了CPU资源

      • 线程中调用了阻塞式IO方法(比如控制台输入),在该方法返回前,该线程被阻塞

      • join/Object类中的wait()

    • 当正在执行的线程被阻塞时,其他线程就获得了机会。阻塞结束时,该线程将进入Runnable等待状态,而并非直接进入Running 运行状态

  5. Dead 死亡状态 当run()方法执行结束,该线程就进入死亡状态

    • 不要试图对于一个已经死亡的线程再次调用start()方法,线程死亡后不能再次作为线程来执行,会抛出异常

程序、进程、线程三者之间的关系

程序是由一个或多个进程组成的,进程又是由一个或多个线程组成的

线程的状态管理

  1. Thread.sleep(long times)

    • 使当前线程从Running放弃处理器,进入Block状态休眠times毫秒,再返回Runnable状态,如果其他线程调用interrupt()方法中断了当前线程的Block就会发生中断异常(InterruptedException)

  2. Thread.yield()

    • 主动的让出CPU,使当前线程进入Runnable等待状态

线程常用的属性与方法

  1. 线程的优先级

    • setPriority(等级) 默认等级为5

  2. 后台线程

    • setDaemon(true)

  3. 获取线程的名称

    • getName()

  4. 获取当前线程

    • Thread.currentThread()

  5. join() 阻塞

  6. sleep状态的打断与唤醒

    • sleep() 进入休眠状态

    • interrupt() 中断休眠

最后更新于