Offer来了(原理篇)笔记之第三章并发编程

 

Java多线程实现方式

  1. 继承Thread类,重写run方法,调用start方法
  2. 实现Runnable接口,重写run方法,调用start方法
  3. 实现Callable接口,调用FutureTask.get()
  4. 线程池:参数(初始化创建的连接数量initialSize,最大连接容量maxActive,保留最大空闲连接数量maxIdle,空闲时允许保留的最小连接数量minIdle,排队等候的超时时间(毫秒)maxWait)
 
start方法用于启动线程,真正实现了多线程运行。调用Thread类的start方法启动一个线程时,此线程处于就绪状态,并没有运行。
run方法也叫作线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态占用CPU。
 

使用线程池中线程对象的步骤

1. 创建线程池对象: public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。
2. 创建Runnable接口子类对象。(task)
3. 提交Runnable接口子类对象。(take task) : public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行
4. 关闭线程池(一般不做)。
 

Java线程的六种状态

线程的六种状态 New、Runnable、Blocked、Waiting、Timed Waiting、Terminated
 

线程的基本方法

线程相关的基本方法有wait、notify、notifyAll、sleep、join、yield等,这些方法控制线程的运行,并影响线程的状态变化。
1. 线程等待 wait方法
调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或被中断后才会返回。需要注意的是,在调用wait方法后会释放对象的锁,因此wait方法一般被用于同步方法或同步代码块中。
2 线程睡眠 sleep方法
调用sleep方法会导致当前线程休眠。与wait方法不同的是,sleep方法不会释放当前占有的锁,会导致线程进入TIMED-WATING状态,而wait方法会导致当前线程进入WATING状态。
3 线程让步:yield方法
调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞争CPU时间片。
4. 线程中断:interrupt方法
interrupt方法用于向线程发行一个终止通知信号,会影响该线程内部的一个中断标识位,这个线程本身并不会因为调用了interrupt方法而改变状态(阻塞、终止等)。状态的具体变化需要等待接收到中断标识的程序的最终处理结果来判定。
5 线程加入:join方法
join方法用于等待其他线程终止,如果在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待获取CPU的使用权。
6. 线程唤醒:notify方法
Object类有个notify方法,用于唤醒在此对象监视器上等待的一个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。
我们通常调用其中一个对象的wait方法在对象的监视器上等待,直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程
7. 后台守护线程:setDaemon方法
setDaemon方法用于定义一个守护线程,也叫作“服务线程”,
该线程是后台线程,有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
将一个用户线程设置为守护线程的方法是在线程对象创建之前用线程对象的setDaemon(true)来设置。
如垃圾回收线程就是一个经典的守护线程
 

终止线程的4种方式

1.正常运行结束:指线程体执行完成,线程自动结束。
2.使用退出标志退出线程:
类中声明公有volatile的一个boolean类型的标志,run方法中while循环判断该标志是否true决定程序是否退出
3.使用Interrupt方法终止线程:
(1)使用了sleep、调用锁的wait或者调用socket的receiver、accept等方法时,会使线程处于阻塞状态。在调用线程的interrupt方法时,会抛出InterruptException异常。我们通过代码捕获该异常,然后通过break跳出状态检测循环,可以有机会结束这个线程的执行。
(2)线程未处于阻塞状态。此时,使用isInterrupted方法判断线程的中断标志来退出循环。在调用interrupt方法时,中断标志会被设置为true,并不能立刻退出线程,而是执行线程终止前的资源释放操作,等待资源释放完毕后退出该线程。
4. 使用stop方法终止线程:
不安全,在程序中可以直接调用Thread.stop方法强行终止线程,导致该线程所持有的所有锁突然释放而使锁资源不可控制,被保护的数据就可能出现不一致的情况,不推荐采用
 

锁机制

Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或者方法之前加锁,这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放,直到其他线程执行完成并释放锁,该线程才有机会再次获取锁进行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改对象,从而保障数据的安全。
 

乐观锁

Java中的乐观锁大部分是通过CAS(Compare And Swap,比较和交换)操作实现的,CAS是一种原子更新操作。
具体过程为:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作,不执行更新操作,直接返回失败状态
 

悲观锁

Java中的悲观锁大部分基于AQS(Abstract Queued Synchronized,抽象的队列同步器)架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。
该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如RetreenLock)。
 

