Java多线程知识点整理
1.1 线程与进程
进程:
是资源分配与调度的基本单位。也可表示内存中运行的应用程序(软件),每个进程都有一个独立的内存空间。
线程:
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。
- 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
1.2 线程和进程有什么区别?
- 进程是一个独立的运行环境,可以被看作一个程序或者一个应用。
- 线程是进程的子集或执行路径,一个进程可以有很多线程,每条线程并行执行不同的任务。
- 不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。
1.3 常用的开启多线程的技术?
1.继承Thread类,实现run()方法
代码实例:
先创建一个子类MyThread继承Thread类,然后重写这个子进程的执行方法。
public class MyThread extends Thread{
/**
* run方法就是线程要执行的任务方法
*/
@Override
public void run(){
//这里的代码 就是一条新的执行路径
//这个执行路径的触发方式,不是调用run方法,而是调用Thread对象的start()方法来启动
for (int i=1; i<4; i++){
System.out.println("小明比较快"+i);
}
}
}
新创建一个Demo类,在main线程中创建子线程对象,通过子线程对象的start()方法来启动子线程,执行run方法。
public class Demo {
/**
* 多线程技术
*/
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
for (int i=1; i<4; i++){
System.out.println("小红比较快" + i);
}
}
}
注意:
- 每个线程都拥有自己的栈空间,共用一份堆内存。
- 由一个线程所调用的方法,这个方法也会执行在这个线程里。
2.实现Runnable接口,实现run()方法
Runnable实例对象作为Thread构造方法中的target参数传入,充当线程执行体。这种方式适用于多个线程共享资源的情况。
代码实例:
public static void main(String[] args) {
//实现Runnable
//1. 创建一个任务对象
MyRunnable mr = new MyRunnable();
//2. 创建一个线程,并为其分配一个任务
Thread thread = new Thread(mr);
thread.start();
for (int i = 0; i < 3; i++) {
System.out.println("小明先吃饭" + i);
}
}
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("小红先吃饭" +i);
}
}
}
实现Runnable 与 继承Thread相比的优势
- 通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况。
- 可以避免单继承所带来的局限性。
- 任务与线程本身是分离的,提高了程序的健壮性。
- 后续学习的线程池技术,接收Runnable类型的任务而不接收Thread类型的线程。
- Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好。
3.实现Callable接口,实现call()方法。
实现Callable接口,通过FutureTask包装器创建Thread线程。
使用步骤:
- 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
- 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask future = new FutureTask<>(callable);
- 通过Thread,启动线程
new Thread(future).start();
代码实例:
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> mc = new MyCallable();
//创建FutureTask对象,传入Callable类对象mc
FutureTask<Integer> task = new FutureTask<Integer>(mc);
//通过Thread,启动线程
new Thread(task).start();
Integer k = task.get();
System.out.println("返回值为:" + k);
for (int j = 0; j < 3; j++) {
System.out.println("今天不休息");
}
}
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
for (int i = 0; i < 3; i++) {
System.out.println("今天休息");
}
return 1024;
}
执行结果:
今天休息
今天休息
今天休息
返回值为:1024
今天不休息
今天不休息
今天不休息
Runnable 与 Callable 的区别
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start( )启动线程
不同点: - Callable的 call( ) 方法可以返回值和抛出异常,而Runnable的run( )方法没有这些功能。
- Callable可以返回装载有计算结果的Future对象。
1.4 Java线程的6种状态(重点)
查看源码(在java.lang.Thread中)可以发现java中的线程状态共有6种而不是像操作系统中的5状态图。
Thead类内部有一个枚举类State,列举了线程中可能存在的6种状态为:
- New 新建状态:新建的Thread对象,未调用start()
- Runnable 可运行状态:可运行状态中包含:
- 就绪态:已经获取所有资源,CPU分配执行权就可以执行,所有就绪态线程都在就绪队列中。
- 运行态:正在执行的线程,一个CPU同一时间只能处理一个线程,因此一个CPU上只有一个运行态程序。
- Blocked 阻塞状态:线程请求资源失败时会进入该状态。所有阻塞态线程都存储在一个阻塞队列中,阻塞态线程会不断请求资源成功后进入就绪队列,等待执行。
- Waiting 等待状态:wait、join等函数会造成进入该状态。同样有一个等待队列存储所有等待线程,线程会等待其他线程指示才能继续执行。等待状态线程主动放弃CPU执行权。
- Timed_Waiting 计时等待:计时等待也是主动放弃CPU执行权的,区别是:超时后会进入阻塞态竞争资源。
- Terminal 结束状态:线程执行结束后的状态。
1.5 并行与并发
并行:多核多CPU处理同一段处理逻辑的时候,多个执行流在同一时刻共同执行。
并发:通过CPU的调度算法,使用户感觉像是同时处理多个任务,但同一时刻只有一个执行流占用CPU执行。即使多核多CPU环境还是会使用并发,以提高处理效率。
1.6 Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,使被创建的线程状态变为可运行状态。当你调用Thread的run()方法的时候,只会是在原来的线程中调用此方法,而没有新的线程启动,start()方法才会启动新线程。为了在新的线程中执行我们的代码,必须使用Thread.start()方法。
1.7 sleep()方法、wait()方法、notify()、notifyAll()方法
1、sleep()方法:
- sleep方法为Thread的静态方法;
- sleep方法的作用:让线程休眠指定时间,在时间到达时自动恢复线程的执行;
- sleep方法不会释放线程锁;
2、wait()方法:
- wait方法是Object的方法,任意一个对象都可以调用wait方法,调用wait方法会将调用者的线程挂起,使该线程进入waitSet 的等待区域,直到其他线程调用同一个对象的notify方法才会重新激活调用者;
- 当wait方法被调用时,它会释放它所占用的锁标记,从而使线程所在对象中的synchronize数据可以被别的线程所使用,所以wait()方法必须在同步块中使用,notify()和notifyAll()方法都会对对象的“锁标记”进行修改,所以都需要在同步块中进行调用,如果不在同步块中调用,虽然可以编辑通过,但是运行时会报IllegalMonitorStateException(非法的监控状态异常);
3、notify()和nofityAll()方法;
- notify()会通知一个处在wait()状态的线程;如果有多个线程处在wait状态,他会随机唤醒其中一个;
- notifyAll()会通知所有处在wait()状态的线程,具体执行哪一个线程,根据优先级而定;
2.1 线程调度
分时调度
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度 - 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
1、调整线程优先级:Java线程有优先级,优先级高的线程获得较多的运行机会(运行时间);
static int Max_priority //线程的最高优先级,值为10;
static int MIN_PRIORIYT //线程的最低优先级,值为1;
static int NORM_PRIORITY //分配给线程的默认优先级,值为5;
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级;
2、线程睡眠:Thread.sleep(long millins)使线程转到阻塞状态;
3、线程等待:Object.wait()方法,释放线程锁,使线程进入等待状态,直到被其他线程唤醒(notify()和notifyAll());
4、线程让步:Thread.yeild()方法暂停当前正在执行的线程,使其进入等待执行状态,把执行机会让给相同优先级或更高优先级的线程,如果没有较高优先级或相同优先级的线程,该线程会继续执行;
5、线程加入:join()方法,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再油阻塞状态转为就绪状态。
2.2 线程类的一些常用方法:
1)sleep():强迫一个线程睡眠N毫秒;
2)isAlive():判断一个线程是否存活;
3)join():线程插队;
4)activeCount():程序中活跃的线程数;
5)enumerate():枚举程序中的线程;
6)currentThread():得到当前线程;
7)isDeamon():设置线程是否为守护线程;
8)setName():为线程设置一个名字;
9)wait():线程等待;
10)notify():唤醒一个线程;
11)setPriority():设置一个线程的优先级;
12)getPriority():获得一个线程的优先级;
2.3 面试可能问:公平锁和非公平锁
- 公平锁:先来先到,排队依次执行,假如A线程先过来等,当锁解开时,A线程先执行,有一个排队的过程。
- 非公平锁:谁抢到谁先执行。
如何实现公平锁?
代码实例:
static class SellTicket implements Runnable {
private int count = 10;
//显式锁 ReentrantLock(true),参数fair为true,就表示公平锁。
private Lock lk = new ReentrantLock(true);
@Override
public void run() {
while (true) {
//给需要锁住的代码上锁
lk.lock();
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "准备开始售票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("售票成功," + "剩余票数为" + count);
} else {
break;
}
//解锁
lk.unlock();
}
}
}
线程池
1.概念
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。
线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
2.线程池的优点
1)重用存在的线程,减少对象创建销毁的开销,降低资源消耗。
2)提高响应速度。
3)提高线程的可管理性。
3.Java中的四种线程池
3.1 缓存线程池
Executors.newCachedThreadPool();
创建一个可缓存线程池。
缓存线程池的长度是无限制的。
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程并放入线程池,,然后使用。
代码实例:
public static void main(String[] args) {
//向线程池中加入新的任务
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"我执行了");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"我执行了");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"我执行了");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"新的任务由我来执行");
}
});
}
Demo执行结果:
pool-1-thread-3我执行了
pool-1-thread-1我执行了
pool-1-thread-2我执行了
pool-1-thread-1新的任务由我来执行
3.2 定长线程池
Executors.newFixedThreadPool(2);
创建一个定长线程池,可控制线程最大并发数。
定长线程池(长度是指定的数值)
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,,然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
代码实例:
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "我执行这个线程");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "我执行这个线程");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "我执行这个新的线程");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
3.3 单线程线程池
Executors.newSingleThreadExecutor();
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
效果与定长线程池创建时,传入数值1 效果一致。
执行流程:
- 判断线程池的那个线程是否空闲
- 空闲则使用
- 不空闲,则等待池中的单个线程空闲后使用
代码实例:
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 我来执行这个线程");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 我来执行这个线程");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 我来执行这个线程");
}
});
}
3.4 周期性任务定长线程池
Executors.newScheduledThreadPool(2);
创建一个定长线程池,支持定时及周期性任务执行。
周期任务,定长线程池。定时执行,当某个时机触发时,自动执行某任务。
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
代码实例:
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我执行了");
}
},5, TimeUnit.SECONDS);
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("你好啊");
}
},1,2,TimeUnit.SECONDS);
4.Java死锁以及如何避免?
4.1 Java死锁
死锁:指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务。
死锁产生的原因:
- 系统资源不足。
- 资源分配不当。
- 进程运行推进的顺序不合适。
4.2 死锁产生的四个条件
-
互斥条件:一个资源每次只能被一个进程使用。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
4.3 如何避免死锁?
-
破除互斥等待:一般无法做到。
-
破除请求和保持:一次性获取所有的资源。
-
破除循环等待:按顺序获取资源。
-
破除无法剥夺的等待:加入超时机制。
-
将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。