基本概念
进程:一个执行单元,PC和移动设备指一个程序或者应用
线程:操作系统能进行运算调度的最小单位。是进程的实际运作单位。
多线程:并发执行,比如单线程完成一个任务100毫秒,十个线程并发执行只需10毫秒
补充:
1、join底层是调用wait,会释放锁
2、sleep、yield不会释放锁
线程包括五种状态:
- 新建状态(New):线程被创建
- 就绪状态(Runnable):线程调用start方法,随时可获取CPU使用权
- 运行状态(Running):已获得CPU使用权的线程
- 阻塞状态(Blocked):失去CPU使用权
- 死亡状态(Dead):线程执行完或者异常退出
1、实现线程的方式:
- 继承Thread重写run方法
- 实现Runnable--run方法
- 实现Callable--call方法
2、synchrized关键字
2.1、实例锁和全局锁
实例锁:是对象锁
全局锁:是类锁,无论有多少个实例对象,都共享一个锁
2.2、当一个线程获取了锁,其他线程访问synchrized修饰的方法或者代码块将会被阻塞,但仍然可以访问非同步代码。被阻塞的线程需要占有锁线程释放锁,才有可能重新获取锁,进入就绪状态。
2.3、当占有锁的线程出现异常,JVM会自动释放锁
建议:synchrized代码块使用起来会比synchronized方法要灵活得多,更高效。因为synchronized方法是对整个方法进行synchronized同步,而synchronized代码块只需对部分代码同步
3、lock
3.1、synchrized和lock的区别
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
- synchrized自动释放锁,lock手动释放锁,需要在finally块中释放锁(unlock)
- lock可以知道有没有成功获取锁,如果没有获取锁可以执行其他代码,synchrized不行(tryLock)
- lock可以让等待锁的线程相应中断(lockInterruptibly),而synchrized会一直等待
- Lock可以提高多个线程进行读操作的效率。(ReadWriteLock)
3.2、lock相关方法
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock() 获取锁
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
unlock 释放锁。获取锁和释放锁是配对使用的,如果没有及时释放锁,那么线程就会发生死锁。unlock通常是放在finally中。
newCondition 创建Condition对象,Condition会在4.6讲到
ReentrantLock是lock唯一实现类
3.4、ReadWriteLock读写锁
ReadWriteLock也是一个接口
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
readLock()获取读取锁
writeLock()获取写锁
可以实现多线程进行读操作,值得注意的是
- 如果有一个线程占用了读操作,申请写操作的线程,必须等待读线程释放锁。
- 如果有写线程在操作,其他申请读锁或者写锁的线程必须等待写线程释放写锁
ReentrantReadWriteLock实现ReadWriteLock接口
3.5 公平锁和非公平锁
公平锁:多个线程等待一个锁,等待最久的线程先获取锁
非公平锁:跟公平锁相反,无法保证等待最久的线程先获取锁。
synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
ReentrantLock和ReentrantReadWriteLock相似,以ReentrantLock为例:
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
可以通过以下构造函数设置锁的公平性
ReentrantLock lock = new ReentrantLock(true);
另外在ReentrantLock类中定义了很多方法,比如:
- isFair() //判断锁是否是公平锁
- iisLocked() //判断锁是否被任何线程获取了
- iisHeldByCurrentThread() //判断锁是否被当前线程获取了
- ihasQueuedThreads() //判断是否有线程在等待该锁
4、Thread
4.1 start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
4.2 run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
4.3 yield
线程让步,让出CPU权,从运行状态进入到就绪状态,不会释放锁。让其他具有相同优先级的线程获取执行权,但不保证其他具有相同优先级的线程一定能获取执行权,也可以是当前线程重新获取执行权继续运行
4.4 sleep
线程休眠,从运行状态进入进入阻塞状态,但不会释放锁,休眠结束,会由阻塞状态变成就绪状态
4.5 join
让子线程执行完,再执行父线程,由于底层是调用wait方法,所以会释放锁
4.6 wait、norify
在Object.java中,定义了wait(), notify()和notifyAll()等接口
- notify()//唤醒在此对象监视器上等待的单个线程。
- notifyAll() //唤醒在此对象监视器上等待的所有线程。
- wait() // 让当前线程处于“等待(阻塞)状态”(当前线程指正在cpu上运行的线程),“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
- wait(long timeout) // 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
注意:wait、notify调用的前提是当前线程是synchrized锁的拥有者
为什么notify(), wait()等函数定义在Object中,而不是Thread中?
notify和wait是通过“同步锁进行关联的”,只有notify才能唤醒wait线程,但是等待线程还不能执行,必须等到唤醒线程释放锁,才可能重新获取锁从而继续运行。
补充学习condition
condition和wait、notify区别
- 1、Condition的await()、signal()、signalAll()对应Object的wait(), notify()和notifyAll()
- 2、wait、norify是和“同步锁”synchronized关键字捆绑使用,condition是和"互斥锁"/"共享锁"ReentrantLock捆绑使用。
- 3、Condition依赖于Lock接口,通过lock.newCondition获取,Condition可以有多个。(例如生产消费者问题的仓库空消费者线程等待,仓库满生产者线程等待)
Condition能够更强大的控制多线程的休眠和唤醒,同一个锁,可以创建多个Condition,在不同情况下使用不同Condition
4.7 interrupt 线程中断和线程终止方式
interrupt方法可以中断阻塞线程,不能中断运行线程
4.7.1 终止处于阻塞状态的线程
@Override
public void run() {
try {
while (true) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 由于产生InterruptedException异常,退出while(true)循环,线程终止!
}
}
4.7.2 终止处于运行状态的线程
interrupt方法不能中断运行线程,所以如果我们需要中断运行线程,需要额外增加标记(可以借助isInterrupted方法)
@Override
public void run() {
while (!isInterrupted()) {
// 执行任务...
}
}
4.7.3 interrupted() 和 isInterrupted()的区别
interrupted() 和 isInterrupted()都能够用于检测对象的“中断标记”。区别是interrupted()还会清楚中断标记,isInterrupted()不会
4.8 线程属性方法
4.8.3 getId
用来得到线程ID
4.8.3 getName和setName
用来得到或者设置线程名称。
4.8.3 getPriority和setPriority
获取优先级和设置优先级
线程优先级范围是0~10,默认优先级是5,“高优先级线程”会优先于“低优先级线程”执行。
4.8.4 setDaemon和isDaemon
设置守护线程和判断是否是守护线程
用户线程和守护线程(也就是后台线程),垃圾收集器线程就是守护线程,当只有守护线程在运行或者调用exit方法,jvm会自动退出。
5、volatile
由于volatile跟java内存模型有关,先了解内存模型相关概念
内存模式:程序运行过程中的临时变量存储在主存里(物理内存),而变量CPU执行速度远大于从内存中存取数据,如果每次操作数据都要和内存交互,会大大降低指令执行速度。于是就有了高速缓存。当程序运行时,会复制一份到高速缓存中,CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
JAVA内存模式:每个线程都有自己的工作内存(类似高速缓存),运行时会复制一份到工作内存中,线程对变量的操作必须在工作内存中进行,不能直接对主存进行操作。
5.1 并发的三个概念
想要并发正确执行必须保证其原子性、可见性、有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
5.1.1 原子性
一个操作要么都执行,要么都不执行。
x = x+1;
举个例子,这里就有三个操作,读取x的值,x+1,写入新值
原子性操作有:读取、赋值、synchrized或者lock实现、JUC原子类
5.1.2 可见性
多个线程共享一个变量,当一个线程改变了这个变量的值,其他线程能立即看到修改的值
volatile具备可见性
可见性操作有:volatile、synchrized或者lock实现
5.1.3 有序性
处理器为了提升运行效率,可能会对执行顺序进行重排序,会保证程序最终结果会和代码顺序执行结果相同。
原因在于,处理器在进行重排序时,会考虑到指令之间的依赖性。如果指令2必须用到指令1的结果,那么处理器会保证指令1在指令2之前执行。
volatile具备有序性
有序性操作有:volatile、synchrized或者lock实现
5.2 volatile
特点:可见性,有序性(禁止指令重排序)
原理和实现机制
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。其他线程再次读取共享变量会从内存读取