初识并发编程(拓展)

1:死锁

1.1基础

        所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

        线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。

        当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

初识并发编程(拓展)_第1张图片

 

1.2:当然死锁的产生是必须要满足一些特定条件的:

​         互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放。

​         请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。

​         不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。

​         循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

1.3:预防死锁:

​         资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)。

​         只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)。

​         可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)。

​         资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)。

14:Demo说明

/**
 * 一个简单的死锁类
 * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
 * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
 * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
 * td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
 * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
 */

@Slf4j
public class DeadLock implements Runnable {
    public int flag = 1;
    //静态对象是类的所有对象共享的
    private static Object o1 = new Object(), o2 = new Object();

    @Override
    public void run() {
        log.info("flag:{}", flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    log.info("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    log.info("0");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
        //td2的run()可能在td1的run()之前运行
        new Thread(td1).start();
        new Thread(td2).start();
    }
}

初识并发编程(拓展)_第2张图片

 

解决方式  ,代码中对于加锁顺序 ,同时加锁o1;对第加锁用ReentrantLock :

@Override
public void run() {
    log.info("flag:{}", flag);
    if (flag == 1) {
        synchronized (o1) {
            try {
                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
            }
            synchronized (o2) {
                log.info("1");
            }
        }
    }
    if (flag == 0) {
        synchronized (o1) {
            try {
                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
            }
            synchronized (o2) {
                log.info("0");
            }
        }
    }
}

2:多线程 并发最佳实践

1.尽量使用本地变量

尽量使用本地变量,而不是创建一个类或实例的变量。

定义在函数内部的变量就是本地变量;

2.尽量使用不可变类

String、Integer等。不可变类可以降低代码中需要的同步数量;

3.最小化锁的作用域范围:S=1/(1-a+a/n)

        该公式称为:“阿姆达尔定律"或"安达尔定理”。 其中:a:并行计算部分所占比例;n:并行处理结点个数;S:加速比;

        当1-a等于0时,没有串行只有并行,最大加速比S=n; 当a=0时,只有串行没有并行,最小加速比S = 1; 当n→∞时,极限加速比 s→ 1/(1-a),即加速比的上限; 例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。

4.创建线程,尽量使用线程池的Executor,而不是直接new Thread执行

创建一个线程的代价是昂贵的,如果要创建一个可伸缩的Java应用,那么你需要使用线程池。

5.宁可使用同步也不要使用线程的wait和notify方法

从Java1.5以后,增加了许多同步工具,如:CountDownLatch、CyclicBarrier、Semaphore等,应该优先使用这些同步工具。

6.使用BlockingQueue实现生产-消费模式

阻塞队列不仅可以处理单个生产、单个消费,也可以处理多个生产和消费;

7.使用并发集合而不是加了锁的同步集合

        同步集合可以简单地理解为通过synchronized来实现同步的集合。如果有多个线程调用同步集合的方法,它们将会串行执行。 ​ 同步集合在单线程的环境下能够保证线程安全,但是通过synchronized同步方法将访问操作串行化,导致并发环境下效率低下。而且同步集合在多线程环境下的复合操作(迭代、条件运算如没有则添加等)是非线程安全,需要客户端代码来实现加锁。

        同步集合包括:arrayList和vector、stack;HashMap和Hashtable;Collections为集合提供各种方便操作的工具类;

Collections.synchronizedCollection(Collectiont)

Collections.synchronizedList(Listlist)

Collections.synchronizedMap(Mapmap)

Collections.synchronizedSet(Set t)

        并发集合 是jdk5.0重要的特性,增加了并发包java.util.concurrent.*。Java内存模型、volatile变量及AbstractQueuedSynchronizer(简称AQS同步器),是并发包众多实现的基础。

        ConcurrentHashMap:线程安全的HashMap的实现 ​

         CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList ​

        CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素 ​

        ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制 ​

        LinkedBlockingQueue:基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue;

8.使用Semaphone创建有界的访问

        为了建立稳定可靠的系统,对于数据库、文件系统和socket等资源必须要做有机的访问,Semaphone可以限制这些资源开销的选择,Semaphone可以以最低的代价阻塞线程等待,可以通过Semaphone来控制同时访问指定资源的线程数。

9.宁可使用同步代码块,也不实用同步的方法

        主要针对synchronized关键字。使用synchronized关键字同步代码块只会锁定一个对象,而不会将整个方法锁定。如果更改共同的变量或类的字段,首先应该选择的是原子型变量,然后使用volatile。如果需要互斥锁,可以考虑使用ReentrantLock。

10.避免使用静态变量

        静态变量在并发执行环境下会制造很多问题,如果必须使用静态变量,那么优先是它成为final变量,如果用来保存集合collection,那么可以考虑使用只读集合,否则一定要做特别多的同步处理和并发处理操作。

 

你可能感兴趣的:(高并发,基础技术,高并发)