一、基础
1、进程和线程
- 进程:进程是用来加载指令、管理内存、管理IO的,操作系统会以进程为单位分配系统资源(cpu、内存等资源),进程是资源分配的最小单位
- 线程:线程是操作系统cpu调度的最小单位,也称为轻量级线程(Lightweight Process, LWP)
c和c++是可以只有进程不需要线程,但是java开启jvm进程必须有线程,这是由实现机制决定的,例如nginx只有多进程的概念,有些程序只能启动一个进程(电脑管家、百度网盘),程序是一组指令的包装,当运行指令时创建进程,开始分配系统资源给进程,而线程是依赖与进程存在,进程可以没有线程,但是线程如果脱离进程只是抢占到了cpu时间片,做不了什么事
2、进程间的通信方式
- 管道:分为匿名管道(pipe)及命名管道(named pipe),匿名管道可用于具有亲缘关系的父子进程间的通信,命名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信
- 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的
- 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新信息,对消息队列有读权限的进程则可以从消息队列中读取信息
- 共享内存(shared memory):可以说这是最有用的进程间通信方式,它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新,这种方式需要依靠某种同步操作,如互斥锁和信号量等
- 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间的同步互斥手段
- 套接字(socket):这是一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。同一机器中的进程还可以使用Unix domain socket(比如同一机器中mysql中的控制台mysql shell和mysql服务程序的连接),这种方式不需要经过网络协议栈,不需要打包拆包、计算校验、维护序号和应答等,比如纯粹基于网络的进程间通信肯定效率更高
3、cpu核心数和线程数的关系
线程是cpu调度的最小单位。理论上一个cpu核心只能运行一个线程,核心数比线程数也就是1:1的关系,但intel引入超线程技术后产生了逻辑处理器的概念,使核心数和线程数形成1:2的关系,在java中提供了Runtime.getRuntime().availableProcessors(),可以获取当前系统的cpu核心数,注意这个核心线程数指的是逻辑处理器的个数
4、上下文切换(Context switch)
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换
上下文是CPU寄存器和程序计数器在任何时间点的内容
- 寄存器是CPU内部的一小部分非常快的内存(相对于CPU内部的缓存和CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。
-
程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。
上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
- 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
- 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
要尽量避免上下文切换
5、并发和并行
- 并发:指在同一时刻只能有一条指令执行,但是基于cpu时间片轮转机制,多个进程指令交替快速执行,在宏观上多个进程看似是同时执行的
- 并行:指在同一时刻,多条指令在多个处理器上同时执行
并行只在多处理器系统中存在,并发无论是单处理器和多处理器系统中都存在
二、线程创建的几种方式
1、启动一个main线程jvm总共有多少个线程
public class OnlyMain {
public static void main(String[] args) {
// java虚拟机线程系统的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息 仅打印线程id和线程名称信息
for(ThreadInfo threadInfo : threadInfos) {
System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());
}
}
}
打印结果:
[6]Monitor Ctrl-Break //监控Ctrl-Break中断信号的
[5]Attach Listener //内存dump,线程dump,类信息统计,获取系统属性等
[4]Signal Dispatcher // 分发处理发送给JVM信号的线程
[3]Finalizer // 调用对象finalize方法的线程
[2]Reference Handler //清除Reference的线程
[1]main //main线程,用户程序入口
java支持多线程
2、线程的创建
Thread是java对线程的唯一抽象实现,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行
/**
* 线程的创建方式
*/
public class NewThread {
private static class UseThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("I am extended Thread");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class UseRunnable implements Runnable {
@Override
public void run() {
System.out.println("I am implements Runnable");
}
}
public static void main(String[] args) {
UseThread useThread = new UseThread();
useThread.start();
UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();
System.out.println("main end");
}
}
Thread源码中写了只有两种创建方式,至于FutureTask也算Runnable,线程池的话只能算线程的复用,而不能说是线程的创建方式
3、Callable、Future和FutureTask
上面的线程都只是异步执行任务没有返回值,当如果需要有返回值时就需要用到Callable
创建一个Thread需要传一个实现Runnable实例化的对象,而FutureTask对象既实现了Runnable接口,而且它的构造方法可以传入一个Callable的接口,这时候只需要创建实现Callable接口的对象,实现自己的逻辑代码,new一个FutureTask的构造方法接收实现Callable的实例对象,FutureTask的实例对象放到Thread对象构造方法中调start()方法,完成线程启动,后面线程启动完如果需要返回值,可以通过FutureTask对象的get()方法获取
public class UseFuture {
private static class UseCallable implements Callable<Integer> {
private int sum;
@Override
public Integer call() throws Exception {
System.out.println("Callable子线程开始计算!");
for(int i=0 ;i<5000;i++){
if(Thread.currentThread().isInterrupted()) {
System.out.println("Callable子线程计算任务中断!");
return null;
}
sum=sum+i;
System.out.println("sum="+sum);
}
System.out.println("Callable子线程计算结束!结果为: "+sum);
return sum;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
UseCallable useCallable = new UseCallable();
//包装
FutureTask<Integer> futureTask = new FutureTask<>(useCallable);
Random r = new Random();
new Thread(futureTask).start();
Thread.sleep(1);
if(r.nextInt(100)>50){
System.out.println("Get UseCallable result = "+futureTask.get());
}else{
System.out.println("Cancel................. ");
futureTask.cancel(true);
}
}
}
三、线程的中断机制
线程有启动必然就有停止,如何停止这是一个问题,是强制停止,还是根据标识停止
1、强制停止
Thread类中stop()方法会强制停止线程,释放对象锁,比如一个累加操作,执行一半突然被强制结束,导致数据不一致,suspend()方法调用后不会释放占有的资源,而是进入睡眠状态,这样容易导致死锁,这些方法都有@Deprecated注解,标明已弃用
2、中断标志位
Thread类中的interrupt()、interrupted()、isInterrupted()方法可以合理按照中断标志位来执行自己的逻辑代码
- interrupt()方法正如注释中写的,只是设置中断标志位
- interrupted()方法是判断当前线程的中断标志位是否为true,重置中断标志位为false
- isInterrupted()方法是判断线程的中断标志位是否为true,不会清除线程标志位
public class HasInterruptException {
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
// 如果设置了中断标志位就不会进入while循环 正常结束了
while (!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+" in InterruptedException interrupt flag is "
+isInterrupted());
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
//while 循环结束后才会打印本句
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("HasInterruptEx");
endThread.start();
Thread.sleep(500);
endThread.interrupt();
}
}
最好不要用自定义标识位来中断线程,正如上面例子,如果线程发生了阻塞,调用sleep,只能等下次自定义标识判断,在jdk内部调用sleep()方法会抛出InterruptedException异常,内部是有检测中断标志位,如果自定义标识位耗费性能
3、run()方法和start()方法
在我们的java代码中,Thread只是一个实例对象,只有调用start()方法时才会真的与线程挂钩,run()方法的调用只是普通方法调用其实是一样的,一个线程start()方法只能调用一次,而run()方法可以调用多次
public class StartAndRun {
public static class ThreadRun extends Thread{
@Override
public void run() {
System.out.println("I am "+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadRun threadRun = new ThreadRun();
threadRun.setName("threadRun");
threadRun.start();
// threadRun.run();
// threadRun.run();
}
}
如果是调用start()方法则是打印I am threadRun,如果是调用run()方法则是打印I am main,由此可见,调用start()方法则是真的创建一个线程,而run()方法则是main线程对普通方法的调用