个人博客网:https://wushaopei.github.io/ (你想要这里多有)
1、程序、进程、线程的理解
1.1 概念
- 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(process)是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的
- 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个程序可同一时间执行多个线程,就是支持多线程的
1.2 进程与多线程
每个Java程序都有一个隐含的主线程: main 方法
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
2、多线程的创建和启动
2.1 Thread 线程概念:
2.2 源码流程解析:
(1)Thread 类 与 Runnable接口
并且在Thread中有一个构造器的参数是Runnable的实现子类,所以当一个自定义类实现了Runnable接口后,可以通过newThread()作为构造参数传入来实现Thread多线程的创建。
如图:
(2)Thread 与 Runnable 、Callable 及Future Task 的关系
首先分析:
Future Task的底层实现了RunnableFuture 接口:
而RunnableFuture 的底层实现了两个接口: Runnable , Future
而根据FutureTask 的构造方法和Callable 的类型可知:
由以上可以得知,通过自定义实现了Callable 接口的实例对象做为FutureTask 的构造器形参,然后因为实现了Runnable 接口的缘故,该对象可以做参数传入Thread对象的构造器中,用来创建新的多线程 thread ,并使用 start ()启动线程。
2.3 案例:mt子线程的创建和启动过程
3、线程中常用的API
interrupt() : 中断线程(也会将当前线程中的任务执行完毕会进行线程的一个标记,标记该线程是要被停止。
isInterrupted() :测试此线程是否已被中断。(如果线程的标记被标识成中断则返回true)
3、创建多线程的四中种方式
3.1 继承Thread
- 自定义一个类并继承Thread
- 重写run方法
- run方法中可以写入需要在分线程中执行的代码
- 创建Thread子类的对象
- 通过对象调用start方法
3.2 实现Runnable接口
- 自定义一个类并实现Runnable接口
- 重写run方法
- 在run方法中写入需要在分线程中执行的代码
- 创建Runnable实现类的对象
- 创建Thread对象并将Runnable实现类的对象作为实参传入到Thread的构造器中
- 通过Thread对象调用start方法
3.3 实现Callable接口
- 自定义一个类,并实现Callable接口
- 重写call方法,并返回结果
- 在call方法中实现需要在分线程中执行的代码
- 创建Callable接口的实现类的对象
- 创建FutureTask对象,并将Callable接口的实现类的对象作为实参传到FutureTask的构造器中
- 创建Thread对象,并将FutureTask对象作为实参传入到Thread构造器中
- 调用start方法
代码:
public class ThreadTest {
public static void main(String[] args) throws Exception {
//4.创建Callable接口的实现类的对象
MyCallble myCallble = new MyCallble();
//5.创建FutureTask对象,并将Callable接口的实现类的对象作为实参传到FutureTask的构造器中
FutureTask<Integer> futureTask = new FutureTask<>(myCallble);
//6.创建Thread对象,并将FutureTask对象作为实参传入到Thread构造器中
Thread thread = new Thread(futureTask);
//7.调用start方法
thread.start();
//阻塞主线程,等待分线程返回数据结果,然后再向下执行。
Integer intNumber = futureTask.get();
System.out.println("===" + intNumber);
System.out.println("aaaaaaaaaaaaaaaaaaa");
}
}
//1.自定义一个类,并实现Callable接口
class MyCallble implements Callable<Integer>{
//2.重写call方法,并返回结果
@Override
public Integer call() throws Exception {
/*
* 3.需要在分线程中执行的代码
*/
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " == " + i);
}
return 100;
}
}
3.4 线程池
//创建一个线程池,根据需要创建新的线程,但在可用时将重用先前构建的线程。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建一个线程池,使用固定数量的线程操作了共享*队列。
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
//创建一个执行器,使用一个单一的工作线程操作关闭一个无限的队列。
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "====" + i);
}
}
});
//关闭线程池
newCachedThreadPool.shutdown();
4、Thread的生命周期
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
- 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止
5、线程的同步机制
5.1 为什么要使用线程的同步机制?
多个线程在同时操作共享数据的时候可能会发生线程安全问题。
比如卖票:会发生重票,0票,负票的问题
5.2 解决线程安全问题的方式?
解决方法一 :同步代码块 :
格式 :
synchronized (同步监视器) {
//操作共 享数据的代码
}
1.同步监视器 : 可以是任何对象
注意 : 多个线程必须使用的是同一把锁(必须保证多个线程使用到的同步监视器是同一个对象)。
2.同步代码块内 : 操作共享数据的代码
(实现Runnable使用同步代码块案例 : 详见RunnableTest.java)
解决方法二: 同步方法
格式 :
public synchronized void say(){} -- 修饰普通方法
默认的同步监视器是:this
public static synchronized void say(){} -- 修饰静态方法
默认的同步监视是运行时类的对象。 例:Person.class
6、线程的单例模式
class Bank {
private Bank() {
}
private static Bank bank = null;
public static Bank getInstance() {
if (bank == null) {
synchronized (Bank.class) {
if (bank == null) {
bank = new Bank();
}
}
}
return bank;
}
}
7、死锁的问题
死锁的原因:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
代码:
//创建两把锁
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
public void run() {
synchronized (s1) {
s2.append("A");
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (s2) {
s2.append("B");
System.out.print(s1);
System.out.print(s2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (s2) {
s2.append("C");
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (s1) {
s1.append("D");
System.out.print(s2);
System.out.print(s1);
}
}
}
}.start();
8、线程通信
8.1 线程通信涉及到三个方法:
注意 :
- 上面个方法只能用在同步代码块和同步方法中
- 调用个方法实际上是调用监视对象中的方法
8.2 [面试题] wait和sleep的区别?
- sleep睡觉的时候会抱着锁。wait睡觉的时候会释放锁。
- sleep时间一到就自动唤醒,wait需要被其它线程调用notify/notifyAll才能唤醒。
- sleep是Thread中的方法, wait是Object中的方法。
8.3 [面试题] 继承Thread和实现Runnable的区别?
继承Thread :
同步监视器 : 不可以使用this |
共享资源 : 需要使用static关键字修饰。 |
单继承 |
public static synchronized void say() - 默认锁是运行时类的对象。 比如 : Person.class |
实现Runnable
同步监视器 : 可以使用this |
共享资源 : 不需要使用static关键字修饰。 |
多实现 |
public synchronized void say(){} - 默认锁是this |
9、同步的优点和缺点
- 同步的优点 : 解决了线程安全问题
- 同步的缺点 : 在同步代码块和同步方法的方法体执行时,只能有一个线程在执行。效率低。
10、ReentrantLock
代码:
class MyRunnable2 implements Runnable {
// 同一个类的多个对象共同拥有一份类变量
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); //锁住当前的线程
try {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " ==== " + ticket);
ticket--;
} else {
return;
}
} finally {
//最后一定要手动解锁
lock.unlock();
}
}
}
}
注意 :ReentrantLock使用到的线程通信不是使用的wait()和notify()