【JUC基础学习笔记】

一、JUC概述

1. JUC是什么

Java.Util.Concurrent包简称JUC,它主要是负责处理线程,实现多线程通信、线程安全、线程间高并发的工具包。

2. 进程与线程
  • 1)进程
    进程即正在运行的程序,可以理解为一个程序的实例对象,它是资源分配的最小单位。在操作系统中,进程由代码块、数据块、程序控制块PCB三部分组成。进程的创建也能理解为PCB的创建。
进程状态:新建态、就绪态、运行态、阻塞态、终止态、(阻塞挂起、就绪挂起)

在这里插入图片描述

  • 2)线程
    线程是轻量级的进程,它是cpu进行调度的最小单位,在Java中一个线程对应一个具体物理线程实体。
线程状态:
          NEW:新建态,创建线程还未启动
     RUNNABLE:可运行态,包括就绪态和运行态
   TERMINATED:终止态,线程结束并回收线程资源
      BLOCKED:阻塞态,线程等待某项资源而主动放弃cpu进行阻塞态
      WAITING:无限等待,调用wait()方法线程会进入无限等待状态,等待其他线程唤醒
TIMED_WAITING:有限等待,调用sleep()方法或带参wait(t)方法,线程进入等待状态直到设置时间才被唤醒。
  • 3)两者异同
    关系:一个进程至少包含一个线程。
    切换:进程切换消耗较多资源、线程切换消耗较少资源。
    资源:进程中的资源能被其线程共享访问。
3. 并发与并行
  • 1)并发:一段时间内,多个进程或线程交替运行。
  • 2)并行:一段时间内,多个进程或线程同时运行。
  • 3)串行:一段时间内,只允许一个进程或线程运行。
4. sleep() 和 wait()等方法
  • 1)sleep()与wait():
    调用两个方法都能让线程进入等待状态,前者有限等待、后者无限等待。可以从以下4点进行区分:
    来源:sleep()是Thread类方法、wait()是Object类方法。
    解锁:sleep()不会释放锁、wait()释放锁。
    唤醒:sleep()等待时间到了会自动唤醒、wait()需要被其他线程调用notify()或notifyAll()唤醒。
    位置:sleep()可以在任意位置被调用,wait()只能在同步块中使用,因为这里存在一个同步关系:wait()方法必须要在notify()、notifyAll()方法之前执行,通过一个同步代码块来实现,否在该进程会错过通知永远被阻塞
  • 2)join()与yield()
    join()可以理解为插队线程,在当前线程A内其他线程B的join()方法会让当前线程A进行阻塞态,直到线程B运行结束才会被其唤醒。
    yield()可以理解为礼让线程,当前线程A调用yield()方法会让其从运行态退出,重新与其他线程争抢cpu资源。

二、Lock

1. Synchronized

Synchronized是Java提供的关键字,是一种同步锁(对方法或者代码块中存在共享数据的操作),同步锁可以是任意对象,主要用于实现线程同步操作,保证线程安全。

  • 修饰代码块:被修饰的代码块被叫为同步代码块,用Synchronized() {}进行定义,作用范围为大括号区间内。
  • 修饰普通方法:普通成员方法添加Synchronized关键字修饰可以变为同步方法,其作用范围为同一对象。只能有一个线程调用同一对象的同步方法,其余想要调用此同步方法的线程将会被阻塞。即同一对象的同步方法争抢一把锁,不同对象则有多把锁。
  • 修饰静态方法:静态方法添加Synchronized关键字修饰可以变为静态同步方法,其作用范围为整个类。所有该类的实例变量争抢同一把锁。
    同步方法只能被显示的设置、子类继承父类的同步方法,在默认情况下不是同步的。
2. Lock接口与方法

