文章目录
- 线程通信
- 概念
- 使用方式
- 案例
- 单例模式
- 阻塞式队列
- 线程池
- 常见的锁策略
- 乐观锁 悲观锁
- CAS
- CAS存在的问题:ABA问题
- 读写锁
- 自旋锁
- 公平锁 非公平锁
- 非公平锁
- 公平锁
- synchronized
- jvm对synchronized的优化:锁升级
- synchronized的其他优化
- Lock体系
- synchronized vs lock
- 独占锁vs共享锁
- 独占锁
- 共享锁
- Semaphare:信号量
- CountDownLatch
- 死锁
- 线程安全的集合
- Map:重点
- Hashtable
- ConcurrentHashMap:并发的hashmap
线程通信
背景:多线程优势是提高cpu利用率,使用要注意:执行时间比较长的任务,可能存在线程安全问题
以上前提下,能否满足一种业务需求:需要让线程执行具有一定的顺序性
概念
线程间通信,就是一个线程以通知的方式,唤醒某些等待线程(也可以在某些条件下,让当前线程等待),这样就可以让线程间通过通信的方式满足一定的顺序性
使用方式
线程间通过通信的方式满足一定的顺序性:
在这里插入代码public class 简单使用 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (lock) {
//先做一些事情
System.out.println("线程1:步骤1");
//在某些条件下,就需要等待
lock.wait();
//被唤醒,就做另一些事情
System.out.println("线程1:步骤2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
/**
* 执行顺序:
* 线程1 synchronized -> wait
* 线程2 synchronized -> notify
* 线程1 wait 往下
*/
Thread.sleep(100);
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
//做一些事情
System.out.println("线程2:步骤1");
//通知线程1:唤醒执行
lock.notify();
//做另一些事情
System.out.println("线程2:步骤2");
}
}
}).start();
}
}片
案例
单例模式
更具体参考之前博客:java设计模式之单例模式
阻塞式队列
概念:
满足队列的结构和特性(链式或数组结构,先进先出),也满足线程安全:
好处:
生产者消费者模型
import java.util.Random;
public class 阻塞队列 {
//循环数组:存取元素
private int[] elements;
//有效负荷
private int size;
//放元素的索引
private int putIndex;
//取元素的索引
private int takeIndex;
/**
* @param capacity 容量
*/
public 阻塞队列(int capacity){
elements = new int[capacity];
}
/**
* 放元素到阻塞队列:需要保证线程安全,如果队列满了,需要等待
* @param element
*/
public synchronized void put(int element) throws InterruptedException {
//如果满了,就等
while(elements.length == size){
wait();
}
//如果不满,就放
elements[putIndex] = element;
//放的索引往后移动一位(取模是在最后一个位置就往首位移动)
putIndex = (putIndex+1) % elements.length;
//有效负荷+1
size++;
//通知wait等待的线程
notifyAll();
}
/**
* 取元素:需要保证线程安全,如果队列是空,需要等待
* @return
*/
public synchronized int take() throws InterruptedException {
//如果是空,就等
while(size == 0){
wait();
}
//如果不空,就取
int element = elements[takeIndex];
//取的索引往后移动一位(取模是在最后一个位置就往首位移动)
takeIndex = (takeIndex+1) % elements.length;
//有效负荷-1
size--;
//通知wait等待的线程
notifyAll();
return element;
}
public static void main(String[] args) throws InterruptedException {
阻塞队列 queue = new 阻塞队列(20);
new Thread(new Runnable() {
@Override
public void run() {
//模拟消费者
while (true){
try {
int e = queue.take();
System.out.println("取到的数字:"+e);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Thread.sleep(100);
new Thread(new Runnable() {
@Override
public void run() {
//模拟生产者
while (true){
try {
queue.put(new Random().nextInt());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
线程池
线程池:就是初始化(new线程池)的时候,就创建一定数量的线程(不停的从线程池内部的一个阻塞队列)取任务(消费者);我们就可以在其他线程中,提交任务(生产者)到线程池了。
jdk原生的线程池api
常见的锁策略
乐观锁 悲观锁
线程冲突:多个线程并发并行的对同一个共享变量操作(存在线程安全问题)
悲观锁:以悲观的心态看待线程冲突(总有刁民要害朕,总是觉得有其他线程会同时操作共享变量)。所以每次都加锁操作共享变量
乐观锁:以乐观的心态看待线程冲突(总是觉得没有线程会同时操作共享变量)。所以每次都不加锁(程序层面),就直接操作共享变量(依赖操作系统及cpu的一些功能,来操作(里边实际有加锁))
CAS
乐观锁(加锁的一种思想,程序看是无锁操作)的一种实现
CAS存在的问题:ABA问题
ABA问题:读和写操作,比较主存中变量的值时,值没有变,都是A,但实际已经被其他线程修改过了(A=>B=>A)
解决方式:引入一个版本号的字段来解决
读写锁
优酷网站,某个用户上传了一个视频文件,这个用户可以修改(写操作),所有用户都可以播放(读操作)
满足:读读并发,读写,写写都是互斥
自旋锁
一般会搭配乐观锁一起来实现
通常结合CAS一起来保证线程安全的修改变量操作
CAS虽然是线程安全的无锁操作,但可能修改失败===>引入自旋的操作,满足不停的尝试修改
CAS+自旋实现线程安全无锁的++ vs synchronized加锁保证线程安全的++
cas+自旋: 一直处于可运行态(有些地方也直接说运行态)
所以如果尝试修改操作,能很快得到执行,效率就比较好(比较适用这种场景)。反之,不能很快得到执行,效率就差。
加锁操作: 申请锁失败会阻塞(线程状态会从运行态转变为阻塞态),锁释放以后,又会被唤醒再次竞争锁(阻塞态=>被唤醒)都有性能消耗
公平锁 非公平锁
非公平锁
synchronized申请对象锁,是竞争的方式
优点: 效率更高(不考虑执行顺序)
缺点: 可能出现线程饥饿(某些线程长期得不到执行)的现象
公平锁
以申请锁的时间先后顺序,来获取到锁——类似排队买票的方式
缺点: 效率稍微差一些
synchronized
以对象头加锁的方式,实现线程安全:申请同一个对象锁时,多个线程间是同步互斥的
对象中有一个对象头,其中就有一个状态的字段:无锁,偏向锁,轻量级锁,重量级锁。
synchronized申请锁成功,是后三种锁状态之一
jvm对synchronized的优化:锁升级
synchronized的其他优化
锁消除
锁粗化:共享变量,可能被其他线程持有,但不停的执行加锁释放锁操作,jvm就可以优化为第一次加锁,全部执行完再释放锁
Lock体系
lock:悲观锁
synchronized vs lock
1.Lock提供了非公平锁和公平锁,而synchronized只是非公平锁
2.线程竞争锁激烈的时候,使用Lock效率更高。
释放锁以后,synchronized是唤醒所有线程来竞争(涉及线程状态转换,就有性能消耗)。lock是唤醒一个(使用aqs来管理线程)
独占锁vs共享锁
独占锁
(1) synchronized加锁(2) Lock加锁 (3)写锁
共享锁
多个线程使用共享的锁,满足一定的条件,就可以并发并行的执行,不满足条件,就需要等待。
读锁属于共享锁
Semaphare:信号量
使用场景:
(1)有限资源的并发执行
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
private static Semaphore S = new Semaphore(5);
public static void main(String[] args) {
//模拟停车场同时停车,但不能超出有限的车位数量
for (int i = 0; i < 20; i++) {
final int j = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
//获取一个车位
S.acquire();
//停车以后做其他事情
System.out.println(j);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//出停车场,归还车位
S.release();
}
}
}).start();
}
}
}
(2)多个线程并发执行,需要全部执行完,某个线程再执行后续的代码
import java.util.concurrent.Semaphore;
public class SemaphoreDemo2 {
private static Semaphore S = new Semaphore(0);
public static void main(String[] args) throws InterruptedException {
//模拟停车场同时停车,但不能超出有限的车位数量
for (int i = 0; i < 5; i++) {
final int j = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(j);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
S.release();
}
}
}).start();
}
S.acquire(5);
System.out.println("main");
}
}
System.out.println(“main”);会等到5个线程执行完后再执行。
CountDownLatch
Semaphore vs CountDownLatch
单纯的从api角度看,CountDownLatch的资源数只能减,semaphore是可以加也可以减。
死锁
双方都持有对方需要的锁,而发生双方无限期的等待锁释放
死锁产生的四个必要条件:
如何解决死锁问题:
破坏这个循环等待的条件 : 比如大家按照一定的顺序来申请锁
线程安全的集合
Map:重点
Hashtable
底层数据结构:数组+链表 相当于锁整个数组
特性: 所有方法都是synchronzied加锁保证线程安全
缺点: 效率不高
ConcurrentHashMap:并发的hashmap
1.8的实现
底层数据:数组+链表+红黑树
1.7的实现:基于数组+链表
实现方式:分段锁(Segment)