一 基础概念
1.1 CPU核心数和线程数关系
CPU核心数是指CPU硬件上存在几个核心,CPU所有的计算、接受/存储命令、处理数据都由核心执行。
对于一个CPU,线程数总是大于或等于核心数的,自从Inter 引入超线程技术后,一个核心可以对应两个线程(即一个核心上可以同时并行2个线程)
1.2 cpu时间片轮转机制(也叫RR调度)
cpu会给每个线程分配个时间片 ,线程执行完自己的时间片内时间后,CPU将被剥夺并分配给另一个线程。而它自己就进入可执行状态,等待下一次被分配时间片执行。
大多数现代操作系统的时间片为 10~100ms,上下文切换的时间一般少于 10ms;
1.3 什么是进程,什么是线程?
进程: 程序运行进行资源分配的最小单位,进程内可有多个线程,共享进程的资源
线程:CPU调度的最小单位
1.4 并行和并发
并行:同一时刻可以处理事情的能力
如8个窗口可以同时买火车票,并行量就是8
并发:单位时间内可以处理事情的能力(与时间单位相关)
10分钟内8个窗口可以买火车票的量(并发量) 可能就是 8(窗口)*10(小时)*2(每分钟2张)= 160
二 多线程的概念:
应用程序中,一个进程中有多个线程,每个线程有自己独立的内存空间(操作空间),彼此之间互相独立,多个线程执行多个任务。
在JAVA中的多线程其实是通过cpu调度算法(时间片轮转调度),让用户看上去同时执行,实际上从cpu操作层面不是真正的同时,而是由java虚拟机调度轮流执行。
三 实现多线程的方法
3.1 实现接口 Runnable ,并实现run()方法
例如
public class ThreadRunnableTest implements Runnable{ @Override public void run() { //线程做的事情 System.out.println("ruunable 线程"); } }
3.2 继承Thread类,并重写run方法
public class ThreadTest extends Thread { @Override public void run() { //线程做的事情 System.out.println("thread 线程"); } }
3.3 实现接口 Callable<Object> 并实现call()方法
并且该线程是有返回值的
public class ThreadCallableTest implements Callable<String > { @Override public String call() throws Exception { //线程做的事情 return "callable result "; } }
//启动第一个线程
Runnable threadRunnable1 = new ThreadRunnableTest(); //创建线程,并绑定线程任务 Thread thread1 = new Thread(threadRunnable1); //启动线程 通过Thread对象调用start()方法启动线程 thread1.start();
//启动第二个线程
Thread threadTest2 = new ThreadTest(); //启动线程 threadTest2 .start();
//启动第三个线程
//callable 类型无法加入到Thread类中被启动,所以我们要包装一下 FutureTask<String> futureTask = new FutureTask<String>(callableTest); //FutureTask<Object> 实现了Runnable 接口,所以可以加入Thread中 Thread threadA = new Thread(futureTask); //启动线程 threadA.start(); try { //获取线程执行后的返回值 String getResult = futureTask.get(); System.out.println("获取到callable线程执行后返回值="+getResult); }catch (Exception ex) { ex.printStackTrace(); }
四 线程的状态
线程状态总的可分为五大状态:分别是生、死、可运行、运行、等待/阻塞
1 新状态:线程对象已经创建,还没有在其上调用start()方法。
2 可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
3 运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
4 等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态
5 死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
附丑图一张,是难看了点
五 阻止线程执行
1 睡眠 Thread.sleep(时间毫秒),在睡眠时间后,线程进入可运行状态等待被运行,睡眠帮助所有线程获得运行机会的最好方法
2 线程的优先级和线程让步yield()
线程的让步是通过Thread.yield()来实现的。yield()方法的作用是:暂停当前正在执行的线程对象(转入可执行状态),让CPU重新调度可执行的线程(理论上该线程和其余线程是公平竞争执行机会的)。优先级范围在1~10,默认5,设计多线程时,不可依赖优先级,因为优先级没有保障,只是作为一种提高程序效率的方法,setPriority() 设置优先级; 但是很多时间并不好用,可能线程会再次被线调度程序选中执行
3 线程插队 join() ,如在A线程中,调用了B.join() 就要等待B线程执行完后,才轮到A线程执行,JDK中的意义 等待这个线程死亡 join(), join(毫秒) ,join(毫秒,纳秒)
4 等待 wait(), 线程等待,直到被唤醒 notify 或者 notifyAll 再继续执行
5 线程终止 stop(),已经废弃不推荐使用,会暴力中断线程使得线程来不及正常的释放资源,
推荐用interrupt();
interrupt中断机制中有如下方法:
- Thread.interrupt(),设置当前中断标记为true(类似属性的set方法)
- Thread.isInterrupted(),检测当前的中断标记(类似属性的get方法)
- Thread.interrupted(),检测当前的中断标记,然后重置中断标记为false(类似属性的get方法+set方法)
调用了Thread.interrupt() 后,只是设置了中断标志位,并不会直接中断线程,
线程的中断需要线程自己根据中断标志位实现
public class ThreadRunnableTest implements Runnable{ @Override public void run() { System.out.println("ruunable 线程"); int a = 0; for(int i=0;i<10000000;i++) { //一般要自己实现中断线程的逻辑, if(Thread.currentThread().isInterrupted()) { System.out.println("由于调用了interrupt()导致的break"); break; } a += i; System.out.println("a="+a); } } }
如上面例子,当线程调用interrupt()后,for循环内就可以判断中断标记为直接跳出循环,快速执行完线程。
如果线程interrupt() 后发生 InterruptedException 时(如处于sleep状态,你强制interrupt() 唤醒她就会发生该异常),会把中断标志位置为false ,此时如果你捕获了异常, 线程依然还是会继续执行下去,
所以应该在catch块里,再调用一次 interrupt();
六 线程的同步与锁
目的;防止多线程执行某段代码时导致的数据异常,互相干扰
1 synchronized 单独使用于对象,使用对象锁
object lock = new object(); public void run() { //锁住了整个对象 synchronized(lock){ do something; } }
Java中的每个对象都有一个监视器,来监测并发代码的重入,上面就synchronized获取了lock的监视器,
2 synchronized 用于普通方法,对象琐
public class Thread1 implements Runable{ //锁住了方法 public synchronized void run() { do something } }
其实是获取了Thread1的监视器,俗称对象锁。多个Thread1 实例在不同线程中调用 run()方法时,是会互斥的。
线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
3 synchronized 用于静态方法 类琐
Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”
情况1:用类直接在两个线程中调用两个不同的同步方法
结果:会产生互斥。
解释:因为对静态对象加锁实际上对类(.class)加锁,类class对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。
情况2:用一个类的静态对象在两个线程中调用静态方法或非静态方法
结果:会产生互斥。
解释:因为是一个对象调用,同上。
本人另一篇博客有专门讲锁,可去哪里获取更多关于锁的介绍