听说往年课的最后都讲了有关多线程的知识,但是由于我们这届学时缩短就省略了,于是我自己找了些资料学习了一下。
1. 一些基本概念
1.进程与线程
程序是静态的;
进程是程序的一次执行过程,是动态的,是系统资源分配的单位
在一个进程中包括若干个线程,且至少有一个线程
线程是CPU调度与执行的单位
2.很多多线程并非是多CPU(多核),而是一个CPU模拟的,在同一个时间点,
CPU只能执行一段代码,切换的很快
3.线程是独立的执行路径
4.在程序运行时,即使没有创建线程,后台也会有多个线程比如主线程main,gc线程
5.主线程main作为系统的入口,用于执行整个程序
6.在一个进程中,如果开辟了多个线程,线程的运行由调度器(CPU)安排,是与操作系统密切相关的,先后顺序是不能人为干预的
7.对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
8.线程会带来额外的开销,如CPU调度时间,并发控制开销
9.每个线程在自己工作内存交互,内存控制不当会造成数据不一致
2. Java多线程
线程的三种实现方式:
1.继承Thread类
2.实现Runnable接口(Thread类就实现了Runnable接口)
3.实现Callable接口
1.继承Thread类:
(1)自定义线程类继承Thread类
(2)重写run方法,编写线程执行体
(3)创建对象,调用start启动线程
注意:调用start是多线程同时进行,而直接调用run就是分布按顺序进行
2.实现Runnable接口:
(1)自定义资源类实现Runnable接口
(2)重写run方法,编写线程执行体
(3)创建Thread对象与资源对象,代理资源类调用start方法
出于单继承局限性,可复用性的考虑,推荐使用第二种
采用runnable接口时,共用资源可以就放在资源类里。
我的理解是,线程对象仍为Thread,只不过进行了资源类里的线程体,操作了资源类中的成员
3.实现Callable接口
(0).编写资源类实现接口
(1).重写call方法(有返回值)(需要抛出异常)implements Callable<返回值类型>
(2).创建资源类实例
(3).开启服务(创建服务对象)
(4).利用服务对象提交资源实例并获取返回值(两步)
(5).关闭服务
3. 静态代理模式
1.三个:目标功能接口;真实对象;代理商
2.真实对象和代理商均实现目标功能
3.将真实对象传入代理商,代理商执行功能时也执行真实对象的功能
4.利用接口回调的方式实现可传入不同真实对象
5.好处:代理商可以完成真实对象不能完成的事,真实对象可以专注于实现目标功能,可以使用一个代理商代理多个不同真实对象,从而实现具有相同部分的不同功能
6.代理商:Thread;真实对象:资源类;目标功能:创建进程
4. 线程的使用
1.线程五大状态:创建(Thread类实例);就绪(start方法执行);进行(CPU调度执行);阻塞;死亡
2.线程的停止
(1).不推荐使用JDK自带的方法
(2).推荐自然停止(如for计数)
(3).推荐使用标识符,编写停止方法
(4).变更标识符时直接使用资源实例调用停止方法;主线程使用for计数从而控制线程时间
3.线程的休眠:Thread.sleep(毫秒),用于模拟网络延迟(放大问题),倒计时
4.线程的礼让(非静态方法)
.yield();
不一定礼让成功,需要看CPU调度
5.线程的插队
.join();
保证插队的线程先运行,其他线程进入阻塞状态
6.线程的状态监控
.getState()
返回的是枚举类实例,该枚举类是Thread的一个静态内部类
Thread.State state = thread.getState();
7.线程优先级 priority
(1).1~10之间,越大获得CPU调度的概率越大(不一定)
(2).在start之前设置
8.守护线程
(1).虚拟机需要等待用户线程执行完毕,但不用等待守护线程执行完
(2).守护线程如gc,后台记录日志等等
(3).setDaemon(true)即可,默认是false即用户进程
5. 线程的并发与锁
线程的同步 synchronized
(1).并发:同一个对象被多个线程同时操作(每个线程都有自己的工作内存,是对资源的拷贝,因此会产生不安全的问题)
(2).机制:队列:操作同一个资源的线程需要进入该对象的等待池进行排队; 锁:每个对象都有一把对象锁保证其安全性,一次只有一个线程持有该锁,其他线程无法操作
(3).一个线程持有锁会导致其他所有需要此锁的线程挂起,在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题,如果一个优先级高的线程被一个优先级低的线程拿到了锁也会引起性能倒置
(4).同步方法:使用synchronized关键字进行标识,默认锁住this
同步块:synchronized(对象引用){对其有增删改等操作的代码块}
这里的对象引用即为同步监视器,推荐使用共享资源作为监视器
Lock锁:显式定义同步锁对象实现同步,同步锁使用Lock对象充当
(1). java.util.concurrent.locks.Lock接口
(2). ReentrantLock(可重入锁)实现了Lock,显式加锁,释放锁,当线程体有异常时需要将unlock写在try{}finnally{}的f内(保证锁被释放)
(3).注意确保Lock对象的唯一性,不要因为NEW了多个对象产生了不同的锁
(4).alock.lock();.........;alock.unlock();
6. 锁的种类
1.可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下面这段代码就明白了:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
2.2.可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。
3.3.公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:
ReentrantLock lock = new ReentrantLock(true);
如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。