博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
并发编程重点笔记
阅读量:4212 次
发布时间:2019-05-26

本文共 12151 字,大约阅读时间需要 40 分钟。

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

悲观锁和乐观锁

悲观锁:

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

乐观锁:

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

并发三大特性

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 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.

转载地址:http://xikmi.baihongyu.com/

你可能感兴趣的文章
高效SQL查询之索引(III)
查看>>
清空压缩数据表
查看>>
MSSQL添加字段说明
查看>>
SQL查表名、字段名、表说明、字段说明
查看>>
在SqlServer中用自定义函数返回动态表内容
查看>>
CONTAINS和FREETEXT
查看>>
深入探讨Truncate Table
查看>>
SQL Server 2008 下的备份和日志收缩
查看>>
SQL 中 Delete、Truncate、Drop 的异同
查看>>
数据仓库的粒度
查看>>
利用同义词简化SQL Server 2005开发
查看>>
查询处理的逻辑顺序
查看>>
SQL Server 2008中的数据压缩策略
查看>>
实现SQL Server 2008数据压缩
查看>>
Sql Server 2005 统计信息用途
查看>>
全文索引的用法
查看>>
grouping sets && grouping_id
查看>>
MS-SQLSERVER数据库SUSPECT状态如何解决
查看>>
视图更新
查看>>
创建数据库快照
查看>>