进程 & 线程
进程是指正在运行的程序,确切的说,当一个程序进入内存开始运行,就开启了一个进程。进程就是处于运行状态的程序,并且具有一定的独立功能。
线程是进程中的一个执行单元,负责当前进程中程序的执行。一个进程中至少有一个线程。
一个程序运行后至少有一个进程,一个进程中可以有多个线程。
什么是多线程?
多线程就是指一个程序中,多个线程“同时”执行。
可以通过下图来理解分多线程程序与单线程程序的不同:
- 单线程程序:若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,单线程的网吧同一时间只能让一个人上网,只有当这个人下机后,下一个人才能上网。
- 多线程程序:即,若有多个任务可以同时执行。如,多线程网吧能够让多个人同时上网。
程序运行原理
大部分操作系统都支持多进程并发运行,也就是可以同时运行多个程序。比如说你在上网逛淘宝的同时(开一个浏览器),还可以听音乐(开一个音乐播放器),同时使用迅雷下载电影,感觉这些软件都在同时运行。而实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
分时调度 & 抢占式调度
分时调度就是说所有线程轮流获取CPU 的使用权,平均分配每个线程占用 CPU 的时间。
而抢占式调度就是
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
创建线程的几种方法
1. 继承Thread类
- 自定义类并继承Thread类,重写run()方法
- 创建该自定义类的对象,并调用其start()方法
class MyThread extends Thread {
@override
public void run() {
System.out.println("Thread Body...");
}
}
class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 开启线程
}
}
2. 实现Runnable接口
- 自定义类实现Runnable接口,并实现run()方法
- 创建Thread对象,用实现Runnable接口的对象作为参数实例化该Thread对象
- 调用Thread对象的start()方法
class MyThread implements Runnable {
@override
public void call() {
System.out.println("Thread Body...");
}
}
class Test {
public static void main(String[] args) {
Thread thread = new MyThread(new MyThread());
thread.start(); // 开启线程
}
}
3. 实现Callable接口
- 实现Callable接口, 实现call()方法
- 创建实现Callable接口的参数,并将之作为参数实例化一个 FutureTask 对象
- 创建Thread对象,并用刚创建的FutureTask对象作为参数实例化该Thread对象
- 调用Thread对象的start()方法
class MyThread implements Callable<String> {
@override
public String call() throws Exception {
System.out.println("Thread Body...");
return "hello";
}
}
class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable = new MyThread();
FutureTask<String> task = new FutureTask<>(callable);
Thread thread = new Thread(task);
thread.start(); // 开启线程
String result = task.get(); // 等待线程结束获取返回值
}
}
Runnbale接口和Callable接口的区别
- Callable接口可以在任务结束后提供一个返回值,Runnable接口无法提供该功能
- Callable接口的call()方法可以抛出异常,而Runnable接口的run()方法不能抛出异常
- 运行实现Callable接口的对象可以获取一个Future对象,Future表示异步计算的结果,提供了检查计算是否完成的方法
注意点:Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
线程的生命周期
1. 新建(new Thread): Thread t = new Thread();
2. 就绪(Runnable): t.start();
3. 运行(Running): run() 运行
4. 死亡(dead): 执行完毕(自然死亡,即run()方法执行结束)/ 被其他线程杀死(异常死亡,stop())
5. 堵塞(blocked): sleep()、 wait()、suspend()
各个方法的使用
- wait() 方法是一种使线程暂停执行的方法,直到被唤醒(notify()方法唤醒)或者等待超时
- sleep() 方法使当前运行的线程休眠指定时间
- stop() 终止线程执行,释放已经锁定它的所监视的资源
- suspend() 方法将一个线程挂起(暂停),且不会自动恢复,必须通过调用resume()方法使之重新进入可执行状态
解决多线程安全问题:
- 明确哪些代码是多线程运行代码
- 明确共享数据
- 明确多线程运行代码中哪些语句是操作共享数据的
注意: 同步函数的锁是this, 而静态同步函数的锁是Class对象:类名.class
解决线程同步问题的方法
1. 同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void save(){}
注意: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
2. 同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){
...
}
**注意:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。**
3. 使用特殊域变量(volatile)实现线程同步
- volatile关键字为域变量的访问提供了一种免锁机制,
- 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
- 因此每次使用该域就要重新计算,而不是使用寄存器中的值
- volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。
4. 使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用