Lock接口是JUC提供的一种更加灵活,功能更为强大的同步锁框架。其有多个功能强大的接口和实现类,例如Future(未来任务接口)、Callable(具有返回值的线程接口)、Executor(线程池接口)、ReentrantLock类(可重入锁)等等。

		//基本用法
		Lock() lock = new ReentrantLock();
		lock.lock();
        try {
        	// 处理逻辑
        } catch(Exception e) {
        	// 处理异常
        } finally {
        	lock.unlock();
        }
3. 两者异同
  • 实现:Synchronized是Java提供的内置关键字、Lock()是JDK实现的接口。
  • 性能:旧版本中Synchronized同步锁性能很低,新版在进行相关锁优化后与ReentrantLock性能大致相同。
  • 中断:当持有锁线程长期不释放锁的情况下,等待Synchronized同步锁的线程不能放弃等待,也不能响应中断。ReentrantLock可以响应中断,放弃等待,去处理其他事情。调用interrupt()方法可以设置线程中断标记,并且中断该线程。如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException。如果该线程没有抛出异常语句,通过interrupted()也可以中断线程。
	while(!interrupted()) {
		// 处理逻辑
	}
  • 公平:Synchronized同步锁和ReentrantLock锁都是非公平锁,但后者可以通过带参构造函数变为公平锁。
	// 变为公平锁
	Lock lock = new ReentrantLock(true);
  • 等待条件:Lock实例对象可以通过newCondition()获取与当前锁绑定的条件对象,一个ReentrantLock对象可以绑定多个条件。通过await()方法和signal()/signalAll方法实现等待和通知,而Synchronized同步锁只能绑定一个等待条件,通过wait()方法和notify()/notifyAll()方法实现等待与通知。
  • 释放:ReentrantLock需要手动释放锁,sync自动释放锁。
4. Lock接口重要实现类

三、线程通信 / 线程同步

1. 生产者消费者问题

1)生产者:生产资源供消费者使用,如果资源区已满,则需等待消费者消费。满则等待
2)消费者:消费生产者生产的资源,若资源区为空,则需等待生产者生产。空则等待
3)资源区/临界区:存放有限资源的区域。

2. 读者写者问题

1)读者:读操作、多个读者线程可以共享访问,且读线程与写线程之间需互斥访问。
2)写者:写操作、多个写线程需要互斥访问,且读线程与写线程之间需互斥访问。
3)分析:读读共享、读写互斥、写写互斥
4)ReadWriteLock读写锁,通过readLock() 和 writeLock() 来实现读写分离。

3. 哲学家问题

1)死锁形成4个条件:资源共享、不可剥夺、请求与保持、循环等待。
2)预防死锁:打破死锁形成四条件。
3)避免死锁:银行家算法(Allocated、Need、Max、Left)
4)死锁判断与解决:对资源分配图进行简化分析。

四、线程安全

1. 阻塞同步

1)悲观锁
2)Sync、ReentantLock

2. 非阻塞同步

1)乐观锁
2)CAS

3. 非同步

1)纯代码 pure code
2)本地线程变量 ThreadLocal

五、集合的线程安全

1. 并发集合 ArrayList

ArrayList底层采用可调整大小的数组来实现,支持随机有序访问,可重复添加元素。其add() 方法没有添加同步关键字,在进行多线程操作时会出现并发修改异常。ConcurrentModificationException

List<String> list = new ArrayList<> ();
2. Vector

Vector列表中的所有方法都有synchronized同步关键字修饰,是线程安全的,不支持并发操作,效率较低。

public synchronized boolean add(E e){}
3. Collections

可以通过Collections工具类将ArrayList<>() 、HashSet<>() 转换为线程安全的集合。

Collections.synchronizedList(new ArrayList<T> ());
Collections.synchronizedSet(new HashSet<T> ());
Collections.synchronizedMap(new HashMap<K,V> ());
4. JUC.CopyOnWriteArrayList

JUC提供的线程安全的集合,底层原理涉及写时复制技术。即:在对集合进行写操作时,会进行copy,复制一个集合副本对象,并执行写操作,完成之后再将原引用指向此对象。而再进行读操作则支持并发。

