自己的学习笔记,材料源于网上教程
基础概念
JUC是什么?
java.util.concurrent包,用于更好地支持高并发任务,在进行多线程编程时减少竞争条件和死锁的问题
并发编程本质是充分利用CPU,并发问题可以理解为多个线程同时操作一个资源类产生的问题
进程与线程区别
进程:写的一个程序集,例如菜谱
线城:进程地具体一次运行,例如根据菜谱进行一次做菜的过程,是操作系统调度的基本单位
java默认有两个线程:main主方法 gc垃圾回收
java是否真的能开启线程:java运行在虚拟机之上无法直接操作硬件,start()方法实际上时调用本地C++ native方法开启线程
并发与并行
并发:CPU单核,多线程共用一个资源,同一时间段内,快速交替执行
并行,CPU多核,同一时间段内,多个线城同时执行
获取CPU核数
Runtime.getRunTime().availableProcessors();
线程状态
新生态NEW, 运行态RUNNABLE, 阻塞态BLOCKED,等待态WAITING,超时等待TIMED_WAITING,终止态TERMINATED
wait与sleep
wait是Object类的方法,且会释放锁,只能在同步代码块中使用,不需要异常捕获
sleep是线程类Thread的方法,不会释放锁,什么地方都能调用,需要异常捕获
JUC构成
tools工具类:
- CountDownLatch闭锁:使一个线程等待其他线程都执行完毕后再执行
- CyclicBarrier栅栏:使一组线程互相等待,当所有线程都达到某个屏障点后再执行后续操作
- Semaphore信号量:共享锁,线程通过调用acquire()获取信号量许可,当信号量中含有可用许可,线程就能获取,否则线程需要一直等待,直到有可用信号许可位置,线程通过release()释放所持有的信号量许可证
executor执行者:
执行线程的工具,真正的线程池接口为ExecutorService
- ScheduledExecutorService 解决需要任务重复执行的问题
- ScheduledThreadPoolExecutor 周期性任务调度
- atomic 原子性包,一组原子操作类。AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,均被volatile修饰,保证每次线程拿到的值都是最新的
locks锁包:
- ReentrantLock 独占锁,同一时间点只能被一个线程锁获取
- ReentrantReadWriteLock 包括子类ReadLock共享锁,WriteLock独占锁
- LockSupport 可阻塞线程和解除阻塞线程且不会引发死锁
collections集合类:
提供线程安全的集合,常见集合类对应的高并发类
- ArrayList - CopyOnWriteArrayList
- HashSet - CopyOnWriteArraySet
- HashMap - ConcurrentHashMap 等
使用
多线程创建的常规方法
继承Thread类
class ThreadTest extends Thread{
@Override
public void run() {
System.out.println("继承Thread的线程创建");
}
}
//使用
ThreadTest test = new ThreadTest();
test.start();
实现runnable接口,Callable接口效率更快
class ThreadTest2 implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口的创建线程");
}
}
//使用
Thread thread = new Thread(new ThreadTest2());
thread.start();
锁Lock
Synchronized同步锁
- 修饰代码块,同步语句块,作用范围为该代码块
- 修饰方法,同步方法,范围为该方法,作用于调用该方法的对象
- 修饰静态方法,范围为该静态方法,作用于这个类所有对象
- 修饰类,范围Synchronized括号起来的部分,作用于这个类的所有对象
Lock接口ReentrantLock
ReentrantLock接口 可重入独占锁
公平锁:先来后到排队
非公平锁:可以插队(默认)
public class ReentrantLock implements Lock, java.io.Serializable {
......
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
......
}
Synchronized与Lock区别
Synchronized:java关键字,无法获取锁状态,自动释放锁,一个线程获取到锁其他线程均需要等待,不可中断,非公平可重入锁,适合解决少量代码同步问题
Lock:类,可判断是否获取锁,需要手动释放锁,不然会出现死锁问题,可以通过tryLock()方法尝试获取锁,不让此线程一直等待,可中断,可以设置公平性的可重入锁,适合解决大量的代码同步问题
ReentrantLock使用举例
模拟简单场景:电影院热门电影抢票,同一时间多个人买了同一张票,假设共20张,编号分别为1-20号
情况1:不使用锁,排队(单线程)购买票(30人一个渠道排队购买20张票)
public class Cinema {
static class TicketOffice {
Integer ticketNum = 20;
public void sale(){
if(this.ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号票" + "当前还剩余" + --ticketNum + "张票");
}
}
}
public static void main(String[] args) {
//资源
TicketOffice ticketOffice = new TicketOffice();
//一个渠道 30人排队购买20张票
new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"A购买渠道").start();
}
}
此时票按顺序正常售出,结果为:
A购买渠道售出20号票当前还剩余19张票
A购买渠道售出19号票当前还剩余18张票
A购买渠道售出18号票当前还剩余17张票
A购买渠道售出17号票当前还剩余16张票
A购买渠道售出16号票当前还剩余15张票
A购买渠道售出15号票当前还剩余14张票
A购买渠道售出14号票当前还剩余13张票
A购买渠道售出13号票当前还剩余12张票
A购买渠道售出12号票当前还剩余11张票
A购买渠道售出11号票当前还剩余10张票
A购买渠道售出10号票当前还剩余9张票
A购买渠道售出9号票当前还剩余8张票
A购买渠道售出8号票当前还剩余7张票
A购买渠道售出7号票当前还剩余6张票
A购买渠道售出6号票当前还剩余5张票
A购买渠道售出5号票当前还剩余4张票
A购买渠道售出4号票当前还剩余3张票
A购买渠道售出3号票当前还剩余2张票
A购买渠道售出2号票当前还剩余1张票
A购买渠道售出1号票当前还剩余0张票
情况2:不使用锁,开启多个渠道(多线程)购买票(多个渠道多抢购20张票)
public class Cinema {
static class TicketOffice {
Integer ticketNum = 20;
public void sale() {
try{
if(this.ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号票" + "当前还剩余" + --ticketNum + "张票");
}
//增加每次购买的时间差,提高同时抢购的错误率
Thread.sleep(10);
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//资源
TicketOffice ticketOffice = new TicketOffice();
//A B渠道各30人准备抢购20张票
new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"A购买渠道").start();
new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"B购买渠道").start();
}
}
此时出现,相同座位号的票出售给多人,造成错误结果
B购买渠道售出20号票当前还剩余19张票
A购买渠道售出19号票当前还剩余18张票
A购买渠道售出18号票当前还剩余17张票
B购买渠道售出17号票当前还剩余16张票
A购买渠道售出16号票当前还剩余15张票
B购买渠道售出15号票当前还剩余14张票
A购买渠道售出14号票当前还剩余13张票
B购买渠道售出13号票当前还剩余12张票
B购买渠道售出12号票当前还剩余11张票
A购买渠道售出11号票当前还剩余10张票
B购买渠道售出10号票当前还剩余9张票
A购买渠道售出10号票当前还剩余8张票
B购买渠道售出8号票当前还剩余7张票
A购买渠道售出8号票当前还剩余6张票
B购买渠道售出6号票当前还剩余5张票
A购买渠道售出6号票当前还剩余5张票
A购买渠道售出5号票当前还剩余4张票
B购买渠道售出5号票当前还剩余4张票
A购买渠道售出4号票当前还剩余3张票
B购买渠道售出3号票当前还剩余2张票
A购买渠道售出2号票当前还剩余1张票
B购买渠道售出2号票当前还剩余1张票
B购买渠道售出1号票当前还剩余0张票
A购买渠道售出1号票当前还剩余0张票
情况3:基于情况2增加锁,确保线程安全,抢票系统正常售票
(1)采用传统方式加Synchronized解决并发可能产生的问题
//......
//将资源中的出售方式加上同步锁
public synchronized void sale() {
if(this.ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号
票" + "当前还剩余" + --ticketNum + "张票");
}
}
//......
(2)采用Lock锁(可重入锁ReentrantLock)
public class Cinema {
static class TicketOffice {
Lock lock = new ReentrantLock();
Integer ticketNum = 20;
public void sale() {
lock.lock();
try{
if(this.ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号票" + "当前还剩余" + --ticketNum + "张票");
}
//增加每次购买的时间差,提高同时抢购的错误率
Thread.sleep(10);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
//资源
TicketOffice ticketOffice = new TicketOffice();
//A B渠道各30人准备抢购20张票
new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"A购买渠道").start();
new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"B购买渠道").start();
}
}
此时双渠道正常售票
A购买渠道售出20号票当前还剩余19张票
A购买渠道售出19号票当前还剩余18张票
A购买渠道售出18号票当前还剩余17张票
A购买渠道售出17号票当前还剩余16张票
A购买渠道售出16号票当前还剩余15张票
A购买渠道售出15号票当前还剩余14张票
A购买渠道售出14号票当前还剩余13张票
B购买渠道售出13号票当前还剩余12张票
B购买渠道售出12号票当前还剩余11张票
B购买渠道售出11号票当前还剩余10张票
B购买渠道售出10号票当前还剩余9张票
B购买渠道售出9号票当前还剩余8张票
B购买渠道售出8号票当前还剩余7张票
B购买渠道售出7号票当前还剩余6张票
B购买渠道售出6号票当前还剩余5张票
B购买渠道售出5号票当前还剩余4张票
B购买渠道售出4号票当前还剩余3张票
B购买渠道售出3号票当前还剩余2张票
B购买渠道售出2号票当前还剩余1张票
B购买渠道售出1号票当前还剩余0张票
生产者消费者问题
背景:当库存为0时候需要生产产品,当库存不为0时候需要卖出产品
使用Synchronized解决生产者消费者
public class Test {
public static void main(String[] args) {
//资源
Products products = new Products();
new Thread(() -> {
for(int i = 0;i < 10;i++){
try {
products.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A生产者线程").start();
new Thread(() -> {
for(int i = 0;i < 10;i++){
try {
products.sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B消费者线程").start();
}
}
//资源类
class Products {
//库存
int stockNum = 0;
//生产 +1
public synchronized void produce() throws InterruptedException {
//当库存不为0时就不生产,从而进行等待,商品满了
if(stockNum > 0){
this.wait();
}
//为0的时候就生产1个
stockNum ++ ;
System.out.println(Thread.currentThread().getName() + "生产了1个产品,当前库存" + stockNum);
this.notifyAll();
}
//售货 -1
public synchronized void sale() throws InterruptedException {
//当库存为0时候就不售出,从而进行等待
if(stockNum <= 0){
this.wait();
}
//不为0正常售出
stockNum -- ;
System.out.println(Thread.currentThread().getName() + "卖出了1个产品,当前库存" + stockNum);
this.notifyAll();
}
}
运行结果正常
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
但当新增线程C生产者线程,D消费者线程后则结果就会出现问题
......
D线程卖出了1个产品,当前库存0
B线程卖出了1个产品,当前库存-1
D线程卖出了1个产品,当前库存-2
C线程生产了1个产品,当前库存-1
C线程生产了1个产品,当前库存0
......
将if改为while即可解决该虚假唤醒问题,while代码省略
虚假唤醒
使用用if或造成虚假唤醒问题,故一般使用while替换if,避免if + wait()
虚假唤醒:理解为,当所有线程都在等待的时候,突然资源允许一个线程来操作,但是此时所有线程均被唤醒,所以有效唤醒为1,其他视为虚假唤醒。
为什么if会造成虚假唤醒?
可能存在情况,比如当库存为0时,B生产者,D消费者均到达sale()方法,此时因为库存为0,于是就执行wait挂起,准备生产者生产后继续售卖,但是期间可能只生产了1个,此时唤醒B,D线程均被唤醒准备售卖,此时两个线程if不再判断,直接执行--操作,故导致库存-1的情况。
换成while后,在B,D被唤醒后,两线程均会在进行一次判断,即 while(stockNum <= 0),此时会有序微小的先后偏差,故一个减1后,库存为0,另一个就会重新被挂起,不会再售卖,避免了虚假唤醒。
JUC中的Condition监视器
相比于synchronized(wait notify),Lock中有Condition监视器(await,signal)
Condition精准的通知唤醒线程,使线程唤醒变得有序(如A线程等待则唤醒B线程,B线程等待唤醒C线程)
通过对同一锁构造不同的Condition监视器,对不同的线程程进行包装标识(自己的理解),即可达到精准唤醒指定的处于等待状态的线程。
用法举例,资源类中,有三个执行方法 A ,B,C 要求A执行完执行B,B执行完执行C(流水线操作)
//资源类
class Products {
//创建锁
Lock lock = new ReentrantLock();
//创建3个Condition监视器
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
//当number= 1执行A 2执行B 3执行C 控制顺序
int number = 1;
public void produceA(){
lock.lock();
try{
//number不等于对应的标识则挂起等待
while(number != 1){
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "A执行");
number = 2;
condition2.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock()
}
}
public void produceB(){
lock.lock();
try{
//number不等于对应的标识则挂起等待
while(number != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "B执行");
number = 3;
condition3.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock()
}
}
public void produceC(){
lock.lock();
try{
//number不等于对应的标识则挂起等待
while(number != 3){
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "C执行");
number = 1;
condition1.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock()
}
}
}
使用JUC解决生产者消费者问题
public class Test2 {
public static void main(String[] args) {
//资源
Products products = new Products();
new Thread(() -> {for (int i = 0; i < 10; i++) {products.produce();}}, "A线程").start();
new Thread(() -> {for (int i = 0; i < 10; i++) { products.sale();}}, "B线程").start();
new Thread(() -> {for (int i = 0; i < 10; i++) {products.produce();} }, "C线程").start();
new Thread(() -> { for (int i = 0; i < 10; i++) { products.sale(); } }, "D线程").start();
}
//资源类
static class Products {
//库存
int stockNum = 0;
Lock lock = new ReentrantLock();
Condition conditionProduce = lock.newCondition();
Condition conditionSale = lock.newCondition();
//生产 +1
public void produce() {
lock.lock();
try {
//当库存不为0时就不生产,从而进行等待,商品满了
while (stockNum > 0) {
conditionProduce.await();
}
//为0的时候就生产1个
stockNum++;
System.out.println(Thread.currentThread().getName() + "生产了1个产品,当前库存" + stockNum);
conditionSale.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
//售货 -1
public void sale() {
lock.lock();
try {
//当库存为0时候就不售出,从而进行等待
while (stockNum <= 0) {
conditionSale.await();
}
//不为0正常售出
stockNum--;
System.out.println(Thread.currentThread().getName() + "卖出了1个产品,当前库存" + stockNum);
conditionProduce.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
8锁现象
8锁现象:关于锁的8个问题
问题1:两个同步方法,同一个对象调用,谁先执行?
先拿到锁,谁先执行。
如下”生产“打印后,时隔5秒后打印“售卖”。
synchronized 锁住的对象是方法的调用者,此处公用一个资源products,该对象只有一把锁,故谁先拿到锁谁就会先执行。此处如果在生产方法中加入sleep 5秒同样是应为生产先拿到锁,所以produce()会先执行。
public class Test3 {
public static void main(String[] args) {
Products products = new Products();
new Thread(() -> { products.produce();}).start();
try {
//sleep 5秒
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {products.sale();}).start();
}
//资源
static class Products {
public synchronized void produce() {
System.out.println("生产");
}
public synchronized void sale() {
System.out.println("售卖");
}
}
}
问题2:两个同步方法,两个对象调用,谁先执行
如果调用均无等待延迟,则按照调用顺序执行,如果设置等待时间,则按照各自的等待时间结束执行,但实际上两个互不影响。
如下,按照执行顺序,先调用.produce(),但方法中延迟了5秒,此时主方法中延迟1秒后调用sale(),直接打印"售卖",等produce()睡眠时间结束后答应"生产"。
public class Test3 {
public static void main(String[] args) {
Products products = new Products();
Products products2 = new Products();
new Thread(() -> { products.produce();}).start();
try {
//sleep 1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {products2.sale();}).start();
}
//资源
static class Products {
public synchronized void produce() {
try {
//sleep 5秒
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产");
}
public synchronized void sale() {
System.out.println("售卖");
}
}
}
问题3:一个同步方法,一个普通方法,一个对象调用,谁先执行
普通方法的调用不受锁影响,正常调用资源方法
如下,sale()方法为非同步方法,即使同步方法produce()对资源products上锁,但对于sale()来说,这个锁对它并无作用,故可以先调用,而不是等produce()方法等待时间过后,执行后再执行,与问题1对比。
public class Test3 {
public static void main(String[] args) {
Products products = new Products();
Products products2 = new Products();
new Thread(() -> { products.produce();}).start();
new Thread(() -> {products2.sale();}).start();
}
//资源
static class Products {
public synchronized void produce() {
try {
//sleep 5秒
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产");
}
public void sale() {
System.out.println("售卖");
}
}
}
问题4:两个同步方法,一个对象调用,其中先调用的方法设置延迟时间,谁先执行?
同一对象调用,先获取到锁的方法先执行,无论sleep多久,应为sleep不释放锁,故后者也等待前者释放资源,此问题类同问题1
如下,produce()设置延迟时间10秒,sale方法也会跟着等待,等10秒后produce()执行完后再执行
public class Test3 {
public static void main(String[] args) {
Products products = new Products();
new Thread(() -> { products.produce();}).start();
new Thread(() -> {products.sale();}).start();
}
//资源
static class Products {
public synchronized void produce() {
try {
//sleep 10秒
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产");
}
public synchronized void sale() {
System.out.println("售卖");
}
}
}
问题5:两个静态同步方法,一个对象调用,谁先执行
静态方法锁得对象是Class类,所以先拿到锁先执行
如下,produce()先执行,后执行sale()
public class Test3 {
public static void main(String[] args) {
Products1 products = new Products1();
new Thread(() -> {products.produce();}).start();
new Thread(() -> {products.sale();}).start();
}
}
//资源
class Products1 {
public static synchronized void produce() {
try {
//sleep 5秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产");
}
public static synchronized void sale() {
System.out.println("售卖");
}
}
问题6:两个静态同步方法,两个对象调用,谁先执行
同问题5,静态方法锁得对象是Class类,所以先拿到锁先执行
如下,produce()先执行,后执行sale()
public class Test3 {
public static void main(String[] args) {
Products1 products = new Products1();
Products1 products2 = new Products1();
new Thread(() -> { products.produce();}).start();
new Thread(() -> {products2.sale();}).start();
}
}
//资源
class Products1 {
public static synchronized void produce() {
try {
//sleep 5秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产");
}
public static synchronized void sale() {
System.out.println("售卖");
}
}
问题7:一个静态同步方法,一个普通同步方法,一个对象调用,谁先执行
静态方法锁类对象,普通方法锁实例化对象,两个不同得锁,互不影响,故按正常执行
如下 produce()因为有延迟,故sale()先执行
public class Test3 {
public static void main(String[] args) {
Products1 products = new Products1();
new Thread(() -> { products.produce();}).start();
new Thread(() -> {products.sale();}).start();
}
}
//资源
class Products1 {
public static synchronized void produce() {
try {
//sleep 5秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产");
}
public synchronized void sale() {
System.out.println("售卖");
}
}
问题8:一个静态同步方法,一个普通同步方法,两个对象调用,谁先执行
同问题7,静态方法锁类对象,普通方法锁实例化对象,两个不同得锁,互不影响,故按正常执行
public class Test3 {
public static void main(String[] args) {
Products1 products = new Products1();
Products1 products2 = new Products1();
new Thread(() -> { products.produce();}).start();
new Thread(() -> {products2.sale();}).start();
}
}
//资源
class Products1 {
public static synchronized void produce() {
try {
//sleep 5秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产");
}
public synchronized void sale() {
System.out.println("售卖");
}
}
集合类并发安全
通常单线程开发中集合类均无问题,但是在并发情况下则会报并发修改异常java.util.ConcurrentModificationException,故要考虑解决
CopyOnWriteArrayList
解决方案1 用vector代替ArrayList,效率低不建议用
List<String> list = new Vector<>();
解决方案2 利用Collections.sysnchronizedList
List<String> list = Collections.sysnchronizedList(new ArrayList<>());
解决方案3 利用JUC CopyOnWriteArrayList解决,最优方案
List<String> list = new CopyOnWriteArrayList<>();
写入时复制
COW,计算机程序设计的一种优化策略,在写入修改操作的时候,会复制原集合副本,在副本进行修改,此时如果同步读取的话,还是会访问旧集合,等副本修改完成后,最终集合指向副本完成修改
优点:多线程调用时,解决读取问题,读写分离,写入时避免覆盖
缺点:同步读取时候,读到的数据并不是最新的
CopyOnWriteHashSet
解决方案1
Set<String> set = Collections.sysnchronizedSet(new HashSet<>());
解决方案2
Set<String> set = new CopyOnWriteHashSet<>();
ConcurrentHashMap
在并发环境下使用HashMap可能导致形成环状链表,get操作时,使得cpu空转,很危险
相对于HashTable线程安全,内部使用synchronized,但当表足够大时,导致效率低下
方案:Map<String,String> map = ConcurrentHashMap<>();
HashTable与ConcurrentHashMap区别:HashTable锁住整张表,而后者分段锁
Callable
类似于Runable,但Callable可以有返回值,可以抛出异常,Runable不行,Callable中call()取代run()
但是Thread类只能接受Runable,不能直接接受Callable,故需要通过Runable来完成调用.
(Runnable与Callable都需要通过Thread来装载)
Runnable的实现
class MyThread implements Runable{
...
@override
public void run(){
}
...
}new Thread(new Mythread()).start();
Callable的实现
class MyThread implements Callable<Integer>{
...
@override
public Integer call (){System.out.println(“运行”);
return 1;
}
}//调用
MyThread myThread = new MyThread();
FutureTask futureTask = new FutureTask(myThread);
new Thread(futureTask,"A").start();
//可以获取到返回值,此方法可能引起阻塞,可放在代码最后一行或者异步处理
Integer i = (Integer)futureTask.get();
三大常用辅助类
CountDownLatch
计数器,一个或者多个线程执行完再执行某个包含此对象的线程
countDownLatch.countDown(); 数量-1
countDownLatch.await();等待计数器的值为0,唤醒当前线程继续执行
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i =1;i <= countDownLatch.getCount();i++){
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
//计时器-1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//等上面线程都执行完后执行主线程,不然主线程会随机执行
countDownLatch.await();
System.out.println("主线程,最终执行");
}
}
CyclicBarrier
栅栏,指定一个属数n,当执行的线程到达n后才执行某个线程的内容
如代码,i如果达不到3,则栅栏中的代码不会执行,且执行完一个线程需要调用cyclicBarrier.await()来计数,否则栅栏中的代码也不会执行
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,() -> {
System.out.println("到达3后 执行");
});
for(int i =1 ; i <= 3 ;i ++){
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程执行");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
Semaphore
计数信号量,指定一个信号量,为当前需要执行的线程数,当线程数达到该信号量,其他线程则需要等待前几个资源使用完并释放信号后才能继续执行
acquire获得凭证,如果已经满员,则等待至释放为止
release 释放凭证,信号量+1,唤醒等待的线程
public class SemaphoreTest {
public static void main(String[] args) {
//设置初始信号量 2 允许两个线程执行
Semaphore semaphore = new Semaphore(2);
//4个线程 acquire获得凭证 release 释放凭证
for (int i = 1; i < 4 ;i++){
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "线程正在使用资源");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "线程释放了资源");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
读写锁ReadWriteLock
保证一个线程写入的时候,不被另一个写入操作的线程插队。
如果不加锁,如下 多线程读取的情况,5个线程写入,5个线程获取对应写入的内容
public class ReadWriteLockTest {
public static void main(String[] args) {
Cache cache = new Cache();
//写
for(int i = 1 ; i <= 5 ; i++){
final int temp = i;
new Thread(() -> {
cache.put(temp+"",temp + "");
},String.valueOf(i)).start();
}
//取
for(int i = 1 ; i <= 5 ; i++){
final int temp = i;
new Thread(() -> {
cache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
class Cache {
private volatile Map<String,Object> map = new HashMap<>();
//写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入完成");
}
//读
public void get(String key){
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取完成");
}
}
结果如下,其中1写入的时候,被5等插队,存在此类情况,古考虑添加读写锁,使读的时候不受限制,写的时候一个线程写完再写入
1写入1
5写入5
4写入4
3写入3
3写入完成
2写入2
2写入完成
4写入完成
1读取1
....
增加读写锁,允许多线程同时读取,不能边读边写,不能边写边写
读写锁:即为互斥锁也是共享锁,两种模式
read:互斥锁 - 写锁 一次只能被一个线程占有
write:共享锁 - 读锁 多个线程可以同时占有
class Cache {
private volatile Map<String,Object> map = new HashMap<>();
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//写
public void put(String key,Object value){
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入完成");
readWriteLock.writeLock().unlock();
}
//读
public void get(String key){
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取完成");
readWriteLock.readLock().unlock();
}
}
结果:写入均需要一个线程写入完成后再进行写入,读取则不限制,如果此处读取不加共享锁,则会导致一个线程还没写完就读取这个资源,就会出现问题
2写入2
2写入完成
3写入3
3写入完成
1写入1
1写入完成
.....
1读取1
1读取完成
2读取2
3读取3
3读取完成
......读取不加共享锁的情况,5还未写入就读取5
......
5读取5
5读取完成
4读取4
4读取完成
5写入5
5写入完成
阻塞队列
队列
先进先出,队列(双端队列deque,非阻塞队列AbstractQueeu,阻塞队列BlockingQueue(ArrayBlockingQueue,LinkedBlockingQueue))
阻塞队列
基于ReentrantLock的队列,常用与生产消费者模式,实现web长连接聊天等。
阻塞情况
1.当队列为空时,需要阻塞等待新元素入队
2.当队列满之后,阻塞等待队列有位置再入队
ArrayBlockingQueue
常用API
//指定容量为2
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(2);
(1)抛异常组
add 添加,成功返回true,超出报错
remove 移除,无元素报错
element 获取队首元素
(2)不抛异常组
offer 入队,成功返回true,队满返回false
poll 出队,获取当前队首元素,空队返回null
peek 获取队首元素
(3)一直阻塞等待组
put 入队,如果入队数量大于容量将一直等待直到有元素出队后入队
take 出队,获取当前队首元素,如果队列无元素则一直等待,直到有元素后获取
(4)超时等待组
超过等待的时间后则不继续等待,当前线程中断
blockingQueue.offer("A",2,TimeUnit.SECONDS)
blockingQueue.poll(2,TimeUnit.SECONDS)
LinkedBlockingQueue
两把锁控制入队,出队,只允许一个线程入队,一个线程出队,但是入队出队互不影响,可同时进行,为了维持线程安全,使用了原子安全类AtomicInteger类型标识当前队列元素个数,确保出队入队两个线程之间的线程安全。
可指定容量,不允许元素为null,如果队列满了,则会调用notFull的await()方法将该线程加入Condition等待队列中。当队列为空,则加入到notEmpty()的等待队列中。
同步队列SynchronousQueue
相当于容量为1的阻塞队列,只允许入队一个元素,等待出队后才能再次入队
put 入队,如果入队数量大于容量将一直等待直到有元素出队后入队
take 出队,获取当前队首元素,如果队列无元素则一直等待,直到有元素后获取
线程池
池化技术
提前准备好资源,在请求量大的时候优化应用性能,减低资源的消耗,提高响应速度,管理方便。
线程池
实现启动若干数量的线程,并让这些线程处于睡眠状态,当需要某个线程工作时则唤醒,完成后继续处于睡眠(非销毁线程)
优点:线程复用,控制最大并发数,管理线程
四个方法
Executors.newSingleThreadExecutor();//单线程 Executors.newFixedThreadPool(5);//固定线程数 Executors.newCachedThreadPool();//可伸缩线程数,无固定大小...
newSingleThreadExecutor() 单个线程的线程池
public class PoolTest {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();// 单线程
try{
for (int i = 0 ; i < 4 ; i++){
//使用线程池创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//线程池使用完要关闭
threadPool.shutdown();
}
}
}
运行结果,同一个线程调用
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
Executors.newFixedThreadPool(5); Executors.newCachedThreadPool();用法同上,只是创建的线程数量不同。
七个参数自定义线程池
ThreadPoolExecutor参数
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //空闲最大存活时间
TimeUnit unit,//超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler) //拒绝策略
public class PoolTest {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2, 5, 3,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());//阻塞吼的处理策略,AbortPolicy-》抛出异常
try {
for (int i = 1; i <= 11; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "线程执行");
});
}
} catch (Exception e){
e.printStackTrace();
}
}
}
建议不要直接使用Executors创建线程池,通过ThreadPoolExecutor创建
ExecutorService threadPool = new ThreadPoolExecutor();创建线程池,并且通过自定义参数,定制线程池。
threadPool.execute()线程执行
corePoolSize, //核心线程池大小 基础工作的线程数
maximumPoolSize, //最大核心线程池大小,当达到条件,开启附加线程进行工作
keepAliveTime, //超时等待,空闲最大存活时间,超过时间,则关闭附加线程
TimeUnit unit,//超时时间单位设置
BlockingQueue<Runnable> workQueue, //阻塞队列,用于存放当前需要被线程操作的资源,指定个数,当达到最大值,则出发附加线程进行工作
ThreadFactory threadFactory, //线程工厂 一般不做改动
RejectedExecutionHandler handler) //拒绝策略,当需要运行的线程数量大于线程池最大的线程容量,需要做的事。线程池承载量=核心线程数+附加线程数+阻塞队列的容量如定义线程池核心线程为3,最大核心线程池大小为5,则附加线程为5-2=3个,核心线程用于处理线程,而附加线程睡眠,资源入阻塞队列等待被操作,当阻塞到达一定情况,则触发附加线程一起工作。
4个拒绝策略
ThreadPoolExecutor.AbortPolicy() 满了抛异常ThreadPoolExecutor.CallerRunsPolicy() 哪里来回哪去,由调用的线程去处理ThreadPoolExecutor.DiscardPolicy() 队列满了不抛异常,丢弃多余任务ThreadPoolExecutor.DiscardOldestPolicy() 队列满了,尝试与第一个线程竞争资源
ForkJoin
并行执行任务
工作窃取:某线程工作执行完毕后会去执行其他线程的工作