Java并发编程相关知识整理
1、什么是进程、线程、多线程?
进程当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。进程间通讯依靠IPC资源,例如管道、套接字
线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码是共享的,即不同的线程可以执行同样的函数。
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说说允许单个程序创建多个并行执行的线程来完成各自的任务。线程间通讯依靠JVM提供的API,例如wait()、notify、notifyAll等方法,线程间还可以通过共享的主内存来进行值的传递
2、多线程的优缺点?
优点:可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其他的线程而不是等待,提高程序相应效率。
缺点:线程也是程序,所有线程需要占用内存,线程越多占用的内存也越多。
线程需要协调和管理,所以需要CPU时间跟踪线程
线程之间对共享资源的访问会互相影响,必须要解决竞用共享资源的问题
线程太多会导致控制太复杂,最终可能造成很多Bug
3、多线程一定比单线程快吗?
不一定,由于多线程会存在线程上下文切换,会导致程序执行速度变慢,但可以充分利用CPU,所以对于用户来说,可以减少用户响应时间。
比如,尝试使用并行和串行分别执行累加的操作观察是否并行执行一定比串行更快:
package com.test.demo; public class Tester { private static final long count = 1000000000; public static void bingxing() throws Exception { long startTime = System.currentTimeMillis(); //通过匿名内部类来创建线程 Thread thread1 = new Thread(new Runnable() { @Override public void run() { int a = 0; for(long i = 0; i < count; i++) { a += 1; } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { int b = 0; for(long k = 0;k < count; k++) { b+=1; } } }); //启动线程 thread1.start(); thread2.start(); //等待线程结束 thread1.join(); thread2.join(); long time = System.currentTimeMillis() - startTime ; System.out.println("并行花费时长:" + time + "ms"); } public static void chuanxing() { long startTime = System.currentTimeMillis(); int a = 0; for(long i = 0; i < count; i++) { a += i; } int b = 0; for(long k = 0;k < count; k++) { b+=k; } long time = System.currentTimeMillis() - startTime ; System.out.println("串行花费时长:" + time + "ms"); } public static void main(String[] args) throws Exception { bingxing(); chuanxing(); } }
循环次数 | 串行执行/ms | 并行执行/ms | 结果 |
1千 | 0ms | 2ms | 慢 |
1万 | 0ms | 2ms | 慢 |
10万 | 3ms | 4ms | 慢 |
100万 | 6ms | 4ms | 快 |
1000万 | 13ms | 11ms | 快 |
1亿 | 89ms | 78ms | 快 |
从测试结果看出当超过100万次循环后,并行执行的优势越加明显,不超过100万次循环时,串行执行的速率要比并行执行的速率高,原因就是多线程有上下文切换的开销。
4、阻塞与非阻塞
阻塞和非阻塞通常用来形容多线程之间的相互影响
阻塞是指一个线程占用了临界区资源,那么其他所有需要这个资源的线程就不洗在这个临界区中进行等待,等待会导致线程挂起,这种情况就是阻塞
非阻塞强调没有一个线程可以妨碍其他线程执行,所有线程都会尝试不断向前执行
5、临界区
临界区用来表示一种公共资源或者共享资源可以被多个线程使用,但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程想要使用这个资源,就必须等待
6、死锁Deadlock、饥饿starvation、活锁Livelock
死锁表示两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们将无法推进下去,此时称系统处于死锁状态,这些永远在互相等待的进程成为死锁进程
饥饿表示一个或者多个线程应为种种原因无法获得所需要的资源,导致一直无法执行。导致饥饿的原因可能是该线程优先级太低,而高优先级的线程不但抢占它所需要的资源,导致其无法向前推进,另外一种可能是,某线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行
活锁表示两个或者多个线程主动将资源释放给其他线程,导致没有一个线程可以同时拿到所有资源而正常执行。
7、如何避免死锁
指定获取锁的顺序
8、sleep()与wait()区别
sleep()是Thread类的静态方法,使当前线程睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,解除阻塞,进行可运行状态,等待CPU的到来。睡眠不释放锁。
wait()是Object的方法,必须和synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,释放互斥锁。
9、synchronized关键字底层实现
进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1;当一个线程判断到计数器为0时,则当前锁空闲,可以占用,反之,当前线程进入等待状态。
10、volatile关键字功能
直接与主内存产生交互,进去读写操作,保证可见性;禁止JVM进行指令重排序;能使一个非原子操作变为原子操作。比如对一个volatile型的long或者douuble变量的读写是原子
11、ThreadLocal关键字
当使用ThreadLocal维护变量时,其为每一个使用该变量的线程提供独立的变量副本,所以当每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本
12、线程池的了解
java.util.concurrent.ThreadPoolExcutor类就是一个线程池。客户端调用ThreadPoolExecutor.submit(Runable Task)提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小。
当前线程池大小:表示线程池中实际工作者线程的数量
最大线程池大小:表示线程池张允许纯在的工作者线程的数量上限
核心线程大小:表示一个不大于最大线程池大小的工作者线程数量上限
线程池有三种状态:
①、如果运行的线程小于核心线程大小,则Executor始终首选添加新的线程,而不进行排队
②、如果运行的线程等于或者多于核心线程大小,则Executor始终首选将请求加入队列,而不是添加新线程;
③、如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超过最大线程池大小,在这种情况下,任务将会被拒绝
13、线程池的作用
减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
可以根据系统的承受能力,调整线程中工作线程的数目,防止因为消耗过多的内存,而把服务器过载
14、创建线程的实现方式
继承Thread类,实现Runable接口,实现Callable接口
15、run()方法和start()方法的区别
start()方法会新建一个线程并让这个线程执行run()方法;而直接调用run()方法只是作为一个普通的方法调用而已,它只是会在当前线程中,串行执行run()中的代码