并发编程的一些概念
同步和异步
同步:
同步方法必须等到方法调用返回后,才能继续后继的行为。也就是说,同步方法执行时,如果没有返回,则后面的方法是执行不到的。同步方法调用,调用过程中可能出现阻塞和等待。
比如说,java读取控制台输入就是同步方法。
异步:
异步方法调用后立即返回,可以立即执行后继的方法。异步方法的返回结果,采用通知的方式来告知调用者。异步方法调用,调用过程中不会出现阻塞和等待。
举个例子:js的ajax方法就默认是异步调用的,执行后马上返回,继续执行下面的代码。如果你想操作结果,就需要传递success方法。那么异步方法调用完成后,会通知你返回结果并调用success方法。也就是说异步方法不会马上得到结果的。
并发和并行
并发 :运行多个任务,不一定要同时,可以交替运行。
并行 :同时运行多个任务,互不干涉。
1.1 并发编程三要素
- 原子性
原子,是指一个操作是不可中断的。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 有序性
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序,必须保证指令重排序不会影响多个线程执行的结果)
- 可见性
当多个线程访问同一个共享变量时,如果其中一个线程对这个共享变量作了修改,其他线程能知道这个修改,并获取到最新的值。
1.2 并发级别
- 阻塞
- 无饥饿
- 无障碍
- 无锁
- 无等待
饥饿,阻塞和非阻塞
阻塞:阻塞是指有其他线程占用了临界区的资源,那么当前线程必须在这个临界区中等待,导致这个线程挂起。这个线程就是阻塞的。如果一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法执行。
非阻塞:是指没有一个线程会妨碍其他线程执行,线程会一直往下执行。
-
饥饿:是指一个或多个线程因为某种原因无法获取所需的资源,导致一直无法执行。
例如有些线程的优先级太低,导致一直被其他高优先级的线程抢占资源执行。(竞争优先级低,无法获取线程资源)
锁
锁:通过对代码块或方法加锁来阻止线程进入,只有获取到锁的线程才能执行代码。
1.1 对象锁,重入锁,显式锁,不可重入锁,读写锁,偏向锁,可中断锁
内置锁:synchronized关键字。
对象锁:锁和对象有关,而且每个对象都会有个隐形的监视器。synchronized是一种对象锁。
显式锁:Reentranctlock类
-
重入锁:线程可以多次获得已经由它自己持有的锁,称之为重入锁。
synchronized关键字和Reentranctlock类都是可重入锁。
不可重入锁:当前线程获得锁之后,此线程无法再次进入同步代码,称之为不可重入锁。
-
读写锁:读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
例如:ReentrantReadWriteLock,ReentrantReadWriteLock,分别为读锁与写锁。
自旋锁:自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
互斥锁:即两个线程获得锁是互斥的,一个线程获得了锁,其他线程在此线程释放锁之前是不能获得锁的,java提供的一个互斥锁为Reentrantlock。
-
可中断锁:顾名思义,就是可以相应中断的锁。
synchronized,不是可中断锁
而Reentranctlock是可中断锁(例如使用lockInterruptibly方法中断线程)。
1.2 悲观锁,乐观锁,自旋锁,互斥锁
-
悲观锁:对数据的一致性表示悲观。假定访问数据时,数据会被修改,所以会发生并发冲突,每次操作都会加锁。
例如独占锁,读写锁等。synchronized就是一种独占锁,也是是一种悲观锁。
-
乐观锁:对数据的一致性表示乐观。假定访问数据时,数据不会被修改,所以不会发生并发冲突,采用错误重试机制。每次操作申请锁失败后不会立刻挂起而是稍微等待再次尝试,如果因为发生冲突就重试,直到成功为止,以减少线程因为挂起、阻塞、唤醒(发生CPU的调度切换) 而造成的开销。
例如:偏向锁,自旋锁,CAS锁。ReentrantLock类就是一种CAS锁,也是一种乐观锁。
自旋锁:设定自旋等待的时间,等持有锁的线程释放锁后即可立即获取锁 。超时后停止自旋进入阻塞状态
偏向锁:偏向于第一个访问锁的线程 。一个线程访问触发偏向锁 ,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁
-
CAS锁:(比较与交换,Compare and swap) 是一种有名的无锁算法
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试
CAS有3个操作数,内存值V,旧的预期值A,新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则等待并继续尝试。
说明
- 悲观锁会导致阻塞,性能比较低
- 乐观锁不会阻塞线程,性能比较好
- 如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次
数不得小于 3 次。
1.3 公平锁和非公平锁
-
非公平锁:是指线程获取锁,采用抢占机制,线程随机获取锁的。非公平锁性能更好,通常线程锁都是非公平锁。
说明
- 例如synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
- 对于ReentrantLock和ReentrantReadWriteLock等,它默认情况下是非公平锁,
- 对于ReentrantLock和ReentrantReadWriteLock等,可以设置参数setFair(true)来使用公平锁.
-
公平锁:是指线程获得锁的顺序是按照线程加锁的顺序来分配的。即先来先得FIFI(先加锁的线程先执行)。
对于ReentrantLock和ReentrantReadWriteLock等,可以设置参数setFair(true)来使用公平锁.
1.5 死锁,活锁
-
死锁:指两个或两个以上的线程分别持有锁,并等待对方释放锁的现象。
进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。若无外力作用,死锁将永远得不到解开,比如2个线程互相持有对方的锁,互相等待对方释放锁。那么这2个线程都得不到释放锁,称之为死锁。(竞争时阻塞,相互等待,导致无法释放锁)
-
活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,有着最终解开锁的可能性。(竞争时谦让,一直重试,导致获取不到锁)
对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造
成死锁