List<String> list = new CopyOnWriteArrayList<>();
Set<String> set = new CopyOnWriteArraySet<>();
Map<String,String> map = new ConcurrentHashMap<>();
// 没有CopyOnWriteMap()对象;

六、线程池

1. Executor 和 Executors

Executor线程池框架,用于创建线程并对线程生命周期进行管理,减少线程开销、增加线程复用性
1)Executor框架结构
在这里插入图片描述
2)Executors工具类
Executors工具类提供了多种默认线程池模板创建方法。

// 创建一个线程大小的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建固定个数线程大小的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 创建线程池随着任务线程多少而变化
ExecutorService threadPool = Executors.newCachedThreadPool();

// 执行线程: void execute(Runnable command);
threadPool.execute();
// 关闭线程池
threadPool.shutdown();

!但通常不建议使用这几种方式创建线程:
(1)newSingleThreadExecutor() 和 newFixedThreadPool(5) 方法会创建大小为Integer.MAX_VALUE大小的阻塞队列,大量请求堆积,导致OOM。
(2)newCachedThreadPool() 会创建无限个线程,导致OOM。

2. 线程池创建

7大参数、4种拒绝策略
1)线程池原理
在这里插入图片描述
2)创建线程池 ThreadPoolExecutor()

public ThreadPoolExecutor(int corePoolSize,	// 核心线程数
                         int maximumPoolSize, // 最大线程数
                         long keepAliveTime, // 临时线程存活时间
                         TimeUnit unit, // 时间单位
                         BlockingQueue<Runnable> workQueue, // 阻塞队列
                         ThreadFactory threadFactory, // 线程创建工厂
                         RejectedExecutionHandler handler //拒绝策略 
                         ){
    if (corePoolSize < 0 ||
      maximumPoolSize <= 0 ||
      maximumPoolSize < corePoolSize ||
     keepAliveTime < 0)
      throw new IllegalArgumentException();
      if (workQueue == null || threadFactory == null || handler == null)
         throw new NullPointerException();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
}
3. 线程池特点与应用

池化技术是创建和管理一个连接的缓冲池的技术,可以对程序中珍惜重要的资源进行有效管理。线程的创建、销毁以及切换都会带来开销,影响系统整体性能,通过线程池技术可以维持多个线程重复使用,并对线程的分配回收、切换进行管理,不仅能够保证内核的充分利用,还能防止过分调度。

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

七、线程创建

1. 继承Thread

继承Thread类重写run() 方法即可,但由于Java只能单继承,所以此类不能再继承其他类。一般不采用这种方式创建线程,继承Thread类开销过大

	class MyThread extends Thread {
		@Override
	    public void run() {
	        // 逻辑
	    }
	}
	Thread t1 = new MyThread();
	t1.start();
2. 实现Runnable接口
	// Thread t1 = new Thread(()->{});
	Thread t1 = new Thread(new Runnable() {
		@Override
		public void run() {
		   // 逻辑
	    }
	});
	t1.start();
3. 实现Callable接口

实现Callable接口并重写call() 方法,该方法具有返回值。可以通过创建FutureTask对象传入Callable实现类,通过线程Thread驱动执行任务,并调用该对象的get() 方法获取返回值。

	// FutureTast<Integer> future = new FutrueTask<> (()->{return 1;})
	FutureTast<Integer> futureTask1 = new FutrueTask<> (new Callable() {
		@Override
	    public Integer call() throws Exception {
	        return 10;
	    }
	});
	Thread t1 = new Thread(futureTask1);
	t1.start();
	// 调用get()方法获取返回值。
	futureTask1.get();

所谓的FutureTask是在不影响主任务的同时,开启单线程完成某个特别的任务,之后主线程续上单线程的结果即可(该单线程汇总给主线程只需要一次即可)。如果之后主线程在开启该单线程,可以直接获得结果,因为之前已经执行过一次了

