爱了,这篇Java并发编程技术点总结的太详细了,建议是先收藏再观看

前言

并发编程技术在Java中属于重要知识点,对于以下内容你有了解多少?

进程、线程、协程关系概述

进程:本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位。

线程:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。

协程:又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程,一个线程可以有多个协程,线程与进程都是同步机制,而协程则是异步。Java的原生语法中并没有实现协程,目前python、Lua和GO等语言支持。

关系:一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程。

协程对于多线程的优缺点

优点:

  • 非常快速的上下文切换,不用系统内核的上下文切换,减小开销
  • 单线程即可实现高并发,单核CPU可以支持上万的协程
  • 由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁

缺点:

  • 协程无法利用多核资源,本质也是个单线程
  • 协程需要和进程配合才能运行在多CPU上
  • 目前Java没成熟的第三方库,存在风险
  • 调试debug存在难度,不利于发现问题

并发和并行的区别

并发 (concurrency):一台处理器上同时处理多个任务,这个同时实际上是交替处理多个任务,程序中可以同时拥有两个或者多个线程,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。

并行(parallellism) :多个CPU上同时处理多个任务,一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行。

并发指在一段时间内宏观上去处理多个任务。 并行指同一个时刻,多个任务确实真的同时运行。

Java里实现多线程的几种方式

1.继承Thread类

继承Thread类,重写里面run方法,创建实例,执行start方法。

优点:代码编写最简单直接操作
缺点:无返回值,继承一个类后,没法继承其他的类,拓展性差

public class ThreadDemo1 extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread实现多线程、名称:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
​
      ThreadDemo1 threadDemo1 = new ThreadDemo1();
      threadDemo1.setName("demo1");
      threadDemo1.start();
      System.out.println("主线程名称:"+Thread.currentThread().getName());
​
}

2.实现Runnable接口

自定义类实现Runnable接口,实现里面run方法,创建Thread类,使用Runnable接口的实现对象作为参数传递给Thread对象,调用start方法。

优点:线程类可以实现多个几接口,可以再继承一个类
缺点:无返回值,不能直接启动,需要通过构造一个Thread实例传递进去启动

public class ThreadDemo2 implements Runnable {
​
    @Override
    public void run() {
        System.out.println("通过Runnable实现多线程、名称:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        Thread thread = new Thread(threadDemo2);
        thread.setName("demo2");
        thread.start();
        System.out.println("主线程名称:"+Thread.currentThread().getName());
}

JDK8之后采用lambda表达式

public static void main(String[] args) {
    Thread thread = new Thread(()->{
                System.out.println("通过Runnable实现多线程、名称:"+Thread.currentThread().getName());
            });
    thread.setName("demo2");
    thread.start();
    System.out.println("主线程名称:"+Thread.currentThread().getName());
}

3.通过Callable和FutureTask方式

创建callable接口的实现类,并实现call方法,结合FutureTask类包装callable对象,实现多线程。

优点:有返回值,拓展性也高
缺点:jdk5以后才支持,需要重写call方法,结合多个类比如FutureTask和Thread类

public class MyTask implements Callable {
    @Override
    public Object call() throws Exception {
​
        System.out.println("通过Callable实现多线程、名称:"+Thread.currentThread().getName());
​
        return "这是返回值";
    }
}

 
 
 public static void main(String[] args) {
​
        MyTask myTask = new MyTask();
        FutureTask futureTask = new FutureTask<>(myTask);
​
        //FutureTask继承了Runnable,可以放在Thread中启动执行
        Thread thread = new Thread(futureTask);
        thread.setName("demo3");
        thread.start();
        System.out.println("主线程名称:"+Thread.currentThread().getName());
​
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            //阻塞等待中被中断则抛出
            e.printStackTrace();
        } catch (ExecutionException e) {
            //执行过程发送异常被抛出
            e.printStackTrace();
        }
    }

 
 

采用lambda表达式

