一、Synchronized同步锁
1. 锁介绍
在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁。
Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有。
如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁。
2. synchronized介绍
-
synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。
-
synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁)。加锁的代码出现异常,会自动解锁。
-
synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区(工作内存或高速缓存)。
-
synchronized 不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。
-
synchronized 可以保证原子性,一个线程的操作一旦开始,就不会被其他线程干扰,只能当前线程执行完,其他线程才可以执行。
-
synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。
-
主要分为下面几种情况:
-
修饰实例方法,非静态方法(对象锁) 需要在类实例化后,再进行调用。
-
修饰静态方法(类锁)静态方法属于类级别的方法,静态方法可以类不实例化就使用。
-
修饰代码块(对象锁、类锁)。
代码演示:
-
package com.lyx.test;
public class Test02 {
static int a = 0;
public static void main(String[] args) throws InterruptedException {
Test02 test02 = new Test02();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
test02.test01();
}
}).start();
}
Thread.sleep(3000);
System.out.println(a);
}
/*
* 加锁:执行到加锁的方法,自动加锁
* 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
* 锁范围:加锁的方法
* 锁类型:对象锁(同一个对象生效)
* 多个线程争夺同一个对象锁
* 锁失效:多个对象失效
* */
public synchronized void test01(){
for (int i = 0; i < 10000; i++) {
a++;
}
}
/*
* 加锁:执行到加锁的方法,自动加锁
* 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
* 锁范围:加锁的方法
* 锁类型:类锁(同一个类生效,和对象无关)
* 多个线程争夺同一个类锁
* */
public static synchronized void test2(){
for (int i = 0; i < 10000; i++) {
a++;
}
}
/*
* 加锁:执行到加锁的方法,自动加锁
* 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
* 锁范围:加锁的代码块
* 锁类型:类锁(同一个固定值)
* 锁失效:多个值
* */
final String LOCK = "锁";
public void test3(){
int a = 0;
System.out.println(a);
synchronized (LOCK){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
public void test4(String str){
synchronized (str){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
/*
* 加锁:执行到加锁的方法,自动加锁
* 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
* 锁范围:加锁的代码块
* 锁类型:对象锁(同一个对象)
* 锁失效:多个对象
* */
public void test5(){
synchronized (this){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
/*
* 加锁:执行到加锁的方法,自动加锁
* 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
* 锁范围:加锁的代码块
* 锁类型:类锁(同一个类对象生效)
* */
public void test6() {
synchronized (Test02.class) {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
3. 对象锁和类锁(面试题)
当synchronized修饰静态方法或代码块参数为Class时或代码块参数为固定值,锁为类锁,作用整个类。同一个类使用,锁生效。
当synchronized修饰实例方法或代码块参数为this时,为对象锁,只对当前对象有效。
体现在:
多个对象使用时,锁生效,使用类锁。
同一对象使用时,所生效,使用对象锁。
4. 什么是可重入锁(面试题)
某个线程已经获得了某个锁,允许再次获得锁,就是可重入锁。如果不允许再次获得锁就称为不可重入锁。
synchronized为可重入锁。但可重入锁不仅仅只有synchronized。后面还会学习ReentrantLock也是可重入锁。
4.1 代码演示
public class Demo22 {
public static void main(String[] args) {
Demo22 demo = new Demo22();
new Thread(new Runnable() {
@Override
public void run() {
demo.test1();
}
}).start();
}
public void test1(){
synchronized (this){
System.out.println("test1执行");
test2();
}
}
public void test2(){
synchronized (this){
System.out.println("test2执行");
}
}
}
4.2 可重入锁底层原理
可重入锁底层原理特别简单,就是计数器。
当一个线程第一次持有某个锁时会由monitor(监控器)对持有锁的数量加1,当这个线程再次需要碰到这个锁时,如果是可重入锁就对持有锁数量再次加1(如果是不可重入锁,发现持有锁为1了,就不允许多次持有这个锁了,阻塞),当释放锁时对持有锁数量减1,直到减为0,表示完全释放了这个锁。
二、生命周期图回顾
线程生命周期从新建到死亡共包含五种状态:
新建状态、就绪状态、运行状态、阻塞状态、死亡状态
1 .新建状态
当实例化Thread对象后,线程就处于新建状态. 这时线程并没有执行。
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("run()执行开始");
System.out.println("run()执行结束");
}
});
}
2. 就绪状态
只要在代码中启动了线程,就会从新建状态,变为就绪状态。
thread.start();
就绪状态属于一种临时状态。处于就绪状态的线程会去抢占CPU,只要抢占成功就会切换到运行状态,失去了cpu执行权,回到就绪状态。
线程抢占CPU的场景和超市中大妈早上去抢菜的场景是一样。
3. 运行状态
运行状态就是开始执行线程的功能。具体就是执行run()方法
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("run()执行开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("run()执行结束");
}
});
thread.start();
在代码执行过程中分为三种情况:
1. 如果碰到sleep() / wait() / join()等方法会让线程切换为阻塞状态。
2. 如果调用yield()方法或失去CPU执行权限会切换为就绪状态。
3. 如果run()方法成功执行完成,或出现问题或被停止(中断)会切换为死亡状态。
4. 阻塞状态
阻塞状态时,线程停止执行。让出CPU资源。
处于阻塞状态的线程需要根据情况进行判断是否转换为就绪状态:
1. 如果是因为sleep()变为阻塞,则休眠时间结束自动切换为就绪状态。
2. 如果是因为wait()变为阻塞状态,需要调用notify()或notifyAll()手动切换为就绪状态。
3. 如果因为join()变为阻塞状态,等到join线程执行完成,自动切换为就绪状态。
4. (已过时)如果是因为suspend()暂停的线程,需要通过resume()激活线程。
5. 死亡状态
死亡状态即线程执行结束。
三、线程中相关方法回顾
1. stop()介绍(已过时)
1.1 stop()介绍(已过时)
stop()可以停止一个线程。让线程处于死亡状态,stop()已经过时
1.2 stop()弃用的原因
stop()太绝对了,什么情况下都能停,并没有任何的提示信息,可能导致混乱结果。
推荐使用interrupt()停止一个长时间wait的线程。
1.3 stop()代码演示
package com.zqwl.test;
/*
* stop():
* 结束阻塞,运行状态的线程
*
* */
public class Test04 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程开始执行");
/*for (int i = 0; i < 999999999; i++) {
System.out.println(i);
}*/
try {
Thread.sleep(500000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程执行结束");
}
});
thread.start();
Thread.sleep(1000);
thread.stop();
2. interrupt()介绍
interrupt()只能中断当前线程状态带有InterruptedException异常的线程,当程序执行过程中,如果被强制中断会出现Interrupted异常。
interrupt() 负责打断处于阻塞状态的线程。防止出现死锁或长时间wait(等待)。
2.1 interrupt()代码演示
package com.lyx.test;
/*
* interrupt()
* 中断阻塞状态的线程,抛出中断异常,线程继续执行
* 运行状态线程不能被中断
* 注意:
* 只能中断声明了InterruptedException异常状态的线程
* *具体来说,当对一个线程,调用 interrupt() 时,
* ① 如果线程处于被阻塞状态(处于sleep, wait, join 等状态),线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
* ② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程将继续正常运行,不受影响。
* interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
* */
public class Test05 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("开始");
/* for (int i = 0; i < 99999999; i++) {
System.out.println(i);
}*/
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束");
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
3. suspend()和resume()
3.1 suspend()介绍(已过时)
suspend()可以挂起、暂停线程,让线程处于阻塞状态,是一个实例方法,已过时。
挂起时,不会释放锁
3.2 resume()介绍(已过时)
resume()可以让suspend()的线程唤醒,变成就绪状态,已过时。
3.3 代码演示
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("run()开始执行");
System.out.println("run()执行结束");
}
}
});
try {
//启动线程
thread.start();
Thread.sleep(1000);//主线程阻塞状态
thread.suspend();//挂起线程
Thread.sleep(2000);
thread.interrupt();//唤醒线程
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.resume();
//结束处于运行状态的线程
thread.interrupt();
}
3.4 被弃用的原因
如果线程A持有锁(假设锁叫做L),对线程A做了suspend,让线程A挂起。在线程A没有resume之前,线程B无论如何也是无法获得锁的,也就出现了死锁。因为suspend时没有释放锁。
四、线程通信回顾
1. 什么是线程通信
需要多个线程配合完成一件事情,如何让多个线程能够合理的切换就是线程通信需要考虑的问题,重点在于配合。
2. 线程通信的几种方式(面试题)
-
wait()和notify() | notifyAll() 方式
-
join()方式
-
Condition 方式
-
…
3. wait()和notify() | notifyAll()
3.1 介绍
wait() 是Object中的方法。调用wait()后会让线程从运行状态变为阻塞状态。
在Object类中提供了wait()的重载方法 。
1. 使用wait()和notify() | notifyAll()要求必须有锁。
2. wait()、notify()、notifyAll() 都是放入锁的代码中。
3. wait()和notify() | notifyAll() 配合使用。
3.2 代码演示
package com.lyx.test;
/*
* t1:白日依山尽,
* t2:黄河入海流。
* t1:欲穷千里目,
* t2:更上一层楼。
* */
public class Test09 {
static final String LOCK = "锁";
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (LOCK) {
System.out.println("t1:白日依山尽,");
LOCK.wait();
System.out.println("t1:欲穷千里目,");
LOCK.notifyAll();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (LOCK) {
System.out.println("t2:黄河入海流。");
LOCK.notifyAll();
LOCK.wait();
System.out.println("t2:更上一层楼。");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
3.3 wait()和sleep()区别(常见面试题)
- 所属类不同
wait(long) 是Object中方法
sleep(long)是Thread的方法
- 唤醒机制不同
wait() 没有设置最大时间情况下,必须等待notify() | notifyAll()
sleep()是到指定时间自动唤醒
- 锁机制不同
wait(long)释放锁
sleep(long)只是让线程休眠,不会释放锁
- 使用位置不同
wait()必须持有对象锁
sleep()可以使用在任意地方
- 方法类型不同
wait()是实例方法
sleep()是静态方法
4. join()
4.1 介绍
join() 把线程加入到另一个线程中。在哪个线程内调用join(),就会把对应的线程加入到当前线程中。
join()后,会让当前线程挂起,变成阻塞状态,直到新加入的线程执行完成。当前线程才会继续执行。
public static void main(String[] args) {
Thread thread1 = new Thread() {
@Override
public void run() {
System.out.println("我是子线程");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
try {
thread1.start();
//thread1线程加入主线程,主线程挂起,thread1执行完主线程继续执行
thread1.join();
System.out.println("我是主线程");
} catch (Exception e) {
e.printStackTrace();
}
}
五、 JUC中的locks包
1. locks包介绍
java.util.concurrent.locks:JUC中对锁支持的工具包 。
六、JUC的锁机制
1. AQS
1.1 介绍
AQS全名AbstractQueuedSynchronizer,是并发容器JUC(java.util.concurrent)下locks包内的一个类。
1.2 工作原理
AQS的核心思想为如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是使用队列实现的锁,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个int state成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。
AQS使用CAS对该同步状态进行原子操作实现对其值的修改,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁。
2. 锁机制介绍
JUC中锁的底层使用的就是AQS
-
ReentrantLock:Lock接口的实现类,可重入锁。相当于synchronized同步锁。
-
ReentrantReadWriteLock:ReadWriteLock接口的实现类。类中包含两个静态内部类,ReadLock读锁、WriteLock写锁。
-
Condition:是一个接口,都是通过lock.newCondition()实例化。属于wait和notify的替代品。提供了await()、signal()、singnalAll()与之对应。
-
LockSupport:和Thread中suspend()和resume()相似。
3. 锁机制详解
3.1 ReentrantLock重入锁
ReentrantLock是JUC中对重入锁的标准实现。作用相当于synchronized。
加锁和解锁过程都需要由程序员手动控制,使用很灵活。
提供了2种类型的构造方法。
1. ReentrantLock():创建非公平锁的重入锁。
2. ReentrantLock(boolean):创建创建锁。取值为true表示公平锁,取值为false表示非公平锁。
公平锁:多线程操作共一个资源时,严格按照顺序执行。
非公平锁:多线程在等待时,可以竞争,谁竞争成功,谁获取锁。
非公平锁的效率要高于公平锁。ReentrantLock默认就是非公平锁。
语法:
创建:
ReentrantLock rk = new ReentrantLock();
加锁:
//无返回值 阻塞代码
rk.lock();
//有返回值 不会阻塞代码
boolean b = rk.tryLock()
解锁:
rk.unLock();
注意:
1. ReentrantLock出现异常时,不会自动解锁
2. 多线程的情况下,一个线程出现异常,并没有释放锁,其他线程也获取不到锁,容易出现死锁
3. 建议把解锁方法finally{}代码块中
4. synchronized加锁与释放锁不需要手动的设置,遇到异常时,会自动的解锁
代码演示:
public class Test01 {
static int a = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock rl = new ReentrantLock(false);
for (int i = 0; i < 5; i++) {
new Thread() {
@Override
public void run() {
rl.lock();//加锁
test();
//重入锁
/*rl.lock();
test();
rl.unlock();*/
rl.unlock();//解锁
}
}.start();
}
Thread.sleep(2000);
System.out.println(a);
}
public static void test() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
注意:
避免死锁,需要将解锁放到finally{}中
3.2 Condition等待 | 唤醒
wait和notify是针对synchronized的,Condition是针对Lock的
语法:
创建:
ReentrantLock rk = new ReentrantLock();
Condition condition = rk.newCondition();
线程等待:
condition.await();
唤醒一个线程 | 唤醒所有线程:
condition.signal(); //唤醒一个线程
condition.signalAll(); //唤醒所有线程
3.2.1 代码演示
public class Test03 {
public static void main(String[] args) {
ReentrantLock rl = new ReentrantLock();
Condition condition = rl.newCondition();
new Thread(){
@Override
public void run() {
try {
//加锁
rl.lock();
System.out.println("白日依山尽");
//线程等待 -> 阻塞状态 释放锁
condition.await();
System.out.println("欲穷千里目");
//唤醒其他线程
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
rl.unlock();
}
}
}.start();
new Thread(){
@Override
public void run() {
try {
//加锁
rl.lock();
System.out.println("黄河入海流");
//唤醒其他线程
condition.signal();
//线程等待 -> 阻塞状态 释放锁
condition.await();
System.out.println("更上一层楼");
} catch (Exception e) {
e.printStackTrace();
} finally{
rl.unlock();
}
}
}.start();
}
}
3.3 ReadWriteLock读写锁
ReadWriteLock为接口,实现类为ReentrantReadWriteLock
ReadLock 读锁,又称为共享锁。允许多个线程同时获取该读锁
WriteLock 写锁,又称为独占锁。只有一个线程能获取,其他的线程等待,避免死锁。
注意:
读写锁,实际含义为是否能有多个线程同时获取
语法:
创建:
ReentrantReadWriteLock rk = new ReentrantReadWriteLock();
读锁:
//获取读锁
ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
//加锁
readLock.lock();
boolean b = readLock.tryLock();
//解锁
readLock.unlock();
写锁:
//获取写锁
ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
//加锁
writeLock.lock();
boolean b = writeLock.tryLock();
//解锁
writeLock.unlock();
代码演示
/*
*ReadWriteLock:
* ReadLock 读锁,又称为共享锁。允许多个线程同时获取该读锁,相当于没有锁。
* WriteLock 写锁,又称为独占锁。只有一个线程能获取,其他写的线程等待,避免死锁。(与lock锁相似)
* */
public class Test14 {
public static void main(String[] args) {
//获取读写锁对象
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
// writeLock.lock();
readLock.lock();
test(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
// writeLock.unlock();
readLock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
readLock.lock();
// writeLock.lock();
test(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
// writeLock.unlock();
}
}
}).start();
}
public static void test(String name) {
for (; ; ) {
try {
System.out.println(name);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.4 LockSupport 暂停 | 恢复
LockSupport是Lock中实现线程暂停和线程恢复。suspend()和resume()是synchronized中的暂停和恢复。
注意:暂停恢复不会释放锁,避免死锁问题
语法:
暂停:
LockSupport.park();
恢复:
LockSupport.unpark(t1);
3.4.1 代码演示
package com.lyx.test;
import java.util.concurrent.locks.LockSupport;
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("程序开始");
for (int i = 0; i < 10; i++) {
try {
System.out.println(i);
if (i == 6){
LockSupport.park(); //暂停线程
}
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("程序结束");
}
});
thread.start();
Thread.sleep(5000);
LockSupport.unpark(thread); //恢复线程
}
}
3.5 synchronized和lock的区别(面试题)
- 类型不同
synchronized是关键字。修饰方法,修饰代码块
Lock是接口
- 加锁和解锁机制不同
synchronized是自动加锁和解锁,程序员不需要控制。
Lock必须由程序员控制加锁和解锁过程,解锁时,需要注意出现异常不会自动解锁
- 异常机制
synchronized碰到没有处理的异常,会自动解锁,不会出现死锁。
Lock碰到异常不会自动解锁,可能出现死锁。所以写Lock锁时都是把解锁放入到finally{}中。
- Lock功能更强大
Lock里面提供了tryLock()/isLocked()方法,进行判断是否上锁成功。synchronized因为是关键字,所以无法判断。
- Lock性能更优
如果多线程竞争锁特别激烈时,Lock的性能更优。如果竞争不激烈,性能相差不大。
- 线程通信方式不同
synchronized 使用wait()和notify()线程通信。
Lock使用Condition的await()和signal()通信。
- 暂停和恢复方式不同
synchronized 使用suspend()和resume()暂停和恢复,这俩方法过时了。
Lock使用LockSupport中park()和unpark()暂停和恢复,这俩方法没有过时。
七、JUC中的Tools
1. Tools介绍
Tools也是JUC中的工具类,其中包含了CountDownLatch、CyclicBarrier、Semaphore
2. CountDownLatch计数器
在开发中经常遇到在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。之前是使用join() | 主线程休眠实现的,但是不够灵活,某些场合和还无法实现,所以开发了CountDownLatch这个类。底层基于AQS。
CountDown是计数递减的意思,Latch是门闩的意思。内部维持一个递减的计数器。可以理解为初始有n个Latch,等Latch数量递减到0的时候,结束阻塞,执行后续操作。
创建:
CountDownLatch cdl= new CountDownLatch(数字);
线程等待:
//当前线程等待,直到到Latch计数到零,或者被interrupt
cdl.await():
计数器递减:
//减少Latch的计数,如果计数达到零,释放等待的线程
cdl.countDown( ):
2.1 代码实现:
public class Test07 {
int a = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
ReentrantLock rl = new ReentrantLock();
Test11 test05 = new Test11();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 5; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
rl.lock();
test05.test();
countDownLatch.countDown(); //计数器-1
rl.unlock();
}
});
}
countDownLatch.await(); //计数器值不为0阻塞,为0 恢复执行
System.out.println(test05.a);
}
public void test() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
3. CyclicBarrier回环屏障
CountDownLatch优化了join()在解决多个线程同步时的能力,但CountDownLatch的计数器是一次性的。计数递减为0之后,再调用countDown()、await()将不起作用。为了满足计数器可以重置的目的,JDK推出了CyclicBarrier类。
await()方法表示当前线程执行时计数器值不为0则等待。如果计数器为0则继续执行。每次await()之后计算器会减少一次。当减少到0下次await从初始值重新递减。
3.1 代码实现
假设多个任务都有三个阶段组成,多个线程分别指向一个任务,必须保证每个任务的一个阶段结束后,才进入下一个阶段。此时使用CyclicBarrier正合适
/*
* 1.裁判:比赛开始
* 2.球员:开始行动
* 3.球员:球员犯规
* 4.裁判:吹哨
* 5.裁判:出示红牌
* 6.球员:接受红牌
* */
public class Test08 {
public static void main(String[] args) {
//回环屏障
CyclicBarrier cb = new CyclicBarrier(2);
//裁判线程
new Thread() {
@Override
public void run() {
try {
System.out.println("1.裁判:比赛开始");
cb.await(); //2-1 = 1 线程阻塞
System.out.println("4.裁判:吹哨");
cb.await(); //1-1 = 0 代码继续执行
System.out.println("5.裁判:出示红牌");
cb.await(); //2-1 = 1 阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}.start();
//球员线程
new Thread() {
@Override
public void run() {
try {
System.out.println("2.球员:开始行动");
cb.await(); // 1-1 = 0 代码继续执行
System.out.println("3.球员:球员犯规");
cb.await(); //2-1 = 1 线程阻塞
System.out.println("6.球员:接受红牌");
cb.await(); //1-1=0 代码继续执行
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}.start();
}
}
4. Semaphore 信号量
CountDownLatch和CyclicBarrier的计数器递减的,而Semaphore的计数器是可加可减的,并可指定计数器的初始值,并且不需要事先确定同步线程的个数,等到需要同步的地方指定个数即可。且Semaphore也具有回环重置的功能,这一点和CyclicBarrier很像。底层也是基于AQS。
语法:
创建:
Semaphore sp= new Semaphore(数字);
获取信号量的值:
int i = sp.availablePermits();
增加信号量:
//信号量+1
sp.release();
//信号量+n
sp.release(n);
减少信号量:
sp.acquire(); //信号量-1,无返回值
sp.tryAcquire(); //信号量-1,有返回值
sp.acquire(n); //信号量-n,无返回值
sp.tryAcquire(n); //信号量-n,有返回值
4.1 代码实现:
public class Test09 {
public static void main(String[] args) {
try {
Semaphore sp = new Semaphore(10);
System.out.println(sp.availablePermits()); //当前信号量的值 10
sp.release(); //信号量+1
System.out.println(sp.availablePermits()); //当前信号量的值 11
sp.release(10); //信号量+10
System.out.println(sp.availablePermits()); //当前信号量的值 21
sp.acquire(); //信号量-1
System.out.println(sp.availablePermits()); //当前信号量的值 20
boolean b = sp.tryAcquire(15);
System.out.println(b+"-"+sp.availablePermits()); //当前信号量的值 true-5,值若小于0,返回0。
/*
* acquire(n):信号量不足 <0,阻塞等待
* sp.tryAcquire(n):信号量不足 <0,返回false,继续执行
* */
sp.acquire(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
八、并发集合类
1. 介绍
并发集合类:主要是提供线程安全的集合。
比如:
1. ArrayList对应的并发类是CopyOnWriteArrayList
2. HashSet对应的并发类是 CopyOnWriteArraySet
3. HashMap对应的并发类是ConcurrentHashMap
这些类的方法API和之前学习的ArrayList、HashSet、HashMap的API是相同的,所以重在实现原理上,而不是API的使用上。
2. CopyOnWriteArrayList
2.1 ArrayList
ArrayList是最常用的集合之一,大小不固定,可以随着元素的增多可以自动扩容。
储存的数据为有序,可重复. 底层实现是基于数组,线程不安全。
2.2. CopyOnWriteArrayList
使用方式和ArrayList相同, 当时CopyOnWriteArrayList线程为安全的。
写时复制
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
对于读操作远远多于写操作的应用非常适合,特别在并发情况下,可以提供高性能的并发读取。
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
2.3 CopyOnWriteArrayList重点源码
public class CopyOnWriteArrayList<E> implements List<E>,RandomAccess, Cloneable, java.io.Serializable {
//创建不可改变的对象
final transient Object lock = new Object();
//volatile修饰的Object类型的数组, 保证了数组的可见性,有序性
private transient volatile Object[] array;
//获取元素,根据下标获取元素,支持多线程查询
public E get(int index) {
return elementAt(getArray(), index);
}
//设置数组
final void setArray(Object[] a) {
array = a;
}
//添加元素,写时复制
public boolean add(E e) {
//加锁
synchronized (lock) {
//获取当前数组
Object[] es = getArray();
//获取数组的长度
int len = es.length;
//复制旧数组,长度+1,创建一个新数组
es = Arrays.copyOf(es, len + 1);
//根据下标,将添加的元素放入
es[len] = e;
//将新数组设置为当前的数组
setArray(es);
return true;
}
}
//修改元素
public E set(int index, E element) {
//加锁
synchronized (lock) {
//获取当前数组
Object[] es = getArray();
//根据传递的下标,获取数组中的元素
E oldValue = elementAt(es, index);
//数组中该下标存储的元素和修改的元素不一致
if (oldValue != element) {
es = es.clone();
//修改元素
es[index] = element;
}
//将新数组设置为当前的数组
setArray(es);
return oldValue;
}
}
//删除元素
public E remove(int index) {
//加锁
synchronized (lock) {
//获取当前数组
Object[] es = getArray();
//获取数组长度
int len = es.length;
//根据传递的下标,获取数组中的元素
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
//最有一个元素
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
}
3. CopyOnWriteArraySet源码分析
3.1 HashSet
HashSet无序,无下标,元素不可重复的集合,线程不安全,底层实现为(HashMap)。
3.2 CopyOnWriteArraySet
它是线程安全的HashSet,CopyOnWriteArraySet则是通过"动态数组(CopyOnWriteArrayList)"实现的,并不是散列表
CopyOnWriteArraySet在CopyOnWriteArrayList 的基础上使用了Java的装饰模式,所以底层是相同的。而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过动态数组实现的Set,CopyOnWriteArrayList中允许有重复的元素;但CopyOnWriteArraySet是一个Set集合,所以它不能有重复数据。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!
3.3 CopyOnWriteArraySet重点源码
public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
//声明CopyOnWriteArrayList
private final CopyOnWriteArrayList<E> al;
//无参构造方法
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
//添加元素
public boolean add(E e) {
return al.addIfAbsent(e);
}
}
public class CopyOnWriteArrayList<E> implements List<E>,RandomAccess, Cloneable, java.io.Serializable {
private transient volatile Object[] array;
//获取当前的数组
final Object[] getArray() {
return array;
}
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
/*
* &&逻辑与,只要第一个返回false,直接返回false
* 第一个判断,当前添加的元素是否存在:
* 存在返回这个元素的下标
* 不存在返回-1
*
* 第二个判断,如果不存在添加元素
* 添加成功,true
* 添加失败,false
* */
return indexOfRange(e, snapshot, 0, snapshot.length) < 0
&& addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
//加锁
synchronized (lock) {
//获取数组
Object[] current = getArray();
//获取数组长度
int len = current.length;
//数组发生过修改
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i]
&& Objects.equals(e, current[i]))
return false;
if (indexOfRange(e, current, common, len) >= 0)
return false;
}
//数组没有发生修改 数组拷贝,原数组长度+1
Object[] newElements = Arrays.copyOf(current, len + 1);
//添加元素
newElements[len] = e;
//新数组替换原数组
setArray(newElements);
return true;
}
}
}
4. ConcurrentHashMap
4.1 HashMap
HashMap也是使用非常多的集合,线程不安全,以key-value的形式存在。
在HashMap中,底层实现为哈希表,系统会根据hash算法来计算key的存储位置,我们可以通过key快速地存、取value,允许一个key-value为null
1. HashMap JDk1.7以及1.7之前
HashMap 底层是基于** 数组+链表
** 组成的
头插
2. HashMap JDk1.8以及1.8之后
HashMap 底层是基于 数组+链表+红黑树
组成的,当 Hash 冲突严重时,在数组上形成的链表会变的越来越长,这样在查询时的效率就会越来越低,达到一定的条件,就会由链表转换为红黑树,提高查询的效率
尾插
4.2 HashTable
HashTable和HashMap的实现原理几乎一样,差别无非是
1. HashTable不允许key和value为null
2. HashTable是线程安全的,但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时,只要有一个线程访问操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差
4.3 ConcurrentHashMap1.7及之前
ConcurrentHashMap采用了非常精妙的"分段锁"策略。
Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,使用多个锁来控制对hash表的不同部分(段segment)进行的修改,如果多个修改操作发生在不同的段上,他们就可以并发进行,从而提高了效率。
4.4 ConcurrentHashMap1.8及之后
ConcurrentHashMap在JDK8中进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用synchronized + CAS,如果没有出现hash冲突,使用CAS直接添加数据,只有出现hash冲突的时候才会使用同步锁添加数据,又提升了效率,它底层由"数组"+链表+红黑树的方式思想(JDK8中HashMap的实现), 为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
4.5 ConcurrentHashMap1.8及之后重点源码
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key-value都为空, 抛出异常
if (key == null || value == null) throw new NullPointerException();
//计算key的hash值
int hash = spread(key.hashCode());
/*
* 使用链表保存时,binCount记录结点数;
*/
int binCount = 0;
//循环遍数组
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
//判断当前桶是否为空,空的就需要初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//计算 key 的 hash 值,通过(n - 1) & hash计算key存放的位置, 存储的位置为空,使用cas直接插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
//发现是ForwardingNode结点,说明此时table正在扩容,则尝试协助数据迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else { //出现hash冲突,也就是table[i]桶中已经曾经添加了Node节点,加锁,添加数据
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 出现hash冲突,就会找到“相等”的结点,判断是否需要更新value值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
//插入数据
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
//如果当前桶为红黑树,那就要按照红黑树的方式写入数据
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 如果链表中节点个数达到阈值,数组长度大于64,链表转化为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 计数值加1
addCount(1L, binCount);
return null;
}