自旋锁

自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。
避免CPU资源因自旋被永久占用,线程设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。
优点:自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升
缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。系统存在复杂锁依赖的情况下不适合采用
 

synchronized

属于独占式的悲观锁,同时属于可重入锁,是非公平锁。
Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的。
◎ synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
◎ synchronized作用于静态方法时,锁住的是Class实例,因为静态方法属于Class而不属于对象。
◎ synchronized作用于一个代码块时,锁住的是所有代码块中配置的对象。
synchronized是JVM级别的。
JDK1.6对synchronized做了很多优化,引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫作锁膨胀。
 

ReentrantLock

ReentrantLock继承了Lock接口并实现了在接口中定义的方法,是一个可重入的独占锁。ReentrantLock通过自定义队列同步器(Abstract Queued Sychronized,AQS)来实现锁的获取与释放。
独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;
可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作,即允许连续两次获得同一把锁,两次释放同一把锁。
ReentrantLock避免死锁策略:响应中断、可轮询锁、定时锁
ReentrantLock支持公平锁和非公平锁两种方式。在构造函数ReentrantLock(boolean fair)中传递不同的参数来定义不同类型的锁,默认的实现是非公平锁。
公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。非公平锁指JVM遵循随机、就近原则分配锁的机制。
ReentrantLock是API级别的,使用ReentrantLock时必须在finally控制块中进行解锁操作。
 
 

原子操作类

原子操作同步类,使得同步操作(线程安全操作)更加方便、高效,它便是AtomicInteger、AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等,
 

读写锁ReadWriteLock

调用该类对象的readlock()方法获取读锁,writelock()方法获取写锁,读锁与写锁通过lock()和unlock()方法进行操作
 

CountDownLatch

CountDownLatch类位于java.util.concurrent包下,是一个同步工具类,允许一个或多个线程一直等待其他线程的操作执行完后再执行相关操作。CountDownLatch基于线程计数器来实现并发访问控制,主要用于主线程等待其他子线程都执行完毕后执行相关操作。
 

Semaphore

Semaphore是一种基于计数的信号量,
new Semaphore(5)创建计数5的对象,通过acquire方法和release方法来获取和释放许可信号资源。
使用基本与ReentrantLock一致
 

CyclicBarrier

CyclicBarrier(循环屏障)是一个同步工具,可以实现让一组线程等待至某个状态之后再全部同时执行。在所有等待线程都被释放之后,CyclicBarrier可以被重用。CyclicBarrier的运行状态叫作Barrier状态,在调用await方法后,线程就处于Barrier状态。
 

volatile变量

具备两种特性:
一种是保证该变量对所有线程可见性,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的;
一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
严格保障变量的单次读、写操作的原子性,但并不能保证像i++这种操作的原子性,因为i++在本质上是读、写两次操作。
 

CAS

CAS(V, E, N)包含3个参数,V表示要更新的变量,E表示预期的值,N表示新值。在且仅在V值等于 E值时,才会将V值设为 N,如果 V值和 E值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS返回当前V的真实值。
采用了乐观锁的思想,总是认为自己可以成功完成操作。是非阻塞算法的一种常见实现。
CAS算法的实现有一个重要的前提:需要取出内存中某时刻的数据,然后在下一时刻进行比较、替换,在这个时间差内可能数据已经发生了变化,导致产生ABA问题。在某些应用场景下可能出现过程数据不一致的问题。
部分乐观锁是通过版本号(version)来解决ABA问题的,具体的操作是乐观锁每次在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时就可以执行修改操作,并对版本号执行加1操作,否则执行失败。
 

AQS

AQS(Abstract Queued Synchronizer)是一个抽象的队列同步器,通过维护一个共享资源状态(Volatile Int State)和一个先进先出(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。
 

锁优化

1.减少锁持有的时间减少锁持有的时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。
2.减小锁粒度减小锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是ConcurrentHashMap中的分段锁。
3.锁分离锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据。
4.锁粗化锁粗化指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分得太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。
5.锁消除在开发中经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是因为程序编码不规范引起的。这时,我们需要检查并消除这些不必要的锁来提高系统的性能。

posted @ 2021-04-20 10:45  WindyZ  阅读(88)  评论(0编辑  收藏  举报