 public static void main(String[] args) {
​
        FutureTask futureTask = new FutureTask<>(()->{
            System.out.println("通过Callable实现多线程、名称:"+Thread.currentThread().getName());
            return "这是返回值";
        });

        //FutureTask继承了Runnable,可以放在Thread中启动执行
        Thread thread = new Thread(futureTask);
        thread.setName("demo3");
        thread.start();
        System.out.println("主线程名称:"+Thread.currentThread().getName());
​
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            //阻塞等待中被中断则抛出
            e.printStackTrace();
        } catch (ExecutionException e) {
            //执行过程发送异常被抛出
            e.printStackTrace();
        }
​
​
    }

 
 
4.通过线程池创建线程

自定义Runnable接口,实现run方法,创建线程池,调用执行方法并传入对象。

优点:安全高性能,复用线程
缺点: jdk5后才支持,需要结合Runnable进行使用

public class ThreadDemo4 implements Runnable {
​
    @Override
    public void run() {
        System.out.println("通过线程池+runnable实现多线程,名称:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
​
        for(int i=0;i<10;i++){
            executorService.execute(new ThreadDemo4());
        }
​
        System.out.println("主线程名称:"+Thread.currentThread().getName());
​
        //关闭线程池
        executorService.shutdown();
}

  • 一般常用的Runnable 和 第四种线程池+Runnable,简单方便扩展,和高性能 (池化的思想)

Java线程常见的基本状态

JDK的线程状态分6种,JVM里面9种。

常见的5种状态

创建(NEW):生成线程对象,但是并没有调用该对象start()。

就绪(Runnable):当调用线程对象的start()方法,线程就进入就绪状态,但是此刻线程调度还没把该线程设置为当前线程,就是没获得CPU使用权。如果线程运行后,从等待或者睡眠中回来之后,也会进入就绪状态。

运行(Running):程序将处于就绪状态的线程设置为当前线程,即获得CPU使用权,这个时候线程进入运行状态,开始运行run里面的逻辑。

阻塞(Blocked)
等待阻塞:进入该状态的线程需要等待其他线程作出一定动作(通知或中断),这种状态的话CPU不会分配过来,他们需要被唤醒,可能也会无限等待下去。比如调用wait(状态就会变成WAITING状态),也可能通过调用sleep(状态就会变成TIMED_WAITING), join或者发出IO请求,阻塞结束后线程重新进入就绪状态。

同步阻塞:线程在获取synchronized同步锁失败,即锁被其他线程占用,它就会进入同步阻塞状态。

备注:相关资料会用细分下面的状态
    等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
    超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

死亡(TERMINATED):一个线程run方法执行结束,该线程就死亡了,不能进入就绪状态。

多线程开发常用方法

sleep
属于线程Thread的方法;
让线程暂缓执行,等待预计时间之后再恢复;
交出CPU使用权,不会释放锁;
进入阻塞状态TIME_WAITGING,睡眠结束变为就绪Runnable;

yield
属于线程Thread的方法;
暂停当前线程的对象,去执行其他线程;
交出CPU使用权,不会释放锁,和sleep类似;
作用:让相同优先级的线程轮流执行,但是不保证一定轮流;
注意:不会让线程进入阻塞状态,直接变为就绪Runnable,只需要重新获得CPU使用权;

join
属于线程Thread的方法;
在主线程上运行调用该方法,会让主线程休眠,不会释放已经持有的对象锁;
让调用join方法的线程先执行完毕,再执行其他线程;

wait
属于Object的方法;
当前线程调用对象的wait方法,会释放锁,进入线程的等待队列;
需要依靠notify或者notifyAll唤醒,或者wait(timeout)时间自动唤醒;

notify
属于Object的方法;
唤醒在对象监视器上等待的单个线程,选择是任意的;

notifyAll
属于Object的方法;
唤醒在对象监视器上等待的全部线程;

线程的状态转换图

Java中保证线程安全的方法

  • 加锁,比如synchronize/ReentrantLock
  • 使用volatile声明变量,轻量级同步,不能保证原子性
  • 使用线程安全类,原子类AtomicXXX,并发容器,同步CopyOnWriteArrayList/ConcurrentHashMap等
  • ThreadLocal本地私有变量/信号量Semaphore等

解析volatile关键字

volatile是轻量级的synchronized,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。为什么会出现脏读?JAVA内存模型简称JMM,JMM规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存就行操作,使用volatile修饰变量,每次读取前必须从主内存属性获取最新的值,每次写入需要立刻写到主内存中。volatile关键字修修饰的变量随时看到的自己的最新值,假如线程1对变量v进行修改,那么线程2是可以马上看见的。

volatile:保证可见性,但是不能保证原子性
synchronized:保证可见性,也保证原子性

​什么是指令重排

指令重排序分两类:编译器重排序和运行时重排序

JVM在编译Java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)

​举例:
int a = 3 //第一步 1
int b = 4 //第二步 2
int c =5 //第三步 3
int h = abc //第四步 4

定义顺序 1,2,3,4
计算顺序 1,3,2,4 和 2,1,3,4 结果都是一样

什么是happens-before以及为什么需要happens-before

happens-before:A happens-before B就是A先行发生于B(这种说法不是很准确),定义为hb(A, B)。在Java内存模型中,happens-before的意思是前一个操作的结果可以被后续操作获取。JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。

happens-before八大规则

1.程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
2.管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
3.volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
4.线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
5.线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
7.传递性规则:这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。
8.对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

并发编程三要素

原子性:一个不可再被分割的颗粒,原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题

int num = 1; // 原子操作
num++; // 非原子操作,从主内存读取num到线程工作内存,进行 +1,再把num写到主内存, 除非用原子类,即java.util.concurrent.atomic里的原子变量类

解决办法是可以用synchronized 或 Lock(比如ReentrantLock) 来把这个多步操作“变成”原子操作

public class Test {
    private int num = 0;

