线程基础
创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口,Callable的call方法可以提供返回值(run方法无返回值)
sleep 、wait、yield 的区别,wait 的线程如何唤醒它?
sleep
线程休眠。让线程暂缓执行,等到预计时间之后再恢复执行。
(1)线程休眠会交出CPU,让CPU去执行其他的任务。
(2)调用sleep()方法让线程进入休眠状态后,sleep()方法并不会释放锁,即当前线程持有某个对象锁时,即使调用sleep()方法其他线程也无法访问这个对象。
(3)调用sleep()方法让线程从运行状态转换为阻塞状态;sleep()方法调用结束后,线程从阻塞状态转换为可执行状态。
yield
线程让步。暂停当前正在执行的线程对象,并执行其他线程。
(1)调用yield()方法让当前线程交出CPU权限,让CPU去执行其他线程。
(2)yield()方法和sleep()方法类似,不会释放锁,但yield()方法不能控制具体交出CPU的时间。
(3)yield()方法只能让拥有相同优先级的线程获取CPU执行的机会。
(4)使用yield()方法不会让线程进入阻塞状态,而是让线程从运行状态转换为就绪状态,只需要等待重新获取CPU执行的机会。
join
等待线程终止。指的是如果在主线程中调用该方法时就会让主线程休眠,让调用join()方法的线程先执行完毕后再开始执行主线程。
wait
线程等待。是Object的方法。
(1)wait()方法的作用是让当前正在执行的线程进入线程阻塞状态的等待状态,该方法时用来将当前线程置入“预执行队列”中,并且调用wait()方法后,该线程在wait()方法所在的代码处停止执行,直到接到一些通知或被中断为止。
(2)wait()方法只能在同步代码块或同步方法中调用,故如果调用wait()方法时没有持有适当的锁时,就会抛出异常。
(3)wait()方法执行后,当前线程释放锁并且与其他线程相互竞争重新获得锁。
(4)wait必须在synchronized
代码块中使用
notify
线程唤醒。Object的方法。
(1)notify()方法要在同步代码块或同步方法中调用。
(2)notify()方法是用来通知那些等待该对象的对象锁的线程,对其调用wait()方法的对象发出通知让这些线程不再等待,继续执行。
(3)如果有多个线程都在等待,则由线程规划器随机挑选出一个呈wait状态的线程将其线程唤醒,继续执行该线程。
(4)调用notify()方法后,当前线程并不会马上释放该对象锁,要等到执行notify()方法的线程执行完才会释放对象锁。
notifyAll
唤醒所有等待方法。Object的方法
Java 线程有哪些状态
- 状态 英文表示 达成条件
- 新建状态 New 创建线程
- 就绪状态 Runnable 调用start方法,等待资源和CPU
- 运行状态 Running 获得CPU时间时间
- 阻塞状态 Blocked 调用方法wait,阻塞IO,等待线程锁等
- 等待状态 WAITING 调用方法wait,join,LockSupport.park中的一个主动等待
- 等待状态 TIMED_WAITING 主动等待指定时间的状态,调用sleep(long),join(long),wait(long),LockSupport.parkNanos,LockSupport.parkUntil等方法中的一个后
- 死亡状态 TERMINATED run方法执行结束,或者抛出异常
进程和线程的区别
- 进程是CPU资源分配的最小单位,线程是cpu调度的最小单位。
- 进程之间不能共享资源,而线程共享进程的空间和其他资源。
- 一个进程可以有多个线程,进程可以开启进程也可以开启线程。
- 一个线程只属于一个进程,线程可以直接使用同进程的资源,线程依赖于进程存在。
线程安全
什么是线程安全问题
多个线程访问同一资源时,在某个线程对资源进行线程操作中途,其他的线程也要也要对这个线程进行读写操作引起的数据问题。
保证线程(并发)安全的几种方法
保证线程安全需要保证:
- 原子性
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行 - 可见性
一个线程对值做出更改,其他线程需要能看到这个值以及被改变 - 有序性
程序的执行要和代码的顺序一致。
对于线程安全,一般的做法是进行同步操作,在某一个时间点,只让一个线程来进行操作
保证线程安全的方法的种类
- 加锁
- 缓存一致性协议
保证线程安全的方法(6种)
- synchronized
保证方法内部或代码块内部资源的访问时互斥的。同一时间内,只有一个线程在访问资源。 - volatile
计算机的的指令在CPU中执行操作的,cup执行速度很快,读写内存的速度相对要慢的多,频繁的读写会大大降低指令的执行速度。因此CPU提供了高速缓存。如果一个变量在多个cpu都存在缓存的时候,那么就可能存在缓存不一致的情况。
volatile保证被修饰的变量每次都被读写到内存中(可见性),当其他线程去读取变量时都能读到最新的值。1
2
3
4
5//线程1
boolean stop = false;
while(!stop){
doSomething();
}如果不使用,volatile修饰,线程2可能无法修改线程1中的stop值。1
2//线程2
stop = true;
使用场景:(1)状态标记(如上);(2)doubleCheck
java.util.concurrent.atomic 包下的类
AtomicInteger AtomicBoolean等,等同于volatile变量Lock/ReentrantReadWriteLock
加锁机制,比synchronized麻烦,不推荐使用
1 | lock.lock(); |
- 使用ThreadLocal
- 使用阻塞队列
Java线程中各种锁的概念
悲观锁和乐观锁
悲观锁
总是假设最坏的情况,每次读取数据时,总是假设有其他人更改数据,每次读取数据时都加锁,这样别人读写数据时就会阻塞直到拿到锁才可以读写数据。
使用场景:一般用于写数据比较多的情况下。
锁实现:java提供的synchronized,和Lock类。
乐观锁
总是假设最好的情况,每次读数据时,总是假设别人不会更改数据,不会上锁,但是每次读数据时,需要检查数据是否被更改过。
使用场景:需要频繁读数据,但是写数据比较少的情况,可提高读数据的效率。
实现:可以使用版本号机制和CAS算法实现乐观锁。java的AtomicXXX类使用了CAS算法保证线程安全。
阻塞和自旋锁
是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断是否能够被成功获取,自旋直到获取到锁才会退出循环。自旋是通过CAS算法进行的。
CAS算法
CAS(Compare and Swap)是一种有名的无锁算法。
核心思想是:比较和交换。比较和交换是线程中的数据和内存中的数据之间的操作。
CAS有3个操作数,线程要操作的内存值V,每个线程读取内存中的旧值A,线程要写入的新值B。
每个线程写入数据B前,都要比较V和A的值:
(1)如果相同,则将V的值改为B。
(2)如果不同,说明V的值被改过,那么根据A的值计算的结果也是错的,需要重新获取V的值,重新计算比较。
CAS算法消耗CPU资源在于写数据遇到自旋的过程中一直占用CPU,而阻塞对CPU的消耗在于操作系统要频繁的切换CPU上下文。
因此,在写操作比较少的情况下,不加锁效率会更高。
ABA问题
执行CAS算法时,存在当V和A的值相同,但是V值已被改过的情况,仍然会出现线程安全的问题。如下图所示
解决办法:对变量加版本号,每次修改时,每次变量更新是版本号+1.
无锁、偏向锁、轻量级锁、重量级锁
随着竞争不断的加剧,锁要不断的升级。
(1)无锁:适用于单线程
(2)偏向锁:适用于只有一个线程访问同步块的情况,因为多个线程同时访问同步块,给某一个线程特权是不合理的
(3)轻量级锁:竞争不是太多,循环等待消耗CPU资源的线程的数量在可接受的范围
(4)重量级锁:多个线程同时竞争资源,只让一个线程运行,其余的线程都阻塞.
可重入锁和非可重入锁
重入锁:一个线程获取对象锁以后,这个线程可以再次获取本对象上的锁,其他线程不可以。
引入目的:一般在递归调用中使用可重入锁,可以防止死锁。
实现方式:是为每个锁关联一个计数器和占有它的线程。
- 计数器为0时,锁未被占有;
- 线程获取锁对象后,JVM记录锁的占有者,同一个线程多次请求可再次获取锁,并累加计数器。
- 占用线程退出同步块,计数器递减,直到计数器为0时,锁被释放。
- java中的实现*:synchronized和ReentrantLock 都是可重入锁。
不可重入锁实现:
1 | public class Lock{ |
可重入锁实现
1 | public class Lock{ |
synchronized和ReentrantLock比较:
- 前者使用灵活,但是必须手动开启和释放锁
- 前者扩展性好,有时间锁等候(tryLock( )),可中断锁等候(lockInterruptibly( )),锁投票等,适合用于高度竞争锁和多个条件变量的地方
- 前者提供了可轮询的锁请求,可以尝试去获取锁(tryLock( )),如果失败,则会释放已经获得的锁。有完善的错误恢复机制,可以避免死锁的发生。
公平锁和非公平锁
引入目的:解决等待线程的排队问题
公平锁: 内部维护了一个FIFO队列,先申请优先获得锁。
非公平锁:申请锁的线程可能插队,后申请锁的线程可能先拿到锁。
在jiava中,可重入锁可以指定锁是否公平
1 | public ReentrantLock(boolean fair) |
AQS(AbstractQueuedSynchronizer)
ReentrantLock使用AQS来解决线程的排队问题。
组成
- State:当前线程锁的个数。
- exclusiveOwerThread:当前占有锁的线程。
- CLH队列等待运行的线程。
无法进入队列的线程,进入ArrayBlockingQueue,等队列有空位再进入队列
互斥锁和共享锁
互斥锁:在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
共享锁:共享锁从字面来看也即是允许多个线程共同访问资源。
场景:读者写者模式,一个人写,多人读。
实现:Semapore信号量和ReadLock读锁
死锁
多个线程竞争资源,导致互相等待的结果。
产生死锁的原因:
- 系统资源竞争
- 请求和释放资源的顺序不当
- 信号量使用不当引起的死锁
死锁产生的必要条件:
- 互斥条件:进程要求的资源有排他性。即一段时间内,该进程仅由一个进程占有,其他进程请求该资源,只能等待。
- 不剥夺条件:进程获得的资源未使用完成前,不可被其他进程剥夺。
- 请求和保持条件:进程已经占有至少一个资源,但又需求被其他进程占用的资源而阻塞,也不是释放当前已有的资源。
- 循环等待条件:存在一种线程资源的循环等待链,链中的每个线程已获得资源同时被下一个线程等待。
如何避免死锁
- 线程按照一定的顺序加锁
- 设置加锁时限,若超过时间释放资源
- 死锁检测
常见阻塞队列
1、BlockingQueue (常用)
获取元素的时候等待队列里有元素,否则阻塞
保存元素的时候等待队列里有空间,否则阻塞
用来简化生产者消费者在多线程环境下的开发
** 2、ArrayBlockingQueue (数组阻塞队列)
FIFO、数组实现
有界阻塞队列,一旦指定了队列的长度,则队列的大小不能被改变
在生产者消费者例子中,如果生产者生产实体放入队列超过了队列的长度,则在offer(或者put,add)的时候会被阻塞,直到队列的实体数量< 队列的
初始size为止。不过可以设置超时时间,超时后队列还未空出位置,则offer失败。
如果消费者发现队列里没有可被消费的实体时也会被阻塞,直到有实体被生产出来放入队列位置,不过可以设置等待的超时时间,超过时间后会返
回null
**3、DelayQueue (延迟队列)
有界阻塞延时队列,当队列里的元素延时期未到是,通过take方法不能获取,会被阻塞,直到有元素延时到期为止
如:
1.obj 5s 延时到期
2.obj 6s 延时到期
3.obj 9s 延时到期
那么在take的时候,需要等待5秒钟才能获取第一个obj,再过1s后可以获取第二个obj,再过3s后可以获得第三个obj
这个队列可以用来处理session过期失效的场景,比如session在创建的时候设置延时到期时间为30分钟,放入延时队列里,然后通过一个线程来获 取这个队列元素,只要能被获取到的,表示已经是过期的session,被获取的session可以肯定超过30分钟了,这时对session进行失效。
4、LinkedBlockingQueue (链表阻塞队列)
FIFO、Node链表结构
可以通过构造方法设置capacity来使得阻塞队列是有界的,也可以不设置,则为无界队列
其他功能类似ArrayBlockingQueue
5、PriorityBlockingQueue (优先级阻塞队列)
无界限队列,相当于PriorityQueue + BlockingQueue
插入的对象必须是可比较的,或者通过构造方法实现插入对象的比较器Comparator<? super E>
队列里的元素按Comparator<? super E> comparator比较结果排序,PriorityBlockingQueue可以用来处理一些有优先级的事物。比如短信发送优先 级队列,队列里已经有某企业的100000条短信,这时候又来了一个100条紧急短信,优先级别比较高,可以通过PriorityBlockingQueue来轻松实现 这样的功能。这样这个100条可以被优先发送
ThreadLocal
每个线程有一份自己独立的变量,使得线程的变量值变化不会互相影响。