Skip to content

Latest commit

 

History

History
executable file
·
410 lines (251 loc) · 15.6 KB

File metadata and controls

executable file
·
410 lines (251 loc) · 15.6 KB

Java多线程

1. 基本概念

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

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

对于一个进程(程序)而言,在进程中(程序内部),也会同时运行多个任务,那么每一个任务称为一个线程。一个进程中至少要有一个进程。

开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容就是线程要执行的任务。

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

2. 线程的调度

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

3. 创建线程的两种方式

  1. 继承 Thread类(线程类),其每一个实例(用这个类创建的对象)表示一个可以并发的线程。
  2. 实现Runnable接口(用于发布线程任务的接口)

使用线程的注意事项:

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

4. 阻塞

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

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

  1. 跳秒:

    阻塞时间结束之后,会进入Runnable等待状态,而不是运行状态,有可能在等待时,CPU恰好在执行其他线程,当前线程的打印任务没有被执行,就可能出现跳秒的情况。

5. 线程的停止

  1. run()方法正常执行完毕

  2. run()方法在执行的过程中抛出了未捕获的异常(中断异常)

    当一个线程处于睡眠阻塞时,若被其他线程调用了interrupt()方法中断,则当前的sleep()方法会抛出一个 InterruptedException异常(中断异常)

    // 例如
    class T1 implements Runnable {
        public void run() {
            // 线程任务执行中
            try {
                Thread.sleep(10000L);  // 睡眠阻塞
            } exception InterruptedException  e {
                e.printStackTrace();
            }
        }
    }
    
    Thread t = new Thread(new T1());
    t.interrupt();  // 结束线程t的睡眠

6. 进程的停止

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

  1. 前台线程和后台线程

    • 后台线程

      用法和前台线程无异,只是当进程中所有的前台进程都结束后,无论后台线程是否还在运行当中,都会被强制结束,从而使得进程结束,程序退出。后台线程也称为守护进程、精灵进程。

      可以用对象调用方法setDaemon(true),将一个前台线程变成一个后台线程

程序开始后,会开启一个进程,进程中会创建一个前台线程来运行main()方法的内容,同时也会创建一个后台线程来运行GC(垃圾回收)

7. 线程的优先级

1-10级(1最低、10最高)

优先级越高的线程,被分配时间片段的机会就越多,那么获得CPU执行的机会就越多

可以调用方法void setPriority(int p)类设计线程的优先级。 最高(Thread.MAX_PRIORITY)、最低(Thread.MIN_PRIORITY)、默认(Thread.NORM_PRIORITY,即5级)的优先级都会对应的常量

8. 线程安全

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

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

    临界资源:

    • 多线程共享的实例变量
    • 静态的公共变量
  • 异步

    执行没有先后顺序

  • 同步

    执行有先后顺序

  1. 解决办法

    把异步操作变为同步操作,用一个同步锁(synchronized关键字)

    该关键字可以修饰方法,当一个方法被修饰之后,这个方法就是一个同步方法,同步方法的特点:同一时刻只能有一个线程访问该方法。

9. synchronized块

  1. 同步代码块

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

  2. 语法

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

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

10. 常见的线程安全类

非线程安全 线程安全
StringBuilder StringBuffer
ArrayList Vector
HashMap HashTable
HashSet ......

对于Collection和Map而言,Collections类提供了可以将给定的集合转换为线程安全的集合的方法,例如将ArrayList转换为线程安全的类:Collections.synchronizedList(Collection c)

11. 线程的协同工作

11.1 第一种方式:join()方法

实现线程1执行完,线程2再执行

// t1线程的run()方法
public void run() {
    // 方法体
}
// t2线程的run()方法
public void run() {
    t1.join();
    // t1.join()方法。作用:先执行t1线程,等t1线程执行完毕再执行该方法下面的方法
    // 方法体
}

11.2 第二种方式:wait()和notify()

使用wait()和notify()方法完成线程的协同工作,这两个方法属于Object类。

实现线程1执行完,线程2再执行

Object object = new Object;
// t1线程
public void run() {
    // 代码块
    synchronized (objcet) {
        // 通知object身上的等待的线程的解除
        object.notify();
    }
}
// t2线程
public void run() {
    synchronized (object) {
        // wait()阻塞会在以下情况解除。当其他线程用object调用notify()方法
        object.wait();
        // 代码块
    }
}

12. 线程池

Java1.5之后,提供了一个并发包concurrent,该包里有一个Executors方法,Executors是一个工厂,包含工厂用于创建Executor接口的实例。

// 创建线程池对象,利用其提供的工厂方法
Executor threadPool = Executors.newFixedThreadPool(3);  // 该线程池中有三条线程 
// 将任务交给线程池处理
threadPool.execute(p2);  // p2是一个任务(即实现了Runnable接口的类的实例)

13. 总结

