并发
1.进程和线程的区别
①进程是资源分配的最小单位,线程是CPU调度的最小单位。
②一个进程可以有多个线程,也就是进程是线程的容器,每个进程都至少有一个线程。
2.并发编程模型中的两个关键问题
①线程之间如何通信
②线程之间如何同步
3.通信的两种机制
①共享内存
②消息传递
4.JMM(java内存模型)
- Java的并发采用的是共享内存模型
- 主内存和工作内存
- 处理器上的寄存器的读写比内存快几个数量级,为了解决这种,加入了高速缓存
- 带来的问题?
- 缓存一致性问题
- 带来的问题?
- 处理器上的寄存器的读写比内存快几个数量级,为了解决这种,加入了高速缓存
- JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,它试图屏蔽各种操作系统的内存访问差异,以实现java程序在各种平台下都能达到一致的内存访问效果
- 8总内存间交互操作
- read 把一个变量的值从主内存传输到工作内存
- load 在read之后执行,把read得到的值放入工作内存的变量副本中
- use 把工作内存中一个变量的值传递给执行引擎。
- assign 作用于工作内存的变量,它把一个从执行引擎收到的值赋值给工作内存的变量
- store 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
- write 作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
- lock 作用于主内存的变量,把一个变量标识为一条线程独占状态
- unlock 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- happens-before
- 从JDK5开始,java使用happens-before概念来阐述操作间的内存可见性。
- 在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。
- 两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
- 规则
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 线程中断规则:对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
- java内存模型中的重排序
- 为什么需要指令重排序?
- 因为指令重排序能够提高性能
- 指令重排序的种类
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
- 规则
- as-if-serial 不管如何重排序,都必须保证代码在单线程下的运行正确
- 解决方案
- 使用锁同步
- 在临界区内的代码可以重排序,但是由于互斥特性,其他线程感受不到重排序带来的影响
- 基于happens-Before规则编程
- 使用锁同步
- 可以使用内存屏障的方法禁止重排序,我们遵守happens-Before规则底层就是使用了内存屏障
- 为什么需要指令重排序?
5.实现线程的方式
①继承Thread类重写run方法
缺点:java中类只能支持单继承。
②实现Runnable接口
缺点:不能获取返回值,还不能抛出异常
③实现Callable接口以及call方法,通过FutureTask包装器来创建Thread线程
特点:可以获取线程的返回值,可以抛出异常,并且get方法在获取返回值的时候会被阻塞,而且结果还会被缓存
##对于Thread来说只能执行实现Runnable接口的类,所有实现Callable并不能直接执行,所有需要借助一个中间类FutureTask类来运行
6.线程的启动方式
①线程的启动方式只有一种,将实现接口或者被继承的类放入Thread类中通过start启动
②java的线程启动是依赖C或C++实现的,java本身不能够开启线程,java的start方法底层依赖start0方法启动,而start0方法是一个native方法
7.Java程序运行的时候至少有两个线程
①主线程
②GC线程
8.Java中线程的6个状态
①NEW 新生
②RUNNABLE 运行
③BLOCKED 阻塞
④WAITING 等待
⑤TIMED_WAITING 超时等待
⑥TERMINATED 终止
在java.lang.Thread类里面有个State的枚举类
9.线程操作的常用方法
- new Thread().setDaemon(boolean on)
- 设置为守护线程
- 当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
- 必须在开启线程前使用setDaemon方法
- Thread.sleep(long millis)
- 休眠当前线程,单位为毫秒
- sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回
- sleep不会释放锁
- 建议使用TimeUnit类代替sleep,因为TimeUnit能够更精细化控制
- 如果线程睡眠时间到了,该线程也不会立即执行,只是从睡眠状态变成了可运行状态
- new Thread().setPriority(int newPriority)
- 设置线程的优先级
- 优先级分为1~10,数值越大优先级越高,CPU优先执行优先级高的任务
- 优先级高的任务并不一定先执行
- Thread.yield()
- 线程让步,当前线程会释放CPU的执行权,转入就绪状态。
- yield不会释放锁
- new Thread().join(long millis, int nanos)
- 阻塞调用此方法的线程进入 TIMED_WAITING 状态,直到目标线程完成,此线程再继续;
- inInterrsupted和interrupted区别
- isInterrupted是非静态的
- interrupted是静态的,而且本质上是调用了当前线程中的isInterrupted 方法,不过传入了一个参数true
- interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态
- isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted(),不会重置当前线程的中断状态
- new Object()
- wait
- 让线程进入等待
- 会释放锁,但是不会释放所有的锁,只会释放当前对象的锁
- notify和notifyAll
- notifyAll唤醒所有的线程
- notify随机唤醒一个线程,如果被唤醒的线程之前放弃的锁被其他对象所有,那么这个线程会进入阻塞状态,他必须等待其他线程释放这个锁,才能开始执行。
- wait
/**
* 首先让线程0拿到object这个锁,然后让线程0进入阻塞状态.
* 之后主线程唤醒线程0,然后但是不释放锁,发现这个时候线程0虽然被唤醒,但是它之前放弃的锁被主线程使用,所以他会进入阻塞状态,等待其他线程释放锁
*/
public class Thread004 {
static Object object=new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
synchronized (object){
try {
System.out.println(Thread.currentThread().getName()+"获取到锁并且准备进入wait");
object.wait();
System.out.println(Thread.currentThread().getName()+"被唤醒");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("thread1运行中吗"+thread.getState());
TimeUnit.SECONDS.sleep(1);
System.out.println("thread1等待中吗"+thread.getState());
synchronized (object){
object.notify();
System.out.println("唤醒了线程"+thread.getName());
System.out.println("thread0被唤醒了吗"+thread.getState());
System.out.println(Thread.currentThread().getName()+"准备获取到锁并且进入同步代码块");
System.out.println(Thread.currentThread().getName()+"获取到锁");
TimeUnit.SECONDS.sleep(10);
}
}
}
- 使用notifyAll和wait实现一个生产者和消费者模型
public class Thread001 {
private static int resourceNum=0;//资源数
private static int resourceMax=10;//最大生产数
public static void main(String[] args) {
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
con();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者1").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
con();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者2").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
pro();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者1").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
pro();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者2").start();
}
/**
* 生产者
* @throws InterruptedException
*/
public static synchronized void con() throws InterruptedException {
while (resourceNum>=resourceMax){
Thread001.class.wait();
}
System.out.println(Thread.currentThread().getName()+"生产了一件,剩余"+(++resourceNum));
Thread001.class.notifyAll();
}
/**
* 消费者
* @throws InterruptedException
*/
public static synchronized void pro() throws InterruptedException {
while (resourceNum<=0){
Thread001.class.wait();
}
System.out.println(Thread.currentThread().getName()+"消费了一件,剩余"+(--resourceNum));
Thread001.class.notifyAll();
}
}
10.synchronized
- 锁升级过程
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
- 轻量级锁
- 偏向锁
- 无锁
- 特性
- 可重入性
- 原理
- 每个synchronized都和一个monitor关联,当获得锁的时候monitor中的成员变量recursions就+1
- 可以避免死锁,如果没有可重入特性,那么线程第二次获得该锁的时候就会死锁
- 不可中断特性
- 异常会释放锁
- 原理
- 可重入性
- 锁的对象
- 在静态方法上synchronized锁的是当前类的class对象
- 在普通方法上synchronized锁的是当前对象
- 传入对象的时候synchronized锁的是传入对象
11.CAS(乐观锁)
- 简介
- Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。
- 作用
- CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧
的预估值X等于内存中的值V,就将新的值B保存到内存中。
- CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧
- 使用CAS和阻塞队列实现一个锁
/**
* @author lixintong
* @date 2021/9/9 下午 2:25
*/
public class CASDemo {
public static void main(String[] args) {
MyLock myLock = new MyLock();
new Thread(()->{
myLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
TimeUnit.SECONDS.sleep(1);
myLock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"释放锁");
}).start();
new Thread(()->{
myLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
TimeUnit.SECONDS.sleep(1);
myLock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"释放锁");
}).start();
}
}
/**
* 使用CAS自定义一个锁
*/
class MyLock{
private AtomicInteger state=new AtomicInteger(1);
LinkedBlockingQueue<Thread> threads=new LinkedBlockingQueue<Thread>();//存放被park的线程,相当于自定义的AQS
public void lock(){
while (!state.compareAndSet(1, 0)){
threads.add(Thread.currentThread());
LockSupport.park();
};
}
public void unlock() throws InterruptedException {
while (!state.compareAndSet(0, 1)){};
if(threads.size()!=0){
Thread take = threads.take();
LockSupport.unpark(take);
}
}
}
12.JUC
- 什么是JUC
- JUC是java.util.concurrent工具包的简称,他是并发大师Doug Lea的杰作
- AQS(AbstractQueuedSynchronizer)队列同步器
- 是JUC的核心
- 原理
- 围绕着一个同步队列和park还有自旋锁实现
- 获取公平锁和非公平锁的区别
- 公平锁线程在唤醒的时候不能插队,非公平锁可以插队
- 详解(以公平锁为例)
- lock加锁
- lock方法真正调用的是acquire(1方法)
- acquire方法
public final void acquire(int arg) {
// 尝试获取同步器tryAcquire false–> 入队addWaiter --> park阻塞该线程acquireQueued
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}- tryAcquire
tryAcquire继承AQS,是获取锁的具体逻辑,如果锁的状态为0,那么获取成功返回true,如果锁的状态不为0,但是当独占的线程是本身,那么计数器加一,实现可重入锁,返回true,如果获取锁失败则返回false- 返回true
- 加锁成功,不在执行后续方法
- 返回false(说明当前锁对象正在被其他线程所持有)
-
调用addWaiter
将当前请求锁的线程包装成Node并且放到等待队列中,并返回该Node。
在该方法中有一个enq方法,他循环体里面的代码因为不是线程安全的,所以会导致尾分叉问题,但是由于每次tail只会执行一个节点,那些尾分叉节点肯定会CAS失败,所以enq会多次自旋等待所有的节点都全部入队成功- 调用AcquireQueued
拿到上一个方法返回的Node节点,如果该节点是的上一个节点是头节点,那么他不断自旋获取锁,如果不是则先自旋两次然后调用park方法阻塞当前线程。
上述的两个操作是一直被循环执行的,也就是说同步队列只有第一个节点才会尝试自旋,这个第一个节点指的是第一个非空节点。
- 调用AcquireQueued
-
- 返回true
- tryAcquire
- acquire方法
- lock方法真正调用的是acquire(1方法)
- unlock解锁
- 真正调用的是sync.release方法
- 该方法会唤醒第一个排队线程,并且把排队线程的上一个节点的ownerThread置为null,然后让head指向该节点
- 真正调用的是sync.release方法
- lock加锁
- ReentrantLock
- 可重入可中断锁,他是Lock最重要的实现类之一
- FairSync()这个方法是公平锁,调用NonfairSync()方法是非公平锁
- CountDownLatch(减法计数器)
- 用法
- 创建该对象的时候可以传入一个数值,每次调用countDown就会减一,计数器为零的时候await才不会阻塞
- 用法
public class Thread11 {
// 计数器
public static void main(String[] args) throws InterruptedException {
// 总数是6,必须要执行任务的时候,再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown(); // 数量-1
},String.valueOf(i)).start();
}
countDownLatch.await(); // 等待计数器归零,然后再向下执行
System.out.println("Close Door");
}
}
- CyclicBarrier(加法计数器)
- 用法
- 创建该对象的时候需要设置一个值,并且可以写一个实现Runnable的类,每次调用await就会加1,当加到设定的值后就会触发实现Runnable类的线程开启
- 用法
public class Thread11 {
// 计数器
public static void main(String[] args) throws InterruptedException {
/**
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;// lambda能不能操作到i,需要借助final来控制
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
try {
cyclicBarrier.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- Semaphore(信号量)
- 用法
- 可以用来做多线程限流,创建该对象的时候可以传入一个资源数量,每次调用acquire就会减少一个资源,资源为零的时候在调用该方法会导致他的线程进入阻塞状态,除非调用release释放锁。
- 用法
public class Thread11 {
// 计数器
public static void main(String[] args) throws InterruptedException {
// 线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
// acquire() 得到
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(); // release() 释放
}
},String.valueOf(i)).start();
}
}
}
- 读写锁ReentrantReadWriteLock
- 读锁和写锁互斥
- 写锁与写锁互斥
- 读锁和读锁可以共存
- 演示
/**
* @author lixintong
* @date 2021/9/9 下午 3:35
*
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
* ReadWriteLock
* 读-读 可以共存!
* 读-写 不能共存!
* 写-写 不能共存!
*
*/
public class Thread11 {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
// 读取
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
// 加锁的
class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
// 读写锁: 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock lock = new ReentrantLock();
// 存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
// 取,读,所有人都可以读!
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
/**
* 自定义缓存
*/
class MyCache{
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()+"写入OK");
}
// 取,读
public void get(String key){
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
}
}
- 阻塞队列
- 详情参考容器中的BlockingQueue
- SynchronousQueue
- 容量为一的同步队列
- 线程池
- 阿里巴巴开发规范中不允许使用Executors去创建线程池,而是推荐使用ThreadPoolExecutor的方式创建
- 三大方法
- Executors.newSingleThreadExecutor();// 单个线程
- Executors.newFixedThreadPool(5); // 创建一个固定的线程池的大小
- Executors.newCachedThreadPool(); // 可伸缩
- 七大参数
- int corePoolSize, // 核心线程池大小
- int maximumPoolSize, // 最大核心线程池大小
- long keepAliveTime, // 超时了没有人调用就会释放
- TimeUnit unit, // 超时单位
- BlockingQueue workQueue, // 阻塞队列
- ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
- RejectedExecutionHandler handle // 拒绝策略
- 4种拒绝策略
- 假设现在有一个银行,银行的窗口就是线程,进来办理业务的人就是task
- new ThreadPoolExecutor.AbortPolicy() // 满了,还有人进来,不处理这个人的,抛出异常
- new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
- new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
- new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出常!
- ForkJoin
- Future
如果本文对你有很大帮助,请点赞收藏一下,谢谢!!!
这篇博客详细介绍了Java并发编程的相关知识,包括进程与线程的区别、并发编程的关键问题、通信机制、Java内存模型(JMM)、并发控制手段如synchronized、CAS,以及JUC组件如CountDownLatch、Semaphore和线程池等。同时,文中还讨论了线程的启动方式、状态以及常用的操作方法。
170万+

被折叠的 条评论
为什么被折叠?



