概念
Java 5.0 提供了java.util.concurrent包,JUC是这个包的简称,这里面有很多并发编程时需要的工具类。
线程:
通常一个进程中包含若干个线程,分配资源的基本单位。
进程:
具有一定独立功能的程序。
并发:
在同一时刻访问同一个资源。
并行:
多个工作一起执行,之后汇总到一起,是一个整体。
wait:释放资源锁,属于Object类
sleep:不释放资源锁,属于Thread类
非JUC创建线程
继承Thread类
重写run方法。
对象.start()启动线程。
实现Runnable接口
实现run方法。
将匿名对象装入Thread构造方法中创建一个线程对象。(也可以直接使用匿名内部类实现Runnable接口)
lambda表达式:
Runnable是函数式接口(SAM),这个接口中只有一个抽象方法。
为了规范函数式接口,可以在接口上加上@FunctionalInterface注解,必须只能有一个抽象方法,否则会报错。
编写lambda表达式的口诀:复制小括号,写死右箭头,落地花括号!
new Thread(()->{ //实现run方法 },"线程名").start();箭头左边(),参数列表
箭头右边{},方法体
函数式接口中是否可以编写其他方法?
在java1.8之后可以写多个已实现的默认方法、静态方法。
但是静态方法是随着类加载而加载的,且优于对象存在,可以直接类名.方法()访问,所以只能通过接口名.方法名()访问。
然后使用对象.start();启动线程。
synchronized实现同步的基础:Java中的每一个对象都可以作为锁,具体表现为以下三种形式。
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的class对象
- 对于同步方法快,锁是synchronized括号里配置的对象。
当一个线程视图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时必须释放锁。
这也就是说:
如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁;可是不同实例对象的非静态同步方法因为用的是不同对象的锁,所以不需要等待其他实例对象的非静态同步方法释放锁,就可以获取自己的锁。
所有的静态同步方法用的是同一把锁——类对象本身。不管是不是同一个实例对象,只要是一个类的对象,一旦一个静态同步方法获取锁之后,其他对象的静态同步方法,都必须等待该方法释放锁之后,才能获取锁。
而静态同步方法(Class对象锁)和非静态同步方法(实例对象锁)之间是不会有竞争条件的。
Lock锁
相比同步锁,JUC中的Lock锁的功能更加强大,它提供了各种各样的锁(公平锁、非公平锁、共享锁、独占锁。。),所以用起来很灵活。
Lock是一个接口,这里主要有三个实现:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
ReentrantLock可重入锁
卖票程序
class Ticket{
private Integer number = 20;
private ReentrantLock lock = new ReentrantLock();
public void sale(){
lock.lock();
if (number <= 0) {
System.out.println("票已售罄!");
lock.unlock();
return;
}
try {
Thread.sleep(200);
number--;
System.out.println(Thread.currentThread().getName() + "买票成功,当前剩余:" + number);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
ReentrantLock的加锁和释放锁是手动的,通过lock()方法和unlock()方法完成。
可重入性
可重入锁又名递归锁。是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法会自动获取锁。
在Java中ReentrantLock和synchronized都是可重入锁
可重入锁的一个优点是可以在一定程度上避免死锁。
公平锁
ReentrantLock还能够实现公平锁。
公平锁:就是在锁上等待时间最长的线程将获得锁的使用权。
ReentrantLock的无参构造声明的是非公平锁。
我们可以使用其中一个有参构造来声明公平锁
private ReentrantLock lock = new ReentrantLock(true);
参数是true说明是公平锁,否则是非公平锁。
限时等待
通过tryLock()方法可以实现限时等待。
可以选择传入时间参数,表示指定等待的时间,无参就表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。
我们可以通过这种方法来解决死锁问题。
ReentrantLock和synchronized的区别
- ReentrantLock和synchronized都是独占锁和可重入锁,但是synchronized是JVM执行加锁和解锁操作,而ReentrantLock锁是程序员手动的加锁和解锁,所以需要注意加锁和解锁的次数必需要一致。
- synchronized不可响应中断,一个线程获取不到锁就一直等待。ReentrantLock可以响应中断。
ReentrantReadWriteLock读写锁
在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话不会影响数据正确性。如果在这种业务场景下,依然使用独占锁的话,很显然会出现性能瓶颈。
针对这种读多写少的情况,Java还提供了另一种实现了Lock接口的ReentrantReadWriteLock(读写锁)。
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
特点
- 写写不可并发
- 读写不可并发
- 读读可以并发
重写读写问题
以缓存为例用代码限时读写锁,重现问题:
class MyCache{
private volatile Map<String, String> cache= new HashMap<>();
public void put(String key, String value){
try {
System.out.println(Thread.currentThread().getName() + " 开始写入!");
Thread.sleep(300);
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入成功!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
public void get(String key){
try {
System.out.println(Thread.currentThread().getName() + " 开始读出!");
Thread.sleep(300);
String value = cache.get(key);
System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
}
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 1; i <= 5; i++) {
String num = String.valueOf(i);
// 开启5个写线程
new Thread(()->{
cache.put(num, num);
}, num).start();
}
for (int i = 1; i <= 5; i++) {
String num = String.valueOf(i);
// 开启5个读线程
new Thread(()->{
cache.get(num);
}, num).start();
}
}
}
打印结果:需要多执行几次,有很大概率不会出问题。
可以发现的是写操作不具有原子性,这是由于执行过程中被读和其他的写操作打断,甚至可能出现null值,这是因为读先执行了。
使用读写锁优化
class MyCache{
private volatile Map<String, String> cache= new HashMap<>();
// 加入读写锁
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void put(String key, String value){
// 加写锁
rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始写入!");
Thread.sleep(500);
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入成功!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
rwl.writeLock().unlock();
}
}
public void get(String key){
// 加入读锁
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始读出!");
Thread.sleep(500);
String value = cache.get(key);
System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}
锁降级
就是从写锁降级为读锁。
在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。
总结
-
支持公平/非公平策略
-
支持可重入
- 同一读线程在获取了读锁后还可以获取读锁
- 同一写线程在获取了写锁后还可以再次获取写锁又可以获取读锁
-
支持锁降级,不支持锁升级
-
读写锁如果使用不当,很容易产生"饥饿"问题
在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用公平策略可以一定程度上缓解这个问题,但是公平策略是以牺牲系统吞吐量为代价的。
-
Condition条件支持
写锁可以通过newCondition()方法获取Condition对象。但是读锁没法获取Conditon对象,读锁调用newCondition()方法会直接抛出UnsupportedOperationException。
线程间通信
案例1:
两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。
线程间通信模型:
- 生产者+消费者
- 通知等待唤醒机制
多线程编程模板中:
- 判断
- 业务
- 通知
代码实现:
class ShareDataOne {
private Integer number = 0;
/**
* 增加1
*/
public synchronized void increment() throws InterruptedException {
// 1. 判断
if (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
/**
* 减少1
*/
public synchronized void decrement() throws InterruptedException {
// 1. 判断
if (number != 1) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
}
/**
* 现在两个线程,
* 可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量减1,
* 交替,来10轮。
*
* 笔记:Java里面如何进行工程级别的多线程编写
* 1 多线程变成模板(套路)-----上
* 1.1 线程 操作 资源类
* 1.2 高内聚 低耦合
* 2 多线程变成模板(套路)-----中
* 2.1 判断
* 2.2 干活
* 2.3 通知
*/
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDataOne shareDataOne = new ShareDataOne();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BBB").start();
}
}
部分打印结果:AAA和BBB交互执行,执行结果是1 0 1 0 ...一共10轮
如果换成4个线程呢?
改造main方法,加入CCC和DDD两个线程:
public static void main(String[] args) {
ShareDataOne shareDataOne = new ShareDataOne();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CCC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "DDD").start();
}
打印结果,依然会有概率是,10101010...。但是,多执行几次,也会出现错乱的现象,可能会导致虚假唤醒
虚假唤醒
换成4个线程会导致错误:虚假唤醒
原因:在java多线程判断时,不能用if,程序的问题出在判断上面。
注意,消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。
如何解决虚假唤醒?
if改成while
查看API,java.lang.Object的wait方法。
中断和虚假唤醒是可能产生的,所以要用loop循环,if只判断依次,while是只要唤醒就要拉回来再判断一次。
线程通信(Condition)
synchronized锁有wait和notify两个方法(等待和唤醒),那么lock接口也有相同的机制Condition。
Condition对象由Lock对象的newCondition()方法获取。
Condition类的await()方法对应Object类的wait()方法。
Condition类的signalAll()方法对应Object类的notifyAll()方法。
那么可以将上一个代码用Lock实现
class ShareDataOne {
private Integer number = 0;
final Lock lock = new ReentrantLock(); // 初始化lock锁
final Condition condition = lock.newCondition(); // 初始化condition对象
/**
* 增加1
*/
public void increment() throws InterruptedException {
lock.lock(); // 加锁
try {
// 1. 判断
while (number != 0) {
// this.wait();
condition.await();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
// this.notifyAll();
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 减少1
*/
public void decrement() throws InterruptedException {
lock.lock();
try {
// 1. 判断
while (number != 1) {
// this.wait();
condition.await();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
//this.notifyAll();
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
定制化调用通信
案例:
多线程之间按顺序调用,实现A->B->C。三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
。。。打印10轮
分析实现方式:
- 有一把锁Lock,3把钥匙Condition
- 有顺序通知(切换线程),需要有标志位
- 判断标志位
- 输出线程名+内容
- 修改标识符,通知下一个
代码实现
class ShareDataTwo {
private Integer flag = 1; // 线程标识位,通过它区分线程切换
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (flag != 1) {
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
}
flag = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (flag != 2) {
condition2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
}
flag = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (flag != 3) {
condition3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
}
flag = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 多线程之间按顺序调用,实现A->B->C
* 三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次
* 接着
* AA打印5次,BB打印10次,CC打印15次
* ......来10轮
*/
public class ThreadOrderAccess {
public static void main(String[] args) {
ShareDataTwo sdt = new ShareDataTwo();
new Thread(()->{
for (int i = 0; i < 10; i++) {
sdt.print5();
}
}, "AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
sdt.print10();
}
}, "BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
sdt.print15();
}
}, "CCC").start();
}
}
并发容器类
常用的实现类ArrayList其实是线程不安全的。
ArrayList在多个线程同时对其进行修改的时候,就会抛出java.util.ConcurrentModificationException异常(并发修改异常),因为ArrayList的add及其他方法都是线程不安全的。
解决方案:
List接口有很多实现类,除了常用的ArrayList之外还有Vector和SynchronizedList。他们都有synchronized关键字,说明都是线程安全的。
Vector和Synchronized的缺点:
vector:内存消耗比较大,适合一次增量比较大的情况。
SynchronizedList:迭代器涉及的代码没有加上线程同步代码,启用该功能时需要程序员手动加锁。Collections.SynchronizedList(List<T> list)
CopyOnWrite容器
简称COW容器,即写时复制的容器。
通俗的理解是当我们网一个容器添加元素的适合,不直接网当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用执行新的容器。
好处是我们对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
实现类
从jdk 1.5开始Java并发包里提供两个使用CopyOnWrite机制实现的并发容器:
- CopyOnWriteArrayList(可以从源码发现add方法加了锁,是线程安全的)
- CopyOnWriteArraySet
使用场景
COW容器用于读多写少的并发场景。
比如说:白名单,黑名单。假如我们有一个搜索网站,用户再这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索,这些不能被搜索的关键字会放在一个黑名单中,黑名单一定周期才会更新一次。
缺点
- 需要足够大的内存。
- 可能会出现数据不一致的问题,不能保证数据的实时一致性,只能保证数据最终一致性。
在多线程操作下,ArrayList推荐使用CopyOnWriteList。
CopyOnWriteArrayList
代替ArrayList,保证线程安全。
CopyOnWriteArraySet
Set是线程不安全的。
CopyOnWriteArraySet和HashSet没有什么关系。
CopyOnWriteArraySet set = new CopyOnWriteArraySet()
ConcurrentHashMap
Map也是线程不安全的,会抛出java.util.ConcurrentModificationException
解决方案:ConcurrentHashMap
原理:通过synchronized保证线程安全。
HashSet底层数据结构是什么?
使用的HashMap。
set.add(value)中的value是hashMap中的key值。
JUC辅助类
CountDownLatch(倒计数器)
例如:在手机上安装一个应用程序,假如需要5个子进程检查服务授权,那么主进程会维护一个计数器,初始计数就是5。用户每同意一个授权该计数器减1,当计数减为0时,主进程才启动,否则就只有阻塞等待了。
构造器:
CountDownLatch(int i),设置初始计数
方法:
countDown() 减少计数
await() 阻塞线程。
模拟:6个同学陆续离开教室后值班同学才可以关门。
public class CountDownLatchDemo {
/**
* main方法也是一个进程,在这里是主进程,即上锁的同学
*
* @param args
*/
public static void main(String[] args) throws InterruptedException {
// 初始化计数器,初始计数为6
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
// 每个同学墨迹几秒钟
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + " 同学出门了");
// 调用countDown()计算减1
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
// 调用计算器的await方法,等待6位同学都出来
countDownLatch.await();
System.out.println("值班同学锁门了");
}
}
CountDownLatch和join方法的区别:
调用一个子线程的join()方法后,该线程会一直被阻塞直到该线程运行完毕。
而CountDownLatch则使用技术器允许子线程运行完毕或者运行中的时候递减技术,也就是CountDownLatch可以在子线程运行任何时候让await方法返回而不一定必须等待到线程结束;另外使用线程池来管理线程的时候一般都是直接添加Runnable到线程池,这个时候就没有办法在调用线程的join方法了,countDownLatch相比join方法让我们对线程同步有更灵活的控制。
CyclicBarrier(循环栅栏)
该命令只在每个屏障点运行一次。若在所有参与线程之前更新共享状态,此屏障操作很有用。
常用方法
- CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数,barrierAction一个可选的Runnable命令,该命令只在每个屏障点运行一次,可以在执行后续业务之前共享状态。该操作由最后一个进入屏障点的线程执行。
- CyclicBarrier(int parties) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数。
- await() 该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,直到所有线程都到达屏障点,当前线程才会被唤醒。
CyclicBarrier和ConutDownLatch的区别:
- CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更复杂的场景。
- CountDownLatch允许一个或者多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
Semaphore(信号量)
可以控制同时访问的线程个数。非常适合需求量大,而资源又很紧张的情况。
使用场景:
比如给定一个资源数目优先的资源池,假设资源数目为N,每一个线程均可获取一个资源,但是当资源分配完毕时,后来的线程需要阻塞等待,直到前面已持有资源的线程释放资源才能继续。
常用方法:
public Semaphore(int permits) // 构造方法,permits指资源数目(信号量)
public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程。
目的:
- 多个共享资源的互斥使用
- 用于并发线程的控制。保护一个关键部分不要一次输入超过N个线程。
Callable接口
启动线程的方法之一。时Runnable接口的增强版。
它使用call()方法作为线程的执行体,增强了之前的run()方法。
因为call()方法可以有返回值,也可以声明抛出异常。
callable接口是一个函数式接口。
和Runnable的对比
相同点:
都是接口,都可以编写多线程程序,都采用Thread.start()启动线程。
不同点:
- 具体执行方法不同:一个是run,一个是call
- Runnable没有返回值;Callable可以返回执行结果,是个泛型。
- Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能自己捕获,不能向上抛出。
- 它提供了检查计算是否完成的方法(isDone()),以等待计算的完成,并检索计算的结果。
使用
需要FutureTask类。
FutureTask:未来的任务。用它只做一件事:异步调用。通常用它来解决耗时任务,挂起堵塞问题。
当主线程中需要执行比较耗时的操作时,又不想阻塞主线程时,可以把这些作业交给FutureTask对象在后台完成,当主线程将来需要时,就可以通过Futrue对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
FutureTask仅在call方法完成时才能get结果;如果计算尚未完成,则阻塞get方法。
一旦计算完成就不能在重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转为完成状态,然后会返回结果或者抛出异常。
方法:
get() 获取到返回值结果,只执行一次!
isDone() 判断线程是否执行完成
注意:
- 为了防止主线程阻塞,建议get方法放到最后
- 只计算一次,FutureTask会复用之前计算过的结果
/**
* 1. 创建Callable的实现类,并重写call()方法,该方法为线程执行体,并且该方法有返回值
*/
class MyCallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "执行了!");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 2. 创建Callable的实例,并用FutureTask类来包装Callable对象
// 3. 创建FutureTask对象,需要一个Callable类型的参数
FutureTask task = new FutureTask<Integer>(new MyCallableThread());
// 4. 创建多线程,由于FutureTask的本质是Runnable的实现类,所以第一个参数可以直接使用task
new Thread(task, "threadName").start();
//new Thread(task, "threadName2").start();
/*while (!task.isDone()) {
System.out.println("wait...");
}*/
System.out.println(task.get());
System.out.println(Thread.currentThread().getName() + " over!");
}
}
如果创建了多个线程,会怎样呢?
运行结果:依然只有一个就是threadName。
如果想打印threadName2的结果,即不想复用之前的计算结果。怎么办?再创建一个FutureTask对象即可。
获得多线程的方法有几种?
传统的是继承Thread类和实现runnable接口
java5以后又有实现callable接口和Java的线程池获得。
尽量少用继承Thread类实现的方法,因为Java是单继承,没法继承别的类,代码复用率低。
阻塞队列(BlockingQueue)
介绍
队列特点:先进先出
好处:阻塞线程,唤醒线程由阻塞队列管理,程序员不必关心。
阻塞情况:
-
当队列满了的时候进行入队列操作
-
当队列空了的时候进行出队列操作
场景:
生产者/消费者。
实现类
主要有以下七个:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表组成的无界阻塞队列。
- LinkedBlockingDeque:由链表组成的双向阻塞队列。
接口方法
可以分为以下四类,其中阻塞方法最为重要。
| 抛出异常 | 特殊值 | 阻塞 | 超时 | |
|---|---|---|---|---|
| 插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 移除 | remove() | poll() | take() | poll(time, unit) |
| 检查 | element() | peek() | 不可用 | 不可用 |
抛出异常
add正常执行返回true,element(不删除)和remove返回阻塞队列中的第一个元素。
当阻塞队列满时,再向队列中add插入元素会抛出IllegalStateException:Queue full。
当阻塞队列空时,再对队列里remove溢出元素会抛出NoSuchElementException
当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
特定值
插入方法,成功true,失败false
移除方法,成功返回出队列的元素,队列里没有就返回null。
检查方法,成功返回队列中的元素,没有就返回null。
阻塞
如果试图的操作无法立即执行,该方法调用会发生阻塞,直到能够执行。
当阻塞队列满时,在向队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出。
当阻塞队列满时,再从队列里take元素,队列会一直阻塞消费者线程直到线程可用。
超时
如果试图进行的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待事件不会超过给定值。
返回一个特定值以告知该操作是否成功(典型的是true/false)
线程池(ThreadPool)
优势:线程复用;控制最大并发数;管理线程。
- 降低资源消耗,通过重复利用已经创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一的分配,调优和监控。
架构说明
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,ExecutorService,ThreadPoolExecutor这几个类。
Executor接口是顶层接口,只有一个execute方法,过于简单,通常不使用它,而是使用ExecutorService接口。
通常使用Executors工具类来创建连接池对象。
Executors工具类
可以快速创建线程池:
List list = Arrays.asList("");
ExecutorService threadPool = Executors.newCachedThreadPool();
连接池效果
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建单一线程的连接池
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// ExecutorService threadPool = Executors.newFixedThreadPool(3);
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
Executors.newCachedThreadPool(); Executors.newSingleThreadExecutor(); Executors.newFixedThreadPool(3); 底层都是使用的ThreadPoolExecutor!
但是实际开发中都不会用,用了可能会导致OOM,因为这三个方式要么创建线程的最大个数是Integer最大值,要么是队列没有限制导致无限存储。
那么应该用什么?
根据业务自定义线程池。
底层原理
上述案例的三个方法本质都是ThreadPoolExecutor的实例对象,只是具体参数值不同。
7个参数
线程池中的重要参数:
- corePoolSize:线程池中的常驻核心线程数
- maximumPoolSize:线程池中能够容纳的 同时执行的最大线程数,这个值必须大于等于1.
- keepAliveTime:多余的空闲线程的存活时间,当前池中的线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止。
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的就可以。
- handler:拒绝策略,表示当队列满了,并且 工作线程 大于等于 线程池的最大线程数(maximumPoolSize)时,如何拒绝请求执行的runnable的策略。
第六,七个参数可以不用给值,直接省略启用的是默认值。
核心线程个数的设置推荐:
n:CPU的核数
IO密集型:2*n
CPU密集型:n+1
如何判定这个线程池能够执行最多的任务是多少?
最大线程数+阻塞队列个数
线程池底层工作原理
具体流程:
注意!!!
- 在创建了线程池后,线程池中的线程数为0
- 当调用**execute()**方法添加一个请求任务时,线程池会做出如下潘多
- 如果正在运行的线程数量 小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量 大于或者等于corePoolSize,那么把这个任务放入队列
- 如果这个时候队列满了并且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了 并且正在运行的线程数量 大于或者等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过keepAliveTime时,线程会判断:如果当前运行的线程数 大于corePoolSize,那么这个线程就会被停掉。所以线程池所有任务完成后,它最终会收缩到corePoolSize的大小。
拒绝策略
一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列。
但这种模式下如果出现任务队列已满且线程池创建的线程数达到定义的最大线程数时,这时就需要指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池超载的情况。
ThreadPoolExecutor自带的拒绝策略如下:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常来阻止系统正常运行
- CallerRunsPolicy:“调用者运行”,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中,尝试再次提交当前任务。
- DicardPolicy:该策略默默丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略。
自定义线程池
在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理的控制开辟线程的数量;另一方面线程的细节交给线程池处理,优化了资源的开销。
而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了不少创建线程池的方法,但是都有局限性,不够灵活;使用ThreadPoolExecutor有助于明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建单一线程的连接池
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建固定数线程的连接池
// ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 可扩容连接池
// ExecutorService threadPool = Executors.newCachedThreadPool();
// 自定义连接池
ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()
//new ThreadPoolExecutor.CallerRunsPolicy()
//new ThreadPoolExecutor.DiscardOldestPolicy()
//new ThreadPoolExecutor.DiscardPolicy()
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("自定义拒绝策略");
}
}
);
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
多线程高并发底层原理
Java内存模型(JMM)
JMM就是Java内存模型。
JMM规定了内存主要分为主内存和工作内存两种。
主内存:保存了所有的变量
共享变量:如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保留一个副本,这种变量就是共享变量。
工作内存:每个线程都有自己的工作内存,线程独享,保存了线程用到的变量副本(主内存共享变量的一份拷贝)。工作内存负责和线程交互,也负责和主内存交互。
volatile关键字
验证可见性
Volatile关键字保证内存可见性。
public class VolatileDemo {
private static volatile Integer flag = 1;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是子线程工作内存flag的值:" + flag);
while(flag == 1){
}
System.out.println("子线程操作结束..." + flag);
}
}).start();
Thread.sleep(500);
flag = 2;
System.out.println("我是主线程工作内存flag的值:" + flag);
}
}
验证有序性
public class VolatileOrderDemo {
static int a,b;
static int x,y;
public static void main(String[] args) throws InterruptedException {
int i = 0;
while (true){
i++;
a = b = x = y = 0;
Thread thread1 = new Thread(() -> {
a = 1;
x = b;
}, "");
Thread thread2 = new Thread(() -> {
b = 1;
y = a;
}, "");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("第" + i + "次打印:x=" + x + ", y=" + y);
if (x == 0 && y == 0){
break;
}
}
}
}
验证不具备原子性
class DataOne{
private volatile Integer number = 0;
public Integer incr(){
return ++number;
}
}
public class VolatileAtomicDemo {
public static void main(String[] args) {
DataOne dataOne = new DataOne();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
System.out.println(dataOne.incr());
}).start();
}
}
}
最终结果显示不足1000,说明++number不具备原子性。
可以给incr方法加上同步锁,结果显示为1000.
原理
Java语言提供了一种稍弱的同步机制,就是用volatile变量,用来确保将变量的更新操作通知到其他线程。当爸变量声明为volatile了icing后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作和其他内存操作一起重排序。
在访问volatile变量时不会执行加锁操作,因此也就不会使线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当一个变量定义为volatile之后,将具备两种特性:
- 保证此变量对所有的线程的可见性。
- 禁止指令重排序优化。有volatile修饰的变量,复制后多执行了一个"load addl $0x0,(%esp)"操作,这个操作相当于一个内存屏障(也就是说指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。
- 不能保证变量的原子性。
volatile性能:volatile的读性能消耗和普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
Happen-Before
在常规开发中,分析一个并发程序是否安全,其实都依赖Happen-Before原则进行分析。
Happen-Before被翻译称先行发生原则,意思就是当A操作线性发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值,发送了消息,调用了方法等。
Happen-Before的规则有以下几条:
- 程序次序规则**:在一个线程内一段代码的执行结果是有序的**。就算还会指令重排,但是随便它怎么派,结果是按照我们代码的顺序生成的不会变。
- 管程锁定规则:就是无论在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果。(管程是一种通用的同步原语,synchronized就是管程的实现)
- volatile变量规则:对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作线性发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则:对于线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)线性发生于它的finalize()方法的开始。
- 传递性:如果操作A先于操作B,操作B先于操作C,那么A先于C。
如果不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序。通过这些条件的判断,仍然很难判断一个线程是否能安全执行,线程安全多数依赖于工具类的安全性来保证。
CAS
CAS:Compare and Swap。比较并交换的意思。
CAS有三个基本参数:内存地址A,旧值B,新值C。
他们的作用是将指定内存地址A的内容和所给的旧值B相比:如果相等,就将内容替换为指令中提供的新值C;如果不等,那么更新失败。
类似于修改登录密码的过程,只有当用户输入的原密码和数据库中存储的原密码相同,才能把原密码更新为新密码,否则不能更新。
CAS是解决多线程并发安全问题的一种乐观锁算法。
Unsafe类是CAS的核心类,提供硬件级别的原子操作。(目前所有CPU基本都支持硬件级别的CAS操作)
// 对象、对象的属性地址偏移量、预期值、修改值public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Unsafe简单Demo
public class UnsafeDemo {
private int number = 0;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
UnsafeDemo unsafeDemo = new UnsafeDemo();
System.out.println(unsafeDemo.number);// 修改前
unsafeDemo.compareAndSwap(0, 30);
System.out.println(unsafeDemo.number);// 修改后
}
public void compareAndSwap(int oldValue, int newValue){
try {
// 通过反射获取Unsafe类中的theUnsafe对象
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true); // 设置为可见
Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 获取Unsafe对象
// 获取number的偏移量
long offset = unsafe.objectFieldOffset(UnsafeDemo.class.getDeclaredField("number"));
// cas操作
unsafe.compareAndSwapInt(this, offset, oldValue, newValue);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
基本代码演示
在JUC下有atomic包,有很多原子操作的包装类。
这里展示AtomicInteger这个类
public class CasDemo {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(1);
System.out.println("第一次更新:" + i.compareAndSet(1, 200));
System.out.println("第一次更新后i的值:" + i.get());
System.out.println("第二次更新:" + i.compareAndSet(1, 300));
System.out.println("第二次更新后i的值:" + i.get());
System.out.println("第三次更新:" + i.compareAndSet(200, 300));
System.out.println("第三次更新后i的值:" + i.get());
}
}
输出结果如下
第一次更新:true
第一次更新后i的值:200
第二次更新:false
第二次更新后i的值:200
第三次更新:true
第三次更新后i的值:300
结果分析:
第一次更新:i的值(1)和预期值(1)相同,所以执行了更新操作,把i的值更新为200 第二次更新:i的值(200)和预期值(1)不同,所以不再执行更新操作 第三次更新:i的值(200)和预期值(1)相同,所以执行了更新操作,把i的值更新为300
验证原子性
class DataOne{
private AutomicInteger number = new AutomicInteger(0);
public Integer incr(){
return number.incrementAndGet();
}
}
public class VolatileAtomicDemo {
public static void main(String[] args) {
DataOne dataOne = new DataOne();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
System.out.println(dataOne.incr());
}).start();
}
}
}
多次运行,发现值为1000线程停止.
缺点
- 开销大:并发量高的情况下,如果反复尝试更新某个变量,却又一直更新不成功, 会给CPU带来较大的压力。
- ABA问题:当变量值从A修改为B在修改为A时,变量值等于期望值A,但是无法判定是否修改,CAS操作在ABA修改后依然成功。
- 不能保证代码块的原子性:CAS机制保证的只是一个变量的原子性操作,不能保证整个代码块的原子性。
AQS
AbstractQueuedSynchronizer抽象队列同步器简称AQS,他是实现同步器的基础组件(框架),JUC下Lock(ReentrantLock、ReentrantReadWriteLock等)的实现以及一些并发工具类(Semaphore、CountDownLatch、CyclicBarrier等)就是通过AQS来实现的。
通过AQS可以实现独占锁。
具体用法是通过继承AQS实现器模板方法,然后将子类作为同步组件的内部类。
但是
StampLock不是基于AQS实现的,能够解决锁的饥饿问题
框架结构
AQS维护了一个volatile语义的共享资源变量state和一个FIFO线程等待队列(对线程进程state资源被阻塞时,会进入这个队列)。
基于AQS实现锁的思路
AQS将大部分的同步逻辑实现好,继承的自定义同步器只需要是按state的获取(acquire)和释放(release)的逻辑代码就可以,主要包括以下方法
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
也就是说:
通过AQS可以实现独占锁(只有一个线程可以获取到锁,比如:ReentrantLock),也可以实现共享锁(多个线程都可以获取到锁Semaphore/CountDownLatch等)
基于AQS实现独占锁
从官方文档挪过来的类
class Mutex implements Lock, java.io.Serializable {
// Our internal helper class
private static class Sync extends AbstractQueuedSynchronizer {
// Reports whether in locked state
protected boolean isHeldExclusively() {
return getState() == 1;
}
// Acquires the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1;
// Otherwise unused
if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }
// Releases the lock by setting state to zero
protected boolean tryRelease(int releases) {
assert releases == 1;
// Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null); setState(0); return true; }
// Provides a Condition
Condition newCondition() { return new ConditionObject(); }
// Deserializes properly
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject(); setState(0);
// reset to unlocked state
} }
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public void unlock() { sync.release(1); }
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively(); }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } }
使用这个类
public class AqsDemo {
public static void main(String[] args) throws InterruptedException {
DataThree dataThree = new DataThree();
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
dataThree.incr();
countDownLatch.countDown();
}, "").start();
}
countDownLatch.await();
System.out.println(dataThree.getNum());
}
}
class DataThree {
private volatile int num;
Mutex mutex = new Mutex();
public void incr(){
mutex.lock();
for (int i = 0; i < 1000; i++) {
num++;
}
mutex.unlock();
}
public int getNum(){
return num;
}
}
ReentrantLock底层原理
在ReentrantLock类中包含了3个AQS的实现类:
- 抽象类Sync
- 非公平锁实现类NonfairSync
- 公平锁实现类FairSync
Sync抽象类
内部主要方法有
/**
* 自定义方法:为非公平锁的实现提供快捷路径
*/
abstract void lock();
/**
* 自定义通用方法,两个子类的tryAcquire方法都需要使用非公平的trylock方法
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果当前没有线程获取到锁
if (compareAndSetState(0, acquires)) { // 则CAS获取锁
setExclusiveOwnerThread(current); // 如果获取锁成功,把当前线程设置为有锁线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果当前线程已经拥有锁,则重入
int nextc = c + acquires; // 每重入一次stat累加acquires
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* 实现AQS的释放锁方法
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 每释放一次stat就减releases
if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不是有锁线程抛异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // stat减为0则释放锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
NonfairSync
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1)) // CAS把stat设置为1
setExclusiveOwnerThread(Thread.currentThread()); // 获取到锁
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); // 使用了Sync抽象类的nonfairTryAcquire方法
}
}
acquire(1)方法是AQS自己实现的。本质就是调用tryAcquire方法,如果TryAcquire获取到锁并无法进入等待队列就中止线程。
FairSync
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 从线程有序等待队列中获取等待
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
hasQueuedPredecessors具体实现如下:
- 当等待队列只有一个线程时,直接获取到锁。
- 如果队列不止一个线程,并且下一个线程就是当前申请锁的线程,则获取锁。
各种锁
synchronized:偏向锁(偏向第一个线程,效率最高)-->如果有线程竞争升级为轻量级锁(自旋锁)-->自旋10次升级为重量级锁(悲观锁)