    //使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作
    Lock lock = new ReentrantLock();
    public  void add1(){
        lock.lock();
        try {
            num++;
        }finally {
            lock.unlock();
        }
    }

    //使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住
    public synchronized void add2(){
        num++;
    }
}

解决核心思想:把一个方法或者代码块看做一个整体,保证是一个不可分割的整体

有序性: 程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序

JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)

int a = 3 //第一步 1
int b = 4 //第二步 2
int c =5 //第三步 3 
int h = a*b*c //第四步 4

上面的例子 执行顺序1,2,3,4 和 2,1,3,4 结果都是一样,指令重排序可以提高执行效率,但是多线程上可能会影响结果

假如下面的场景,正常是顺序处理

//线程1
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
    run(); //核心业务代码
}

指令重排序后,导致顺序换了,程序出现问题,且难排查

//线程1
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
    run(); //核心业务代码
}
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法

可见性: 一个线程A对共享变量的修改,另一个线程B能够立刻看到
// 线程 A 执行
int num = 0;
// 线程 A 执行
num++;
// 线程 B 执行
System.out.print("num的值:" + num);

线程A执行 i++ 后再执行线程 B,线程 B可能有2个结果,可能是0和1。

因为 i++ 在线程A中执行运算,并没有立刻更新到主内存当中,而线程B就去主内存当中读取并打印,此时打印的就是0;也可能线程A执行完成更新到主内存了,线程B的值是1。所以需要保证线程的可见性,synchronized、lock和volatile能够保证线程可见性。

常见的进程间调度算法

先来先服务调度算法:
按照作业/进程到达的先后顺序进行调度 ,即:优先考虑在系统中等待时间最长的作业,排在长进程后的短进程的等待时间长,不利于短作业/进程

短作业优先调度算法:
短进程/作业(要求服务时间最短)在实际情况中占有很大比例,为了使得它们优先执行,对长作业不友好

高响应比优先调度算法:
在每次调度时,先计算各个作业的优先权:优先权=响应比=(等待时间+要求服务时间)/要求服务时间,因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应比=响应时间/要求服务时间,选择优先权高的进行服务需要计算优先权信息,增加了系统的开销

时间片轮转调度算法:
轮流的为各个进程服务,让每个进程在一定时间间隔内都可以得到响应,由于高频率的进程切换,会增加了开销,且不区分任务的紧急程度

优先级调度算法:
根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理,如果高优先级任务很多且持续产生,那低优先级的就可能很慢才被处理

