记录一些并发编程当中的注意点
悲观锁和乐观锁
悲观锁:
当一个线程必须拿到资源的锁,才进行进行相关操作,否则进入阻塞,直到其他线程释放资源锁;
乐观锁:
当一个线程进行操作时,不对资源进行加锁,它认为该对象在当前操作时,应该不会有其他线程来影响,所以多个线程都可以对该资源进行操作,对操作进行提交之前,会进行一次比较,把该资源和初始资源进行对比,如果资源内容一致,那么认为这个资源当前只有我操作了,那么就直接提交资源修改;否则进行重试或报错等;
并发三大特性
1)原子性:在cpu执行时间单位内,一个操作要么全部执行完成,不可被中断,中断会出现线程安全问题;
2)可见性:一个线程修改的内容能够被其他线程所读取,换句话说,如果一个对象是可见的,那么多线程之间修改对象,都是互相知晓的,保证对象在多个线程之间读取到最新的值;
3)有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序会影响到多线程并发执行代码的正确性;
synchronized
synchronized是jvm提供的关键字,他的作用是为了保证操作的原子性,也加加锁;
这个锁是一种悲观锁,同一个对象锁,同一时刻只能被一个线程锁占有,也叫独占锁,也叫可重入锁;
它实现原子性的原理是:利用底层的Monitor对象锁,加锁monitorEnter,释放锁monitorExit;
竞争激烈的情况下,使用它;
volatile
volatile关键字也是jvm提供的关键字,他的作用是为了保证多线程之间的可见性和一致性;
他不是锁,他是为了保证被volatile修饰的属性在多线程之间的访问安全性;一般用在判断true,false场景;他不具备有原子性,比如在循环i++场景,无法保证i最终得到正确的值;
Atomic*类
Atomic保证对象操作的原子性,如果一个变量被Atomic修饰,那么该变量在多线程操作之间的修改是安全的;它实现原子性的原理是:利用cas操作,是非阻塞的;竞争不激烈的情况下,使用它;
锁
锁是一种概念,为了保证多线程之间操作对象的安全性;在java中体现为接口Lock;
常用的锁比如:
private final ReentrantLock mainLock = new ReentrantLock();
ReentrantLock:
它是一种可重入锁,代表一个线程可以无限对同一个对象锁进行拿锁操作;
一般使用方式为:
定义一个final 全局对象:
private final ReentrantLock mainLock = new ReentrantLock();
然后再业务逻辑代码中:
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//doBusi
}finally {
mainLock.unlock();
}
interrupt
该方法为Thread的一个方法,用于设置调用线程的中断标识;意思就是:如果线程调用了该方法,那么该线程可能会中断,也可能不会中断,它仅仅只是设置线程中断的标识等于true,什么时候中断取决于程序本身;
在线程处于等待状态,比如调用了wait,join,sleep等方法之后,如果调用线程的interrupt()方法,那么会抛出一个线程中断异常:InterruptedException,可以通过捕获该异常来终止等待中的线程,也可以调用Thread.interrupted()方法清除中断标识,来忽略该中断请求;
shutdown
该方法为线程池的关闭操作;如果线程池调用了该方法,且阻塞队列不为空,那么线程池会在阻塞队列任务全部执行完成之后,才会关闭线程池;
shutdownNow
该方法一是线程池的关闭操作;如果线程池调用了该方法,会立即中断所有线程池内的线程执行,并且将未执行的任务放入一个list,作为返回,返回类型为任务Runnable
spring中事务四大特性(这里仅仅记忆一下和并发特性的区别)
1)原子性
2)一致性
3)隔离性
4)持久性
线程池静态创建方法的分类和风险
1)newFixedThreadPool:核心线程数等于最大线程数,当线程数已满,任务队列已满,不会继续创建新线程,直接执行拒绝策略;同时阻塞队列为LinkedBlockingQueue,为无限大小队列,风险是:如果阻塞队列过大,可能会造成程序OOM异常
2)newSingleThreadExecutor:线程池内永远只会有一个单线程在执行子任务,阻塞队列为LinkedBlockingQueue,为无限大小队列,风险是:如果阻塞队列过大,可能会造成程序OOM异常
3)newScheduledThreadPool和newSingleScheduledThreadPool:定时线程池,类似一个job定期执行任务,阻塞队列为:DelayedWorkQueue,风险是:如果阻塞队列过大,可能会造成程序OOM异常
4)newCachedThreadPool:缓存线程池,基于任务创建线程,也就是说来多少子任务,就新创建多少线程,如果线程没有被销毁,那么就复用现有线程;阻塞队列为:SynchronousQueue,无法存储任务;风险是:线程创建过大可能会将cpu资源耗尽,因为一个服务器能创建的最大线程数是有上限的;
公平锁和非公平锁
公平锁:
按照线程先来后到的原则,去依次按照顺序获取锁
非公平锁:
在一定条件下,新来的线程可以插队,不需要进入等待队列;
一定条件指的是:当上个线程执行完成,释放锁的一瞬间,刚好来了一个新的线程的拿锁请求,那么cpu会把锁优先分配给它,而不会给等待队列中时间最长的那个;
java默认的锁是非公平锁;
非公平锁的好处是:可以优先把锁分配给新来的线程,节省了去唤醒等待队列中的线程的开销;
而且,如果新线程如果执行很快结束释放了锁,相当于既完成了新线程的任务,同时又没有耽误等待队列正在唤醒的线程拿到锁;
缺点是:可能有的等待线程一直拿不到锁出现饥饿;
另外,即使设置了锁为公平锁,如果程序中调用的是tryLock(),那么tryLock可以插队,它实际上调用的还是Sync的非公平锁
读写锁
如果使用ReentrantLock,在只有读没有写的情况下,其实就会造成读性能的影响,因为多线程的读是没有线程安全问题的;
接口ReadWriteLock有个实现类叫ReentrantReadWriteLock,他有2个静态成员:1:读锁对象;2:写锁对象
它允许读锁被多个线程加锁,写锁它只能被一个线程拿锁,其他线程进入等待;
总结来看就是:读读可以同时拿锁,一个线程已经拿了读,另一个想要拿写,必须等上个读释放;同理,一个线程已经拿了写,另一个想要拿读,必须等上个写释放;
代码示例:
package com.test;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read(){
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+"拿到读锁,正在读取...");
Thread.sleep(600);
}catch (Exception e){
e.printStackTrace();
}finally {
readLock.unlock();
}
}
private static void write(){
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName()+"拿到写锁,正在写数据...");
Thread.sleep(600);
}catch (Exception e){
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(Test::read).start();
new Thread(Test::read).start();
new Thread(Test::write).start();
new Thread(Test::write).start();
}
}
运行结果:
Thread-0拿到读锁,正在读取...
Thread-1拿到读锁,正在读取...
Thread-2拿到写锁,正在写数据...
Thread-3拿到写锁,正在写数据...
可以看出来:线程0和1可以同时读,线程2和3的写只能一个个来
另外:如果设置公平锁=true,那么readLock和writeLock都会排队
如果设置公平锁=false,那么writeLock允许插队,但是readLock不允许插队,他它优先让等待队列的写锁拿锁执行,然后再让新来的线程拿读锁进行读取;
读写锁降级:读写锁允许从写锁降级成读锁,不允许从读锁升级为写锁,因为如果2个读都想要升级成写,那都需要互相等待释放对方的读,造成死锁问题;
jvm锁优化
jvm的锁升级过程为:
无锁->偏向锁->轻量级锁->重量级锁
偏向锁:开销很小,当对象被尝试拿锁时,会记录该对象信息,下次拿锁,直接上锁;
轻量级锁:当存在短时间竞争时,偏向锁升级成轻量级锁,利用了自旋和cas;
重量级锁:悲观锁,当锁被其他线程拥有时,当前线程进入阻塞;
CopyOnWriteArrayList
1:当发生写数据的时候,将当前数组复制出一份新的数组,数组大小为原有数组+1,并且将新元素添加到新数组当中,然后再将旧数组的指针指向新的数组;
2:迭代期间允许修改元素,不会报错,因为修改的是新数组
3:get不加锁,保证多线程访问高效性
缺点:
1:因为复制数组,所以多出了一部分内存的开销
2:数据可能会有可见性的问题
BlockingQueue
1.抛出异常
1)add:往队列中添加一个元素,如果队列已满,则抛出异常;
2)remove:删除并返回队列中的元素,如果队列为空,则抛出异常;
3)element:返回队列中的头结点但不删除,如果队列为空,则抛出异常;
2.不抛出异常,给出提示
1)offer:往队列插入一个元素,插入成功,返回true,如果队列已满,返回false;
2)poll:删除并返回队列中的元素,如果队列为空,则返回null(前提是你不能往队列中插入null元素),否则无法区分;
3)peek:返回队列中的头结点但不删除,如果队列为空,则返回null;
3.put和take
1)put:往队列中插入一个元素,如果队列已满,则线程进入阻塞;如果队列有了空闲,则会将元素添加到队列中;
2)take:获取并移除队列中的头结点,如果队列为空,也就是没有元素可以取出,那么线程进入阻塞,直到队列中有了新元素可以取出;
如何实现生产者和消费者模式
BlockingQueue实现
代码示例:
package com.test;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Test2 {
/***
* 使用BlockingQueue实现生产者和消费者
*/
public static void main(String[] args) {
//定义一个容量为10的阻塞队列
BlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(10);
//生产者
Runnable producer = ()->{
while (true){
try {
Object obj = new Object();
blockingQueue.put(obj);
System.out.println(Thread.currentThread().getName()+"生产了一个对象:"+obj);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(producer,"生产者1").start();
new Thread(producer,"生产者2").start();
//消费者
Runnable consumer = ()->{
while (true){
try {
Object obj = blockingQueue.take();
System.out.println(Thread.currentThread().getName()+"消费了一个对象:"+obj);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(consumer,"消费者1").start();
new Thread(consumer,"消费者2").start();
}
}
Condition实现
代码示例:
(基于Condition手动实现put和take)
package com.test;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class MyBlockingQueue {
private Queue queue;
private int max = 16;
//锁
private ReentrantLock reentrantLock = new ReentrantLock();
//队列非空
private Condition notEmpty = reentrantLock.newCondition();
//队列非满
private Condition notFull = reentrantLock.newCondition();
public MyBlockingQueue(int size) {
this.max = size;
this.queue = new LinkedList();
}
//生产者
public void put(Object o) throws InterruptedException {
reentrantLock.lock();
try{
//队列已满,则等待
while (queue.size() == max){
notFull.await();
}
//否则新增元素
queue.add(o);
//唤醒等待的消费者可以消费元素了
notFull.signalAll();
}finally {
reentrantLock.unlock();
}
}
//消费者
public Object take() throws InterruptedException {
reentrantLock.lock();
try{
//如果队列为空则等待,这里不能用if,以为如果2个线程同时消费,那么第一个消费释放锁,
//第二个再去remove,因为队列为空,则会抛出异常
while(queue.size()==0){
notEmpty.await();
}
//否则进行消费
Object o = queue.remove();
//唤醒等待的生产者可以放入元素了
return o;
}finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue(16);
new Thread(()-> {
try {
while (true){
Object o = new Object();
queue.put(o);
System.out.println(Thread.currentThread().getName()+"生产了一个对象"+o);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
while (true){
Object take = queue.take();
System.out.println(Thread.currentThread().getName()+"消费了一个对象"+take);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
CAS
在代码中体现为:
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
这段代码的意思是:将变量进行进行修改,修改提交之前进行比较变量的偏移量和当前的值做比较,如果是同一个值,说明没有被其他线程修改过,那么就提交修改,返回成功,如果返回失败了,怎么办呢?
1)根据实际业务,可以选择放弃本次操作或者认为进行业务重试
2)利用死循环进行自旋cas直到成功为止,代码中一般是这样的:
do {} while (! compareAndDecrementWorkerCount(ctl.get()));
注意:cas操作是不可中断的,它是乐观锁的一种是先,它是非阻塞的\
模拟cas执行过程:
package com.test;
public class DebugCas implements Runnable{
private volatile int value;
public synchronized int compareAndSwap(int expectedValue,int newValue){
int oldValue = value;
if(oldValue == expectedValue){
value = newValue;
System.out.println(Thread.currentThread().getName()+"更新了value值");
}
return oldValue;
}
@Override
public void run() {
compareAndSwap(100,150);
}
public static void main(String[] args) throws InterruptedException {
DebugCas r = new DebugCas();
r.value = 100;
Thread t1 = new Thread(r,"Thread1");
Thread t2 = new Thread(r,"Thread2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
AtomicLong和LongAdder
两个都是原子性操作类
1)AtomicLoing一般用在一般场景,用来保证cas操作;
2)LongAdder一般用在只用来求和和计数的场景,吞吐量较高,性能较高,但是占用内存也较高;
模拟一个死锁的例子
代码示例:
package com.test;
public class DealLockDemo {
private static Object o1 = new Object();
private static Object o2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (o1){
System.out.println(Thread.currentThread().getName()+"获取到了o1锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println(Thread.currentThread().getName()+"获取到了2把锁");
}
}
},"Thread-1");
Thread t2 = new Thread(() -> {
synchronized (o2){
System.out.println(Thread.currentThread().getName()+"获取到了o2锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println(Thread.currentThread().getName()+"获取到了2把锁");
}
}
},"Thread-2");
t1.start();
t2.start();
}
}
线程1拿到了o1,等待拿o2,
线程2拿到了o2,等待拿o1,
互相等待,不会释放,形成死锁;
查看jps找到死锁进程pid,然后jstack pid可以看到死锁日志:
Java stack information for the threads listed above:
===================================================
"Thread-2":
at com.test.DealLockDemo.lambda$main$1(DealLockDemo.java:32)
- waiting to lock <0x000000076b91acb8> (a java.lang.Object)
- locked <0x000000076b91acc8> (a java.lang.Object)
at com.test.DealLockDemo$$Lambda$2/122883338.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-1":
at com.test.DealLockDemo.lambda$main$0(DealLockDemo.java:18)
- waiting to lock <0x000000076b91acc8> (a java.lang.Object)
- locked <0x000000076b91acb8> (a java.lang.Object)
at com.test.DealLockDemo$$Lambda$1/1534030866.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.