2023-06-29 16:31:47
浏览数 (2)
一.多线程基础
1.进程
它是内存中的一段独立的空间,可以负责当前应用程序的运行,当前这个进程负责调度当前程序中的所有运行细节,不同进程彼此之间不会相互影响。
2.线程
在一个进程中,每个独立的功能都需要独立的去运行,这时又需要把当前这个进程划分成多个运行区域,每个独立的小区域(小单元)称为一个线程。
进程是负责整个程序的运行,而线程是程序中具体的某个独立功能的运行。一个进程中至少应该有一个线程。
3.多线程
在一个进程中,我们同时开启多个线程,让多个线程同时去完成某些任务(功能)。比如后台服务系统,就可以用多个线程同时响应多个客户的请求。
目的:提高程序的运行效率。
4.多线程原理
CPU在线程中做时间片的切换。
其实真正电脑中的程序的运行不是同时在运行的。CPU负责程序的运行,而CPU在运行程序的过程中某个时刻点上,它其实只能运行一个程序。而不是多个程序。而CPU它可以在多个程序之间进行高速的切换。而切换频率和速度太快,导致人的肉眼看不到。
每个程序就是进程, 而每个进程中会有多个线程,而CPU是在这些线程之间进行切换。 了解了CPU对一个任务的执行过程,我们就必须知道,多线程可以提高程序的运行效率,但不能无限制的开线程。
5.实现线程的两种方式
在java语言中实现线程两种方式:继承Thread的方式,实现Runnable接口方式。
6.java同步关键词解释
synchronized
加同步格式:
synchronized( 需要一个任意的对象(锁) ){
代码块中放操作共享数据的代码。
}
synchronized缺点
synchronized是java中的一个关键字,是java语言内置的特性。
如果一个代码块被synchronized修饰时,当一个线程获取对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取所得线程释放锁只会有两种情况:
- 获取锁的线程执行完该代码块,然后线程释放对锁的占有;
- 线程执行发生异常,此时JVM会让线程自动释放锁。
Lock
Lock和synchronized的区别
- lock不是java语言内置的,synchronized是java语言的关键字,因此具有内置特性。lock是一个类,通过这个类可以实现同步访问;
- lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可以导致出现死锁的现象。
Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
api方法省略
ReentrantLock
直接使用lock接口的话,我们需要实现很多方法,不太方便,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法,ReentrantLock,意思是“可重入锁”。
ReadWriteLock也是一个接口,在它里面只定义了两个方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
注意:
使用读写锁,可以实现读写分离锁定,读操作并发进行,写操作锁定单个线程。
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
示例:
代码语言:javascript
复制public class MyReentrantReadWriteLock {
private ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
public static void main(String[] args) {
final MyReentrantReadWriteLock test = new MyReentrantReadWriteLock();
Thread t1=new Thread(){
public void run() {
test.get(Thread.currentThread());
test.write(Thread.currentThread());
}
};
Thread t2=new Thread(){
public void run() {
test.get(Thread.currentThread());
test.write(Thread.currentThread());
}
};
t1.start();
t2.start();
}
/**
* 读操作,用读锁来锁定
* @param thread
*/
public void get(Thread thread) {
reentrantReadWriteLock.readLock().lock();
try {
int i=5;
while(i>0) {
i--;
System.out.println(thread.getName() "读");
}
System.out.println(thread.getName() "读操作完毕");
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}
/**
* 写操作,用写锁来锁定
* @param thread
*/
public void write(Thread thread) {
reentrantReadWriteLock.writeLock().lock();;
try {
int i=5;
while(i>0) {
i--;
System.out.println(thread.getName() "写");
}
System.out.println(thread.getName() "写操作完毕");
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
}
运行结果:
Lock和synchronized的选择
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
7.java并发包
JDK5.0 以后的版本都引入了高级并发特性,大多数的特性在java.util.concurrent 包中,是专门用于多线程发编程的,充分利用了现代多处理器和多核心系统的功能以编写大规模并发应用程序。主要包含原子量、并发集合、同步器、可重入锁,并对线程池的构造提供了强力的支持。
线程池
线程池的5中创建方式:
Single Thread Executor : 只有一个线程的线程池,因此所有提交的任务是顺序执行,
代码: Executors.newSingleThreadExecutor()
Cached Thread Pool : 线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除,
代码:Executors.newCachedThreadPool()
Fixed Thread Pool : 拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,
代码: Executors.newFixedThreadPool(4)
在构造函数中的参数4是线程池的大小,你可以随意设置,也可以和cpu的核数量保持一致,获取cpu的核数量int cpuNums = Runtime.getRuntime().availableProcessors();
Scheduled Thread Pool : 用来调度即将执行的任务的线程池,可能是不是直接执行, 每隔多久执行一次... 策略型的
代码:Executors.newScheduledThreadPool()
Single Thread Scheduled Pool : 只有一个线程,用来调度任务在指定时间执行,代码:Executors.newSingleThreadScheduledExecutor()
线程池的使用
提交 Runnable ,任务完成后 Future 对象返回 null
调用excute,提交任务, 匿名Runable重写run方法, run方法里是业务逻辑
提交 Callable,该方法返回一个 Future 实例表示任务的状态
调用submit提交任务, 匿名Callable,重写call方法, 有返回值, 获取返回值会阻塞,一直要等到线程任务返回结果
callable 跟runnable的区别:
runnable的run方法不会有任何返回结果,所以主线程无法获得任务线程的返回值
callable的call方法可以返回结果,但是主线程在获取时是被阻塞,需要等待任务线程返回才能拿到结果。
并发编程的一些总结
7.1不应用线程池的缺点
有些开发者图省事,遇到需要多线程处理的地方,直接new Thread(...).start(),对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有些隐患:
- 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是挺大的,决不同于新建一个对象
- 资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量,这样有潜在的线程数据巨大的可能,那么资源消耗量将是巨大的。
- 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题
7.2制定执行策略
在每个需要多线程处理的地方,不管并发量有多大,需要考虑线程的执行策略
- 任务以什么顺序执行
- 可以有多少个任务并发执行
- 可以有多少个任务进入等待执行队列
- 系统过载的时候,应该放弃哪些任务?如何通知到应用程序?
- 一个任务的执行前后应该做什么处理
7.3线程类型
不管是通过Executors创建线程池,还是通过Spring来管理,都得清楚知道有哪几种线程池:
- FixedThreadPool:定长线程池,提交任务时创建线程,直到池的最大容量,如果有线程非预期结束,会补充新线程
- CachedThreadPool:可变线程池,它犹如一个弹簧,如果没有任务需求时,它回收空闲线程,如果需求增加,则按需增加线程,不对池的大小做限制
- SingleThreadExecutor:单线程。处理不过来的任务会进入FIFO队列等待执行
- SecheduledThreadPool:周期性线程池。支持执行周期性线程任务
- 其实,这些不同类型的线程池都是通过构建一个ThreadPoolExecutor来完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory这么几个参数。具体可以参见JDK DOC。
7.4线程池饱和策略
由以上线程池类型可知,除了CachedThreadPool其他线程池都有饱和的可能,当饱和以后就需要相应的策略处理请求线程的任务,比如,达到上限时通过ThreadPoolExecutor.setRejectedExecutionHandler方法设置一个拒绝任务的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy几种策略,具体差异可见JDK DOC
7.5线程无依赖性
多线程任务设计上尽量使得各任务是独立无依赖的,所谓依赖性可两个方面:
- 线程之间的依赖性。如果线程有依赖可能会造成死锁或饥饿
- 调用者与线程的依赖性。调用者得监视线程的完成情况,影响可并发量
当然,在有些业务里确实需要一定的依赖性,比如调用者需要得到线程完成后结果,传统的Thread是不便完成的,因为run方法无返回值,只能通过一些共享的变量来传递结果,但在Executor框架里可以通过Future和Callable实现需要有返回值的任务,当然线程的异步性导致需要有相应机制来保证调用者能等待任务完成,关于Future和Callable的用法前文已讲解;