4. 线程池

也可以使用线程池统一创建和管理线程,增加线程的复用性,减少系统开销。

// 自定义线程池
ExecutorService pool = new ThreadPoolExecutor( 3,
											   5,
											   2L,
											   TimeUnit.SECONDS,
											   new BolckingQueue(5),
											   Executor.defaultThreadFactory(),
											   new ThreadPoolExecutor().AbortPolicy());
5. 几种方式的区别

1)实际上Thread类继承了Runnable接口,但线程真正的创建是由Thread类的 native 本地 start() 方法实现的。调用start() 方法可以在创建一条与本地线程实体一一对应的Java线程,并执行其run() 方法。
2)后两者实现方式均需要通过Thread类调用start() 方法开启线程。可以理解为:实现接口创建任务,通过线程驱动任务。通常可以配合Lambda表达式简化线程创建过程。
3)Callable接口可以实现有返回值的多线程。需要配合FutureTask类进行使用。
4)通过线程池ExecutorService对线程进行统一分配和管理,这种方式也最为常见。

八、锁概述

1. 公平锁与非公平锁

1)公平锁:FIFO即线程根据先后顺序依次获取CPU时间片。当一个线程拥有公平锁时,其余需要获取该锁的线程必须依次等待。
2)非公平锁:线程之间可以相互争抢CPU时间片,没有先来后到之分。通常可以设置线程优先级来建议操作系统调用优先级更高的线程。(但是具体如何调用还得根据操作系统的具体实现。)
3)ReentrantLock()对象可以通过参数修改锁为公平锁,而Sync则不能。

// 设置公平锁
Lock lock = new ReentrantLock(true);

4)公平锁实现简单但效率低,非公平锁效率高,一般场景下很少使用公平锁。

2. 乐观锁与悲观锁

1)悲观锁:悲观并发策略总认为如果不采取同步措施、任务总会出现问题;因此无论共享数据的竞争是否发生,都会进行加锁操作。用于线程调度需要操作系统从用户态切换为核心态,频繁地切换、调度和唤醒线程会带来很大的开销。
2)乐观锁:基于冲突检验的乐观并发策略,即线程在访问共享资源时,乐观地认为不会有其他线程来争抢该资源。即先不上锁进行访问,如果操作成功,则直接返回结果;如果操作失败,表示有其他线程想要访问该共享资源,此时应该采取补救措施来保证线程安全。例如通过不断重试、自旋(CAS)操作避免进入阻塞态,减少系统线程切换开销。其中,保证冲突检测操作的原子性非常重要,但随着硬件的发展也得到了解决(CAS等指令)。

3. 可重入锁与不可重入锁

1)可重入锁:一个线程可以重复获取已拥有的锁,而不会发生死锁现象。这一功能主要是通过锁计数器来实现的,即获取一次锁,计数器+1,释放一次锁,计数器-1,直到计数器为0。Sync和ReentrantLock都属于可重入锁。
2)不可重入锁:一个线程不能重复获取已拥有的锁,必须释放该锁之后才能继续获取该锁,使用不可重入锁可能会引发死锁问题。

4. 共享锁与独占锁

1)共享锁:又称读锁(S锁),表示多个线程可以同时访问共享资源而不会出现线程安全问题。如果只有读操作,无论是单线程还是多线程环境下都是安全的。读读共享
2)排他锁:又称独占锁、写锁(X锁),一次只允许一个线程进行写操作。写写互斥、读写互斥
3)读写锁:ReadWriteLock 管理了读锁和写锁。读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock 适用于读多写少的并发情况。读写锁一般适用于读多写少的场景

	public interface ReadWriteLock {
		//返回读锁
	    Lock readLock();
	    //返回写锁
	    Lock writeLock();
	}
	ReadWriteLock rwl = new ReentrantReadWriteLock();
	// 获取读锁
	Lock readLock = rwl.readLock();
	readLock.lock();
	readLock.unlock();
	// 获取写锁
	Lock writeLock = rwl.writeLock();
	writeLock.lock();
	writeLock.unlock();