13.1 创建线程的方式

  1. 继承Thread类

    实现步骤

    • 继承Thread类,覆盖run()方法,提供并发运行的过程。
    • 创建该类的实例。
    • 调用start()方法启动线程。
  2. 实现Runnable接口

    实现步骤

    • 创建一个类实现Runnalbe接口,重写run()方法。
    • 创建这个类(任务类)的实例,用这个类作为Thread的构造器参数,创建Thread类对象,交给线程处理。
    • 调用start()方法启动线程。
  3. 使用匿名内部类

    两种方式

    // 方法一
    Thread t = new Thread() {
        public void run() {
            // 方法体
        }
    };
    // 方法二
    Thread t = new Thread(new Runnable() {
    	public void run() {
            // 方法体
        }
    });

13.2 线程的状态

  • new:新建状态,还没有启动

  • Runnable:等待状态,处于可以运行但是还未运行(就绪)

  • Running:运行状态(运行),此时该线程已经获得了CPU。

    • 假设线程获取了CPU,则进入Running状态,开始执行线程体。
    • 假设系统只有一个CPU,那么在任意时间点,只有一个线程处于Running状态。但是当线程数大于处理器的数量,依然会是多条线程在同一CPU轮换执行。
    • 当一条线程开始运行的时候,如果它不是一瞬间完成,那么它不可能一直处于Running状态,线程在执行的过程中会被中断,目的是让其他线程获得执行的机会。像这样的线程调度的策略取决于底层平台。对于抢占式策略的平台来说,系统会给每一个可执行的线程一小段时间来处理任务,当该时间片段用完了,系统会剥夺线程所占的资源(CPU),让其他线程获得执行的机会。
  • Block:阻塞(挂起)状态。

    • 以下情况会发生阻塞状态
    • 线程使用了sleep()方法,主动放弃了CPU资源
    • 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞(比如控制台输入)
    • join()、Object类中的wait()。
    • 当正在执行的线程被阻塞时,其他线程就获得了机会,阻塞结束的时候,该线程进入Runnable等待状态,而非直接进入Running状态。
  • Dead:死亡状态,run()方法执行完,该线程进入死亡状态。不要试图对一个已经死亡的线程调用start()方法,死亡后将不能再次作为线程被执行,会抛出异常

13.3 程序、进程、线程三者的关系

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

13.4 线程的状态管理

  • Thread.sleep(times)

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

  • Thread.yield()

    主动的让出CPU,使线程让出处理器(离开Running),使它进入Runnable等待状态

13.5 线程的常用属性和方法

  1. 线程的优先级

    setPriority(等级1-10,默认是5)

  2. 后台线程(守护线程)

    setDaemon(true),将当前线程变为后台线程

  3. 获取线程的名称

    getName()

  4. 获取当前线程

    Thread.currentThread()

  5. join()

    先执行某个线程(即先执行调用join()方法的线程)

  6. sleep状态的打断和唤醒

    Thread.sleep(times),

    interrupt()方法,打断、中断休眠。

    被唤醒的线程会出现中断异常

13. 6 线程池

Java1.5之后,提供了一个并发包concurrent,该包里有一个Executors方法,Executors是一个工厂,包含工厂用于创建Executor接口的实例。

如果为线程池中的每个线程都分配不同的任务对象,那么即便任务有synchronized,也还是会有重复的东西。可以将一个任务对象分配给多个线程去完成,那么就不会有重复的了。

Java计时器(定时器)

Timer。

  1. 可以安排自动计划的类,每一个任务都是一个线程。
  2. 创建一个Timer实例
  3. timer.schedual(),可以为Timer实例增加计划任务,计划任务是一个抽象类(TimerTask),该抽象类实现了Runnable接口
  4. cancel(),清除计时器对象上的所有计划任务。

例题1:创建一个计时器,在指定时间后开始执行任务

public static void main(String[] args) throws ParseException {
        // 创建计时器对象
        Timer timer = new Timer();
        // 给计时器添加计划任务
        String str = "2019-12-04 17:09:00";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse(str); 
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到了:" + str);
            }
        }, date);
}

例题2:打印倒计时一分钟

public static void main(String[] args) {
		Timer timer = new Timer();
		/*
		 * task:任务
		 * delay:延时,距离计划任务开始的时间间隔
		 * period:间隔多久执行一次计划任务
		 */
		// 三个参数的写法
		timer.schedule(new TimerTask() {
//			long start = new Date().getTime();
			long start = System.currentTimeMillis();
             long end = 60L; // 计时时间
			@Override
			public void run() {
//				long now = new Date().getTime();
				long now = System.currentTimeMillis();
				long pass = now - start;  // 经过了多少毫秒
				long h = pass/1000/60/60;
				long m = pass/1000/60;
				long s = pass/1000;
				if ( (end -s) < 0) {
//					throw new RuntimeException("时间到");
					timer.cancel();
				}
				System.out.println(h + "时" + m + "分" + (end - s) + "秒");
			}	
		}, 0, 1000L);
	}