什么是进程
-
正在运行的程序,是系统进行资源分配的基本单位。
-
目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分。
-
单核CPU在同一时刻,只能运行一个进程;宏观并行、微观串行
什么是线程
-
-
一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程。
进程和线程的区别
-
进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
-
一个程序运行后至少有一个进程。
-
一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
-
进程间不能共享数据段地址,但同进程的线程之间可以。
线程的组成
-
任何一个线程都具有基本的组成部分:
-
CPU时间片:操作系统(OS)会为每个线程分配执行时间。
-
运行数据:
-
堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
-
栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
-
-
线程的逻辑代码。
-
线程的特点
-
线程抢占式执行
-
效率高
-
可防止单一线程长时间独占CPU
-
-
在单核CPU中,宏观上同时执行,微观上顺序执行。
创建线程
-
创建线程三种方式:
-
继承 Thread 类,重写 run 方法
-
实现 Runnable 接口
-
实现 Callable 接口
-
创建线程(1)
-
第一种方式:继承 Thread 类
-
新建子类继承 Thread 类
-
覆盖 run 方法
-
创建子类对象
-
调用 start() 方法
-
获取和修改线程名称
-
获取线程ID和线程名称
-
在 Thread 的子类中调用 this.getId() 或 this.getName()
-
使用 Thread.currentThread().getId() 和 Thread.currentThread().getName()
-
-
线程 Id 是系统自动分配的, 不允许修改.
-
修改线程名称:
-
调用线程对象的setName() 方法
-
使用线程子类的构造方法赋值
-
创建线程 (2)
-
实现 Runnable 接口:
-
新建 实现类 实现 Runnable 接口
-
覆盖 run() 方法
-
创建实现类对象
-
创建线程对象
-
调用 start() 方法
-
线程的状态(基本)
-
New 初始状态:线程对象被创建,即为初始状态。只在堆中开辟内存,与常规对象无异。
-
调用start()进入就绪态
-
-
Ready 就绪状态:调用start() 之后,进入就绪状态。等待OS选中,并分配时间片。
-
OS选中,分配时间片进入 运行态
-
-
Running 运行状态:获得时间片之后,进入运行状态,如果时间片到期,则回到就绪状态。
-
时间片到期,回到 就绪态
-
run() 运行结束,进入 终止状态
-
-
Terminated 终止状态:主线程 main() 或独立线程 run() 结束,进入终止状态,并释放持有的时间片。
线程的常见方法
-
休眠:
-
public static void sleep(long millis) //参数:毫秒数
-
当前线程主动休眠 millis 毫秒。
-
-
放弃:【又叫避让,当前线程主动放弃 CPU 使用权,给别的线程抢到CPU机会】
-
public static void yield()
-
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
-
-
加入:【非静态方法,只能通过对象来调用;在线程启动之后再加】
-
public final void join()
-
允许其他线程加入到当前线程中。【直到加入线程运行完毕,才执行当前线程】
-
-
优先级:
-
线程对象 . setPriority()
-
线程优先级为 1-10,数字越大优先级越高,默认为5,优先级越高,表示获取CPU机会越多。
-
-
守护线程:
-
在线程启动之前完成设置
-
线程对象. setDaemon( true );设置为守护线程
-
线程有两类:用户线程(前台线程)、守护线程(后台线程):为前台线程提供服务。
-
如果程序中所有前台线程都执行完毕了,后台线程会自动结束
-
垃圾回收器线程属于守护线程
-
线程的状态(等待)
-
当前线程被执行 sleep( 等待时间 ) 指令后,进入Timed Waiting 限期等待。等待时间 结束进入 就绪状态
-
当前线程执行 join() 指令后,进入 Waiting 无限期等待。
线程安全问题
-
多线程安全问题:
-
当多线程并发访问 临界资源【共享资源】时,如果破坏原子操作,可能会造成数据不一致。
-
临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
-
原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
-
-
解决方式:同步
同步方式(1)
-
同步代码块:
-
synchronized ( 临界资源对象 ) { //对临界资源对象加锁,注意这个参数必须是一个对象类型,这是个锁
// 代码 (原子操作)
} -
注:
-
每个对象都有一个互斥锁标记,用来分配给线程的。
-
只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
-
线程退出同步代码块时,会释放相应的互斥锁标记。
-
-
同步方式(2)
-
同步方法:
-
synchronized 返回值类型 方法名称( 形参列表0 ) { //对当前对象(this)加锁
// 代码 (原子操作)
} -
注:
-
只有拥有对象互斥锁标记的线程, 才能进入该对象加锁的同步方法中.
-
线程退出同步方法时, 会释放相应的互斥锁标记.
-
-
同步规则
-
注意:
-
只有在调用包含同步代码块的方法, 或者同步方法时, 才需要对象的锁标记.
-
如调用不包含同步代码块的方法, 或普通方法时, 则不需要锁标记, 可直接调用.
-
-
已知 JDK 中线程安全的类:
-
StringBuffer
-
Vector
-
Hashtable
-
以上类中的公开方法,均为 synchronized 修饰的同步方法.
-
线程的状态(阻塞)
-
运行态 进入 同步操作后,没有拿到锁,进入阻塞状态(Blocked)
-
阻塞状态(Blocked)拿到锁后,回到 就绪状态(Ready)
-
JDK5 之后, 就绪、运行统称为 Runnable
经典问题 (1)
-
死锁:
-
当第一个线程拥有 A 对象锁标记, 并等待 B 对象锁标记, 同时第二个线程拥有 B 对象锁标记, 并等待 A 对象锁标记时, 产生死锁.
-
一个线程可以同时拥有多个对象的锁标记, 当线程阻塞时, 不会释放已拥有的锁标记, 由此可能造成死锁.
-
线程通信
-
等待: [ 通过锁对象调用的方法 ]
-
public final void wait()
public final void wait(long timeout) -
必须在对 obj 加锁的同步代码块中. 在一个线程中, 调用 obj.wait() 时, 此线程会释放其拥有的所有锁标记. 同时此线程阻塞在 obj 的等待队列中. 释放锁, 进入等待队列.
-
-
通知:
-
public final void notify()
public final void notifyAll() -
其他线程调用 notify() 方法来唤醒 等待的线程.
-
经典问题 (2)
-
生产者、消费者:
-
若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区【一般用数组或集合实现】,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。
-
总结
-
线程的创建:
-
方式1:继承 Thread 类
-
方式2:实现 Runnable 接口(一个任务Task),传入给Thread对象并执行。
-
-
线程安全:【多个线程访问临界资源】锁是引用对象,必须唯一
-
同步代码块:为方法中的局部代码(原子操作)加锁。
-
同步代码:为方法中的所有代码(原子操作)加锁。
-
-
线程间的通信:
-
wait( )/ wait( long timeout ): 等待
-
-