并发编程重点笔记

记录一些并发编程当中的注意点

悲观锁和乐观锁

悲观锁:

当一个线程必须拿到资源的锁,才进行进行相关操作,否则进入阻塞,直到其他线程释放资源锁;

乐观锁:

当一个线程进行操作时,不对资源进行加锁,它认为该对象在当前操作时,应该不会有其他线程来影响,所以多个线程都可以对该资源进行操作,对操作进行提交之前,会进行一次比较,把该资源和初始资源进行对比,如果资源内容一致,那么认为这个资源当前只有我操作了,那么就直接提交资源修改;否则进行重试或报错等;

并发三大特性

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拿到写锁,正在写数据...

可以看出来:线程01可以同时读,线程23的写只能一个个来

另外:如果设置公平锁=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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值