常见的线程间调度算法

线程调度是指系统为线程分配CPU使用权的过程,主要分两种:

协同式线程调度(分时调度模式):线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。

抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会由一个线程导致整个进程阻塞。

​Java线程调度就是抢占式调度,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程。所以我们如果希望某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。

JAVA的线程的优先级,以1到10的整数指定。当多个线程可以运行时,JVM一般会运行最高优先级的线程(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)。在两线程同时处于就绪runnable状态时,优先级越高的线程越容易被系统选择执行,但是优先级并不是100%可以获得,只不过是机会更大而已。

Java多线程里面常用的锁

悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized
乐观锁:每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否回去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,比如CAS是乐观锁,但严格来说并不是锁,是通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁多。

公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说,一个线程组里,能保证每个线程都能拿到锁,比如ReentrantLock(底层是同步队列FIFO:First Input First Output来实现)
非公平锁:获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronized、ReentrantLock
小结:非公平锁性能高于公平锁,更能重复利用CPU的时间。

可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁
不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
小结:可重入锁能一定程度的避免死锁 ,synchronized、ReentrantLock 是重入锁。

自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁
小结:自旋锁不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU。常见的自旋锁:TicketLock,CLHLock,MSCLock。

共享锁:也叫S锁/读锁,能查看但无法修改和删除的一种数据锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享。

互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁/ 该锁每一次只能被一个线程所持有,加锁后任何试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得互斥锁的线程即能读数据又能修改数据。

死锁:两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去。

下面三种是JVM为了提高锁的获取与释放效率而做的优化,针对Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程。

偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,获取锁的代价更低。

轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点。

重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会一直循环下去,当自旋一定次数的时候且还没有获取到锁,就会进入阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低。

分段锁、行锁、表锁

编写多线程死锁的例子

死锁:线程在获得了锁A并且没有释放的情况下去申请锁B,这时另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

public class DeadLockDemo {
​
    private static String locka = "locka";
​
    private static String lockb = "lockb";
​
    public void methodA(){
​
        synchronized (locka){
            System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );
​
            //让出CPU执行权,不释放锁
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            synchronized(lockb){
                System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );
            }
        }

    }
​
    public void methodB(){
        synchronized (lockb){
            System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );
​
            //让出CPU执行权,不释放锁
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            synchronized(locka){
                System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );
            }
        }
​
    }
​
    public static void main(String [] args){
​
        System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());
​
        DeadLockDemo deadLockDemo = new DeadLockDemo();
​
        new Thread(()->{
            deadLockDemo.methodA();
        }).start();
​
        new Thread(()->{
            deadLockDemo.methodB();
        }).start();
​
        System.out.println("主线程运行结束:"+Thread.currentThread().getName());
​
    }
​
}

对于上面的例子如何解决死锁,常见的解决办法有两种:

  • 调整申请锁的范围
  • 调整申请锁的顺序
public class FixDeadLockDemo {
​
    private static String locka = "locka";
​
    private static String lockb = "lockb";
​
    public void methodA(){
​
        synchronized (locka){
            System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );
​
            //让出CPU执行权,不释放锁
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
        }
​
        synchronized(lockb){
            System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );
        }
    }
​
​
    public void methodB(){
        synchronized (lockb){
            System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );
​
            //让出CPU执行权,不释放锁
            try {
                Thread.sleep(2000);
​
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
        }
​
        synchronized(locka){
            System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );
        }
    }
​
​
    public static void main(String [] args){
​
        System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());
​
        FixDeadLockDemo deadLockDemo = new FixDeadLockDemo();
​
        for(int i=0; i<10;i++){
            new Thread(()->{
                deadLockDemo.methodA();
            }).start();
​
            new Thread(()->{
                deadLockDemo.methodB();
            }).start();
        }
​
        System.out.println("主线程运行结束:"+Thread.currentThread().getName());
​
    }
​
}

死锁的4个必要条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
  • 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
  • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • 环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。

设计一个简单的不可重入锁例子

不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在其他方法中尝试再次获取锁时,就会获取不到被阻塞。

    private void methodA(){
            //获取锁 TODO
        methodB();
    }
