进程:就是正在进行中的程序,在操作系统中,同时运行的每一个程序都是一个进程。
线程:就是进程中一个负责程序执行的控制单元(执行路径),一个进程中可以有多个执行路径,称之为多线程
对于一个进程(程序)而言,在进程中(程序内部),也会同时运行多个任务,那么每一个任务称为一个线程。一个进程中至少要有一个进程。
开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容就是线程要执行的任务。
线程的运行都是并发执行的,所谓的并发:宏观上所有的线程都是同时运行的,微观上看所有的线程都是走走停停的。
将CPU的时间划分为若干个时间片段,尽可能的均匀分配给每个线程,获得CPU时间片段的线程将得以被CPU执行
- 继承 Thread类(线程类),其每一个实例(用这个类创建的对象)表示一个可以并发的线程。
- 实现Runnable接口(用于发布线程任务的接口)
使用线程的注意事项:
- 对于线程调度而言,分配的时间片段长短、具体分配给哪一个线程时间片段,对于程序而言都是不可控的
线程在执行某段逻辑时,可能发生阻塞现象
sleep阻塞:该阻塞可以指定阻塞时间,并在线程阻塞了该时间后,自动返回Runnable等待状态,并不是直接进入Running运行状态。Thread提供了一个静态方法sleep(),该方法就是用于睡眠阻塞的。
-
跳秒:
阻塞时间结束之后,会进入Runnable等待状态,而不是运行状态,有可能在等待时,CPU恰好在执行其他线程,当前线程的打印任务没有被执行,就可能出现跳秒的情况。
-
run()方法正常执行完毕
-
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的睡眠
当一个进程中所有的前台线程停止后,该进程结束。
-
前台线程和后台线程
-
后台线程
用法和前台线程无异,只是当进程中所有的前台进程都结束后,无论后台线程是否还在运行当中,都会被强制结束,从而使得进程结束,程序退出。后台线程也称为守护进程、精灵进程。
可以用对象调用方法setDaemon(true),将一个前台线程变成一个后台线程
-
程序开始后,会开启一个进程,进程中会创建一个前台线程来运行main()方法的内容,同时也会创建一个后台线程来运行GC(垃圾回收)
1-10级(1最低、10最高)
优先级越高的线程,被分配时间片段的机会就越多,那么获得CPU执行的机会就越多
可以调用方法void setPriority(int p)类设计线程的优先级。 最高(Thread.MAX_PRIORITY)、最低(Thread.MIN_PRIORITY)、默认(Thread.NORM_PRIORITY,即5级)的优先级都会对应的常量
多线程并发访问同一个资源,会产生线程安全问题。
-
多线程并发读写同一临界资源,会发生线程安全并发问题,如果保证多线程同步访问临界资源,就可以解决线程安全的问题
临界资源:
- 多线程共享的实例变量
- 静态的公共变量
-
异步
执行没有先后顺序
-
同步
执行有先后顺序
-
解决办法
把异步操作变为同步操作,用一个同步锁(synchronized关键字)
该关键字可以修饰方法,当一个方法被修饰之后,这个方法就是一个同步方法,同步方法的特点:同一时刻只能有一个线程访问该方法。
-
同步代码块
当一个方法被synchronized修饰之后,该方法变为同步方法,虽然保证了代码的安全,但是效率低下,我们实际上只需要将方法需要同步的代码片段加锁即可,这样可以缩小同步范围,从而提高代码的运行效率
-
语法
synchronized(同步监视器) { // 需要同步的代码片段 }
同步监视器:就是一个对象,任何对象都可以,但是要保证一点,多线程看到的应该是同一个对象,通常情况下使用this就可以。
| 非线程安全 | 线程安全 |
|---|---|
| StringBuilder | StringBuffer |
| ArrayList | Vector |
| HashMap | HashTable |
| HashSet | ...... |
对于Collection和Map而言,Collections类提供了可以将给定的集合转换为线程安全的集合的方法,例如将ArrayList转换为线程安全的类:Collections.synchronizedList(Collection c)
实现线程1执行完,线程2再执行
// t1线程的run()方法
public void run() {
// 方法体
}
// t2线程的run()方法
public void run() {
t1.join();
// t1.join()方法。作用:先执行t1线程,等t1线程执行完毕再执行该方法下面的方法
// 方法体
}使用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();
// 代码块
}
}Java1.5之后,提供了一个并发包concurrent,该包里有一个Executors方法,Executors是一个工厂,包含工厂用于创建Executor接口的实例。
// 创建线程池对象,利用其提供的工厂方法
Executor threadPool = Executors.newFixedThreadPool(3); // 该线程池中有三条线程
// 将任务交给线程池处理
threadPool.execute(p2); // p2是一个任务(即实现了Runnable接口的类的实例)-
继承Thread类
实现步骤
- 继承Thread类,覆盖run()方法,提供并发运行的过程。
- 创建该类的实例。
- 调用start()方法启动线程。
-
实现Runnable接口
实现步骤
- 创建一个类实现Runnalbe接口,重写run()方法。
- 创建这个类(任务类)的实例,用这个类作为Thread的构造器参数,创建Thread类对象,交给线程处理。
- 调用start()方法启动线程。
-
使用匿名内部类
两种方式
// 方法一 Thread t = new Thread() { public void run() { // 方法体 } };
// 方法二 Thread t = new Thread(new Runnable() { public void run() { // 方法体 } });
-
new:新建状态,还没有启动
-
Runnable:等待状态,处于可以运行但是还未运行(就绪)
-
Running:运行状态(运行),此时该线程已经获得了CPU。
- 假设线程获取了CPU,则进入Running状态,开始执行线程体。
- 假设系统只有一个CPU,那么在任意时间点,只有一个线程处于Running状态。但是当线程数大于处理器的数量,依然会是多条线程在同一CPU轮换执行。
- 当一条线程开始运行的时候,如果它不是一瞬间完成,那么它不可能一直处于Running状态,线程在执行的过程中会被中断,目的是让其他线程获得执行的机会。像这样的线程调度的策略取决于底层平台。对于抢占式策略的平台来说,系统会给每一个可执行的线程一小段时间来处理任务,当该时间片段用完了,系统会剥夺线程所占的资源(CPU),让其他线程获得执行的机会。
-
Block:阻塞(挂起)状态。
- 以下情况会发生阻塞状态
- 线程使用了sleep()方法,主动放弃了CPU资源
- 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞(比如控制台输入)
- join()、Object类中的wait()。
- 当正在执行的线程被阻塞时,其他线程就获得了机会,阻塞结束的时候,该线程进入Runnable等待状态,而非直接进入Running状态。
-
Dead:死亡状态,run()方法执行完,该线程进入死亡状态。不要试图对一个已经死亡的线程调用start()方法,死亡后将不能再次作为线程被执行,会抛出异常
程序是由一个或者多个进程组成的,进程是由一个或者多个线程组成的
-
Thread.sleep(times)
使当前线程从Running放弃处理器,进入Block状态,休眠times毫秒之后,进入Runnable状态。如果其他线程调用了interrupt()方法,打断了当前线程的Block(sleep),就会发生中断异常(InterruptedException)
-
Thread.yield()
主动的让出CPU,使线程让出处理器(离开Running),使它进入Runnable等待状态
-
线程的优先级
setPriority(等级1-10,默认是5)
-
后台线程(守护线程)
setDaemon(true),将当前线程变为后台线程
-
获取线程的名称
getName()
-
获取当前线程
Thread.currentThread()
-
join()
先执行某个线程(即先执行调用join()方法的线程)
-
sleep状态的打断和唤醒
Thread.sleep(times),
interrupt()方法,打断、中断休眠。
被唤醒的线程会出现中断异常
Java1.5之后,提供了一个并发包concurrent,该包里有一个Executors方法,Executors是一个工厂,包含工厂用于创建Executor接口的实例。
如果为线程池中的每个线程都分配不同的任务对象,那么即便任务有synchronized,也还是会有重复的东西。可以将一个任务对象分配给多个线程去完成,那么就不会有重复的了。
Timer。
- 可以安排自动计划的类,每一个任务都是一个线程。
- 创建一个Timer实例
- timer.schedual(),可以为Timer实例增加计划任务,计划任务是一个抽象类(TimerTask),该抽象类实现了Runnable接口
- 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);
}