一,基础概念
1,CPU核心数和线程的关系
CPU核心数:最早的cpu是单核的。后来出现了多核cpu(2核,4核)
CPU和线程的个数是1:1的关系。比如4核可以允许4个线程同时运行。后来intel提出了超线程的概念。使cpu和线程个数1:2。
2,CPU时间片轮转机制
给每一个进程分配一个时间段,这个时间段就被称为进程的时间片 ---> 这个进程允许运行的时间。
不同进程在cpu上执行,cpu需要进行不同进程之间的切换。每次切换需要耗费5000-20000个时钟周期。这其实是浪费了cpu的资源。
我们在开发时,尽量减少让cpu去进行进程间的切换。
3,什么是线程和进程
进程:程序运行进行资源分配的最小单位。一个进程内部有多个线程,多个会共享这个进程的资源
线程:CPU调度的最小单位,线程不拥有资源。线程依附于进程。
4,并行和并发
并行:某一个时间点,可以处理的事情(同一时刻,可以处理事情的能力)
并发:与时间单位相关,某个时间段内,可以处理的事情(单位时间内可以处理事情的能力)
二,认识Java里的线程
1,Java里的程序天生就是多线程的,比如我们执行main方法时,并不是只有main这个主线程,还有别的线程在运行:
/**
* java天生就是多线程的
*/
public class OnlyMain {
public static void main(String[] args) {
//Java虚拟机线程管理的接口。
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//通过这个类可以拿到当前应用程序有多少个线程
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
for (ThreadInfo threadInfo:threadInfos){
System.out.println("["+threadInfo.getThreadId()+"] "+threadInfo.getThreadName());
}
/**
* 打印结果:说明执行main方法时至少启动了5个线程
[5] Monitor Ctrl-Break
[4] Signal Dispatcher
[3] Finalizer
[2] Reference Handler
[1] main main方法线程
*/
}
}
2,启动新线程的三种方式:
/**
* 创建线程的三种方式
*/
public class NewThread {
/**
方式一:扩展自Thread类
*/
private static class UseThread extends Thread{
@Override
public void run() {
System.out.println("i am extends Thread");
}
}
/**
方式二:实现Runnable
*/
private static class UseRun implements Runnable{
@Override
public void run() {
System.out.println("i am implements Runnable");
}
}
/*
方式三:实现Callable接口,允许有返回值
*/
private static class UseCall implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("i am implements Callable");
return "CallResult";
}
}
public static void main(String[] args) throws InterruptedException,ExecutionException{
//Thread启动线程
UseThread useThread = new UseThread();
useThread.start();
//Runnable启动线程
UseRun useRun = new UseRun();
new Thread(useRun).start();
//Callable启动线程
UseCall useCall = new UseCall();
/**
* 注意:
* 1,Callable是不能直接交给Thread的
* 2,可以把Callable包装成Runnable。FutureTask实现了Runnable接口
* 3,包装成Runnable后,交给Thread
*/
FutureTask<String> futureTask = new FutureTask<String>(useCall);
new Thread(futureTask).start();
//我们可以从Callable拿到返回值。注意:get()方法是阻塞的
String result = futureTask.get();
System.out.println(result);
}
}
3,Java提供了Thread类,为什么还要提供Runnable接口?
从面向对象的角度思考:Java是单继承的,提供Runnable接口可以多实现。
4,如何让Java里的线程安全的停止工作?
4.1,三种方式:
方式一:线程正常运行结束
方式二:运行过程中抛出了异常
方式三:Java提供的方法:
suspend():调用该方法后,线程是不会释放资源的。比如该线程有锁,他不会释放这把锁。容易发生死锁
stop():调用该方法后,强行终止线程。无法保证线程资源正常释放。
resume():
方式四:建议使用的方法(中断线程安全的方法):
interrupt():中断一个线程,并不是强行关闭这个线程。只是把一个中断标志设置为true
isInterrupted():判定当前线程是否处于中断状态。判断中断标志是否为true
静态的interrupted():判定当前线程是否处于中断状态。 把中断标志改为false
4.2,注意:
java线程是协作式进行工作的,所以别的线程调用interrupt()并不是强行关闭这个线程,而是对该线程打个招呼。该线程会不会立即停止工作,完全由该线程自己做主。
示例代码:
public static void main(String[] args){
UseRun useRun = new UseRun();
Thread thread = new Thread(useRun);
//main线程对thread线程打了个招呼,至于thread线程会不会停止,完全自己做主
thread.interrupt();
}
三,深入理解Java线程
1,线程的生命周期
2,start()和run()的区别
thread.start():开启一个新的线程,然后该线程会运行run()方法
thread.run():就是单纯的调用run()方法,不会开启一个新的线程
3,了解yield():
将线程从运行转到可运行状态(就绪态),放弃了当前cpu资源,进入就绪态,然后和其他线程一起去竞争cpu资源
4,线程的优先级
设置线程的优先级:thread.setPriority(int priority); 优先级:1-10
注意:理论上来说,线程的优先级越高,就越先被执行。有些操作系统会忽略我们设置的优先级。所以了解就行
5,守护线程
和主线程共死的(主线程退出,守护线程也会退出。比如垃圾回收线程)
public class DaemonThread {
private static class UseThread extends Thread{
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+"i am daemon Thread");
}
}
}
public static void main(String[] args)throws InterruptedException {
Thread endThread = new UseThread();
/**
* 注意:必须在start()方法之前,
* 如果主线程执行完毕,守护线程也停止.
*/
endThread.setDaemon(true);
endThread.start();
Thread.sleep(1);
/**
* 在main主线程睡眠的这一1ms期间,守护线程一直在运行,当main线程执行完毕,守护线程也退出
* 打印结果:
* Thread-0i am daemon Thread
Thread-0i am daemon Thread
Thread-0i am daemon Thread
...
*/
}
}
四,线程间的共享
1,什么是线程间的共享
2,如何实现线程间的共享
2.1,synchronized内置锁
分为类锁和对象锁:
对象锁:锁代码块,锁方法
类锁:static+synchronized : public static synchronized void method(){}
能不能同时运行:
同一个对象锁,不能同时运行
两个不同的对象锁,可以同时运行
一个类锁和一个对象锁,可以同时运行
两个类锁,不能同时运行。 因为每个类的Class对象只有一个,所以还是同一把锁。
区别:
对象锁,锁的是new出来的对象实例
类锁,锁的是每个类的Class对象
2.2,volatile关键字:最轻量的同步机制
保证了可见性:修改了数据后,强制把数据刷到主内存。读取数据时,强制从主内存中读取数据。
但是不保证原子性:即 a = a+1;不是一步完成的。操作系统会执行好几条指令才完成加1操作。
Volatile的使用场景:
只有一个线程写,多个线程读。
2.3,ThreadLocal的使用
每个线程只是用自己线程的变量,把数据和当前线程绑定
案例:我在当前线程中保存该用户的信息,那么在用户的请求线程进入程序时把用户的信息和当前线程绑定,当用户的请求线程结束,我再把这个线程移除。
如何实现:
public class RequestHolder {
private static final ThreadLocal<String> userHolder = new ThreadLocal<>();
public static void add(String sysUser) {
userHolder.set(sysUser);
}public static String getCurrentUser() {
return userHolder.get();
}public static void remove() {
userHolder.remove();
}
}
在拦截器的preHandle()方法中把线程和用户信息绑定,在postHandle()方法中把该线程销毁
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
RequestHolder.add(authorization);//这里我绑定的是token
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 正常返回时, 显式回收threadLocal里的信息
RequestHolder.remove();
}
在我需要获取用户信息的地方(比如Service中的方法),通过线程对应的用户信息
String token = RequestHolder.getCurrentUser();
五,线程间的协作
1,轮询:难以保证及时性,资源开销很大
2,等待和通知
等待和通知的标准范式:
等待方:
要去获取对象的锁,
然后在循环里判断条件是否满足,不满足调用wait方法。
条件满足,执行业务逻辑
通知方:
获取对象的锁
改变条件
通知所有等待在对象的线程
3,方法:
wait():等待着获取对象的锁
wait(1000):等待超时,超过一定时间就不等待了。
notify:通知一个线程
notifyAll:通知所有等待同一把锁的线程
4,join()方法
面试问题:有线程A和线程B,如何保证线程B一定在线程A执行完以后才执行?
方法一:join()
方法二:countDownLatch
解释:如果线程A执行了线程B的join方法,线程A必须等待线程B执行完了以后,线程A才能继续自己的工作。
5,
yield(),sleep(),wait(),notify()等方法对锁的影响
线程在执行yield()以后,持有的锁是不释放的
sleep()方法调用以后,持有的锁是不释放的
wait():在调用wait()方法之前,必须要持有锁。在调用wait()方法以后。锁就会被释放(虚拟机进行释放),当wait方法返回时,线程会重新持有锁
notify():在调用之前,必须要持有锁。调用notify()方法本身是不会释放锁的,只有synchronized代码块执玩才释放锁
notifyAll():同notify()
比如:public synchronized void changeKm(){
this.km = 101;
notify();//当执行完这行代码时,此时还没有释放锁。
System.out.println("处理业务逻辑"); //执行完这一行代码后,才释放锁。
}