4)锁升级与锁降级

  • 锁升级是指读锁升级为写锁。
  • 锁降级是指写锁降级为读锁。
  • 在Java中只允许写锁降级为读锁,而不允许读锁升级为写锁。即写线程在写的过程中,可以降级为读线程与其他线程一起进行读操作。

[1] 深入理解读写锁—ReadWriteLock源码分析

5. 锁的4种状态

Java对象在内存中被划分为对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)三部分。对象头由运行时数据(Mark Word)和类型指针(数组对象还有数组长度)组成。其中Mark Word存放了对象的哈希码、分代年纪、锁状态标志等信息(长度为32 或 64比特)。

1)无锁:没有获取任何锁。
2)偏向锁:线程A获取锁时,会进入偏向模式,将标志位设置为01,偏向锁可以让该线程一直获取锁,可以消除同步,提高性能。当有其他线程B争抢该锁时,需要等待线程A释放。
3)轻量级锁:一旦存在竞争锁现象,线程A会撤销偏向锁,进入无锁状态,此时锁膨胀升级为轻量级锁,线程A与线程B需要互斥(交替)进入临界区。锁标志位设为00。
4)重量级锁:当多个线程需要同时进入临界区,此时锁升级为重量级锁(Sync和ReentrantLock)。

随着锁的竞争,锁的状态会从偏向锁到轻量级锁,再到重量级锁。而且锁的状态只有升级,没有降级。也就是只有偏向锁->轻量级锁->重量级锁,没有重量级锁->轻量级锁->偏向锁。
偏向锁可以提高带有同步但无竞争程序的性能,但它同样是一种带有效益权衡性质的优化,并不总是有利的,只在某些场景下适用。

[2] java中锁的四种状态

6. 锁优化技术

九 、JUC工具类

1. CountDownLatch

线程减法计数器:对于计数器归零之后再进行后面的操作。
应用场景:倒计时

	CountDownLatch cdl = new CountDownLatch(5);
	new Thread(() -> {cdl.countDown();}).start();
	// 等下计数器为0
	cdl.await();
	// 处理逻辑
2. CyclicBarrier

循环栅栏(循环加法计数器):计数器达到设置值再执行其他操作。
应用场景:达成条件获取奖励

	CyclicBarrier cb = new CyclicBarrier(7, ()->{});
	for(int i = 0; i < 7; i++) {
		new Thread(()->{cb.await();}).start();
	}
3. Semaphore

信号量机制:通过acquire() 方法获取资源、通过release() 方法释放资源。

  • acquire():获取资源,资源数-1,若semaphore小于0,则线程进入阻塞队列
    应用场景:对有限个共享资源进行分配与回收。
  • release():释放资源:资源数+1,若semaphore大于0,且存在某个等待线程,则将其唤醒。
Semaphore semaphore = new Semaphore(2);
// 获取资源
semaphore.acquire();
// 释放资源
semaphore.release();

十、Java内存模型JMM

《Java虚拟机规范》定义了一种“Java内存模型”试图来屏蔽不同硬件与操作系统的内存访问差异,以实现让Java程序在各种平台下都具有内存访问一致性。
在这里插入图片描述

1. 主内存与工作内存

主内存对应于Java堆中的对象实例部分。工作内存对应于Java虚拟机栈中的部分区域(局部变量表、操作数栈等) 更具体而言,主内存直接对应硬件的物理内存、而Java程序运行时主要访问其工作内存,因此工作内存会优先存储于高速缓冲区和寄存器中。
1)主内存:JMM规定所有的变量都存储在主内存。
2)工作内存:每条线程有各自的工作内存,存储了该线程使用的主内存中变量的副本。

  • 线程对变量的所有操作(读写赋值)都必须在工作内存中进行,而不能直接对主内存中的变量进行操作。
  • 不同线程间的不能直接访问对方工作内存中的变量,线程间变量值传递必须经过主内存来实现。