​
    private void methodB(){
            //获取锁 TODO
            //其他操作
    }

/**
 * 不可重入锁 简单例子
 *
 *  不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在其他方法中尝试再次获取锁时,就会获取不到被阻塞
 */
public class UnreentrantLock {
​
    private boolean isLocked = false;
​
    public synchronized void lock() throws InterruptedException {
​
        System.out.println("进入lock加锁 "+Thread.currentThread().getName());
​
        //判断是否已经被锁,如果被锁则当前请求的线程进行等待
        while (isLocked){
            System.out.println("进入wait等待 "+Thread.currentThread().getName());
            wait();
        }
        //进行加锁
        isLocked = true;
    }
    public synchronized void unlock(){
        System.out.println("进入unlock解锁 "+Thread.currentThread().getName());
        isLocked = false;
        //唤醒对象锁池里面的一个线程
        notify();
    }
}
​
​
​
public class Main {
    private UnreentrantLock unreentrantLock = new UnreentrantLock();
    //加锁建议在try里面,解锁建议在finally
    public void  methodA(){
        try {
            unreentrantLock.lock();
            System.out.println("methodA方法被调用");
            methodB();
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            unreentrantLock.unlock();
        }
    }
​
    public void methodB(){
        try {
            unreentrantLock.lock();
            System.out.println("methodB方法被调用");
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            unreentrantLock.unlock();
        }
    }
    public static void main(String [] args){
        //演示的是同个线程
        new Main().methodA();
    }
}
​
//同一个线程,重复获取锁失败,形成死锁,这个就是不可重入锁

设计一个简单的可重入锁例子

可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁

/**
 * 可重入锁 简单例子
 *
 *  可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁
 */
public class ReentrantLock {
​
    private boolean isLocked = false;
​
    //用于记录是不是重入的线程
    private Thread lockedOwner = null;
​
    //累计加锁次数,加锁一次累加1,解锁一次减少1
    private int lockedCount = 0;
​
    public synchronized void lock() throws InterruptedException {
​
        System.out.println("进入lock加锁 "+Thread.currentThread().getName());
​
        Thread thread = Thread.currentThread();
​
        //判断是否是同个线程获取锁, 引用地址的比较
        while (isLocked && lockedOwner != thread ){
            System.out.println("进入wait等待 "+Thread.currentThread().getName());
            System.out.println("当前锁状态 isLocked = "+isLocked);
            System.out.println("当前count数量 lockedCount =  "+lockedCount);
            wait();
        }
​
        //进行加锁
        isLocked = true;
        lockedOwner = thread;
        lockedCount++;
    }
    public synchronized void unlock(){
        System.out.println("进入unlock解锁 "+Thread.currentThread().getName());
​
        Thread thread = Thread.currentThread();
​
        //线程A加的锁,只能由线程A解锁,其他线程B不能解锁
        if(thread == this.lockedOwner){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                lockedOwner = null;
                //唤醒对象锁池里面的一个线程
                notify();
            }
        }
    }
}

public class Main {
    //private UnreentrantLock unreentrantLock = new UnreentrantLock();
    private ReentrantLock reentrantLock = new ReentrantLock();
​
    //加锁建议在try里面,解锁建议在finally
    public void  methodA(){
​
        try {
            reentrantLock.lock();
            System.out.println("methodA方法被调用");
            methodB();
​
        }catch (InterruptedException e){
            e.fillInStackTrace();
​
        } finally {
            reentrantLock.unlock();
        }
​
    }
​
    public void methodB(){
​
        try {
            reentrantLock.lock();
            System.out.println("methodB方法被调用");
​
        }catch (InterruptedException e){
            e.fillInStackTrace();
​
        } finally {
            reentrantLock.unlock();
        }
    }
​
    public static void main(String [] args){
        for(int i=0 ;i<10;i++){
            //演示的是同个线程
            new Main().methodA();
        }
    }
}

欢迎补充!!!

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

你可能感兴趣的:(爱了,这篇Java并发编程技术点总结的太详细了,建议是先收藏再观看)