文章目录
单线程与多线程
单线程:先吃饭,然后学习,最后玩游戏
多线程:边吃饭,边学习,边玩游戏
线程和进程
在操作系统中,使用进程是为了使多个程序能并发执行,以提高资源的利用率和系统吞吐量。在操作系统中再引入线程,则是为了减少系统开销,使计算机操作系统具有更好的并发性。
由于进程是一个资源的拥有者,因而在创建、撤销和切换中,系统必须为之付出较大的系统开销。所以,系统中的进程数目不宜过多,进程切换的频率也不宜过高,这也就限制了系统并发性的进一步提高。
进程是资源分配的基本单位,所有与该进程有关的资源,如打印机,输入的缓冲队列等都被记录在进程控制块中,以表示该进程拥有这些资源或正在使用它们。与进程相对应,线程是进程内一个相对独立的、可调度的执行单元。线程属于某一个进程,并与进程内的其他线程一起共享进程的资源。
线程是操作系统中的基本调度单元,所以每个进程在创建时,至少需要同时为该进程创建一个线程,线程也可以创建其他线程。
多线程好处
- 系统开销小
创建和撤销线程的系统开销,以及多个线程之间的切换,都比使用进程进行相同操作要小的多。
- 方便通信和资源共享
如果是在进程之间通信,往往要求系统内核的参与,以提供通信机制和保护机制。而线程间通信在同一进程的地址空间内,共享主存和文件,操作简单,无须系统内核参与。
- 简化程序结构
用户在实现多任务的程序时,采用多线程机制实现,程序结构清晰,独立性强。
线程三状态
线程是相对独立的、可调度的执行单元,因此在线程的运行过程中,会分别处于不同的状态。通常而言,线程主要有下列几种状态。
- 就绪状态:线程已经具备运行的条件,等待调度程序分配 CPU 资源给这个线程运行。
- 运行状态:调度程序分配 CPU
- 资源给该线程,该线程正在执行。 阻塞状态:线程正等待除了 CPU 资源以外的某个条件符合或某个事件发生。
关系如下:
定义线程类的两种方式
直接继承 Thread 类
class 类名 extends Thread{
//属性
//其他方法
public void run() { // 重写 `Thread` 类中的 run() 方法
//线程需要执行的核心代码
}
}
Thread 线程对象名 = new Thread(对象名);
实现 Runnable 接口
class 类名 implements Runnable{
//属性
//其他方法
public void run() { // 实现Runnable接口中的 run() 方法
//线程需要执行的核心代码
}
}
Runnable 实现类名 对象名 = new Runnable实现类名();
比较:
哪种方式比较好?
Runnable,继承Thread类就不能继承其他类,接口可以
线程操作常用方法
void start():使该线程开始执行,Java 虚拟机负责调用该线程的 run() 方法。
void sleep(long millis):静态方法,线程进入阻塞状态,在指定时间(单位为毫秒)到达之后进入就绪状态。
void join():只有当前线程等待加入的线程完成,才能继续往下执行。
void isAlive():判定该线程是否处于活动状态,处于就绪、运行和阻塞状态的都属于活动状态。
void setPriority(int newPriority):设置当前线程的优先级。
int getPriority():获得当前线程的优先级。
守护线程
守护线程是为其他线程的运行提供便利的线程。Java 的垃圾收集机制的某些实现就使用了守护线程。
守护线程会在所有非守护线程结束后自动强制终止,而不是等待其它线程执行完毕后才终止
设置线程为守护线程需要在线程启动前通过setDaemon(true)设置 并且可以使用 isDaemon() 方法的返回值( true 或false )判断一个线程是否为守护线程。
数据不一致问题
当一个数据被多个线程存取的时候,通过检查这个数据的值来进行判断并执行操作是极不安全的。因为在判断之后,有可能因为 CPU时间切换或阻塞而挂起,挂起过程中这个数据的值很可能被其他线程修改了,判断条件也可能已经不成立了,但此时已经经过了判断,之后的操作还需要继续进行。这就会造成逻辑的混乱
针对这种情况,Java 提供了同步机制,来解决控制共享数据的问题,可以使用 synchronized 关键字确保数据在各个线程间正确共享。
synchronized
线程对象在访问 synchronized 代码块前,会先主动尝试获取锁对象。并且只有成功的获取到了对象锁之后,线程对象才能执行synchronized 代码块。此外, synchronized 代码块可以让对象锁在同一时间内,只能被一个线程对象获取到。
对象锁机制
代码结构如下:
synchronized(obj){
//同步代码块
}
实例代码如下
private static class ShareThread1 implements Runnable {
public void run() {
//获取对象锁lock
synchronized (lock) {
while (data < 10) {
try {
Thread.sleep(1000);
System.out.println(data++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
同步方法
代码结构如下:
访问修饰符 synchronized 返回类型 方法名{
//同步方法体内代码块
}
示例代码如下:
private static synchronized void doTask() {
for (int i = 0; i < 10; i++) {
System.out.println("正在输出:" + i);
}
}
死锁定义
线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。
死锁原因
- 系统资源不足。如果系统的资源充足,所有线程的资源请求都能够得到满足,自然就不会发生死锁。
- 线程运行推进的顺序不合适。
- 资源分配不当等。
死锁条件
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
只要系统发生死锁,这四个条件就必然成立;反之,只要破坏四个条件中的任意一个,就可以避免死锁的产生。解决这类死锁问题最有效的方法是破坏循环等待条件。
线程间协作
JDK 的 Object 类提供了 void wait()、void notify()、void
notifyAll()三个方法,解决线程之间协作的问题。语法上,这三个方法都只能在 synchronized
修饰的同步方法或者同步代码块中使用,否则会抛出异常。下面是这三个方法的简单介绍。
void wait():当前线程等待,等待其他线程调用此对象的 notify() 方法或 notifyAll() 方法将其唤醒。
void notify():唤醒在此对象锁上等待的单个线程;如果有多个等待的线程,则随机唤醒一个。
void notifyAll():唤醒在此对象锁上等待的所有线程。