2. 内存间的交换操作

JMM定义了8种4对原子操作来实现内存间的交换操作。
1)lock 与 unlock :lock操作将一个变量标识为某个线程独占;unlock操作释放该独占变量。两个操作都作用于主内存中的变量。
2)read 与 load:read操作从主内存中读取变量;load操作将read读取的变量加载到工作内存中。read操作作用于主内存变量,load操作作用于工作内存变量。
3)use 与 assign:use操作将工作内存中的变量传递给执行引擎使用;assign操作把从执行引擎获取的变量值赋值给工作内存变量。两个操作都作用于工作内存变量。
4)store 与 write:save操作将工作内存变量的值传递到主内存。write操作将传递的值写入到主内存。前者作用于工作内存,后者作用于主内存。

操作含义作用域
lock对主内存变量加锁主内存
unlock释放独占变量主内存
read读取主内存变量主内存
load加载主内存变量到工作内存工作内存
use传递给执行引擎使用工作内存
assign接收执行引擎赋值工作内存
store保存工作内存变量到主内存工作内存
write写入主内存变量主内存
3. Volatile

1)Valotile关键字是JVM提供的最轻量级同步机制,它具有以下三个特点:

  • 可见性:被修饰的变量对所有线程可见。可见即是对该变量的进行的操作会立即更新到其他线程中。
  • 禁止指令重排:通过内存屏障(Memory Barrier)机制保证被修饰变量之后的指令不会重排到内存屏障之前。(双锁检测单例模式)
  • 不保证原子性:自增运算(i++)代码只有一句,但其字节码指令却不止一句:(1)getstatic:取出 i 放入操作数栈 ;(2)iconst_1:将 1 放入操作数栈; (3)执行iadd加指令。在并发操作下会出现线程安全问题。

2)Valotile如何保证可见性
JMM对Valotile变量定义了特殊规则来保证其可见性。被修饰的变量在进行read、load、use、assign、store、write操作需要遵守以下几点规则:

  • use操作执行的前一操作必须是load操作(read和load操作必须同时出现),所以此时的执行顺序为 read --> load --> use。即每次使用变量之前必须从主内存中进行更新。
  • assign操作执行的后一操作必须是store操作(store操作和write操作必须同时出现),所以此时的执行顺序为 assign --> store --> write。即每次对变量重新赋值之后必须同步回内存
  • 假定动作A是线程T对变量V实施的use或assign操作,动作P是相应的load(read)或store(write)操作;假定动作B是线程T对变量W实施的use或assign操作,动作Q是相应的load(read)或store(write)操作;如果A先于B,则P先于Q。这条规则保证了被Valotile修饰的变量不会被指令重排优化。
4. JMM特性

1)原子性 Atomicity:JMM提供的6种保证变量原子性的操作:read、load、use、assign、store、write。对基本数据类型的访问读写都是具备原子性的。(long、和double除外)。lock和unlock可以保证更大范围的原子性。
2)可见性 Visibility:指一个线程对某个变量进行了修改后能立即被其他线程感知。一般可以通过Valotile关键字来实现变量可见性。
3)有序性 Ordering:多线程情景下的指令重排优化会打乱代码的执行顺序,(如果代码直接不存在依赖关系)。一句话概括为:线程内所有操作有序(Within-Threa As-If-Serial Semantics)、线程间所有操作无序通过内存屏障、以及一些内置的先行发生原则可以保证部分有序性。

十一、补充

1)AQS
2)阻塞队列BlockingQueue

参考文章

[1] 深入理解读写锁—ReadWriteLock源码分析
[2] java中锁的四种状态
[3] 深入理解Java虚拟机(第3版) —— 周志明

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值