一,基础概念
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("处理业务逻辑"); //执行完这一行代码后,才释放锁。
}