JUC并发编程笔记(上)多线程基础知识、Synchronized优化、ReentrantLock

1|0JUC并发编程

1|1一、前言篇

1|0一、进程和线程

进程:进程是资源分配的最小单位

线程:线程是CPU调度的最小单位,一个进程中可以包含多个线程

1|0二、并发和并行

并发:同一时间,应对多件事情的能力;例如:多个线程争抢一个CPU核心 ;

并线:同一时间,做多件事情的能力;例如:CPU有两个核心,同一时间,线程1和线程2分别用着2个核心!

1|0三、同步和异步

同步:需要等待结果的返回,同步运行

异步:不需要等待结果的返回,异步运行

多线程能否提升效率?

对于单核来说,并不会提升效率,这种情况开多个线程依旧是一个核心再运行,只不过是交替的执行!

对于多核来说,会有明显提升,我们开辟多个线程,多个核心分别执行这多个线程,结束时间由用时最长线程决定!

1|2二、Java线程篇

1|0线程创建的方式

  • Thread
  • Runnable [推荐]
  • Callable [Future Task 方式] ,可以有返回值!

1|0Lambda表达式

当我们的接口当中只有一个方法,那么我们实现这个接口可以用lambda表达式

/* 传统写法: Runnable runnable = new Runnable() { public void run() { System.out.println(Thread.currentThread()+"------>执行"); } } ; */ /*alt + enter 自动替换为lambda表达式 Runnable runnable = () -> { }; */

1|0Thread和Runnable的关系

当我们是用Runnable的方式创建线程的时候,我们的Thread的构造方法会先判断我们传入的Runnable接口的实现类是否为null,如果部位null,会调用其run方法!

、

我们的线程启动时执行run方法,如果Runnable对象不为null则调用Runnable对象的方法!【Future Task 实现了Runnable接口】

1|0查看进程、线程的方法

windows

  • 任务管理器可以查看进程和线程数、可以杀死线程!
  • tasklist 查看进程
  • taskkill 杀死进程

Linux

  • ps - ef | grep java : 查看java有关进程

Java

  • jps : 查看java的所有进程

  • jstack : 查看某个进程的详细信息(会列出线程信息)

  • jconsole: 查看某个java进程中的,线程信息(图形化界面),通过配置可以操作远程的Linux服务器上的进程


    win+r 组合,输入jconsole

1|0线程运行原理

  • 可以通过Debug的方式在Idea中查看基础的JVM中方法栈帧!

    、

  • 可以通过JVM内存模型,解释方法栈帧

    程序计数器:用来记录当前线程代码执行的位置,线程栈中私有!

    、

1|0线程的上下文切换(Thread Context Switch)

  • 当一个线程所分配的时间片用完后,就是一次上下文切换
  • 垃圾回收:垃圾回收的时候会暂停其他所有工作线程
  • 更高优先级线程需要运行
  • 线程自己调用了,sleep、yield、wait、join、park、synchronized、lock等方法

上下文切换的时候,每个线程中的程序计数器就会发起作用,记录当前前程执行到哪个位置(状态)!当再次执行当前线程的时候,按照程序计数器中保存的继续执行!

状态包括:虚拟栈中的每个栈帧信息,局部变量、操作数栈、返回地址等

由于需要记录状态,所以得出频繁的上下文切换会影响性能!

1|0线程的常见方法

start : 使得我们的线程变为就绪状态,具体的转为运行状态还要看CPU的调度器来决定

run:线程运行后执行的方法

sleep:当前线程休眠 (线程变为阻塞状态、Timed waiting) , TimeUnit方法,是封装后的,可读性更好

interrupt : 打断线程休眠

yield :线程让位,当前线程让出CPU的使用权,使得当前线程变为就绪状态(具体还要看操作系统的任务调度器,例如:没有其他线程)

join :等待线程结束

1|0线程的优先级

设置线程优先级知识一个提示的作用,类似yield,具体的时间片分配还是需要看操作系统的任务调度器!

默认:5、最大:10、最小:1 ;

1|0sleep方法应用

为的是避免由于空转导致的CPU利用率占用到100%,需要休眠

Thread t = new Thread(new Runnable() { public void run() { while (true){ //sleep的目的是防止空转 ,导致CPU利用率占用到100% try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start();

1|0join方法详解

等待线程运行结束(线程合并)

  • join()
  • join(long n) :设置一个最大等待时间n,如果时间超过n过去后仍然没有结束,则停止等待!

1|0Interrupt方法详解

打断sleep、wait、join(阻塞状态)线程

打断标记:

t.isInterrupt : 默认返回false,如果是正常的线程被打断返回true,如果阻塞(休眠的)线程被打断还是返回false ;

打断正常线程,仅仅只是把打断标记置为true,线程是否终止还得靠线程本身!

public class ThreatInterrupt { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true){ if (Thread.currentThread().isInterrupted()){ break; //打断标记为true,则结束线程! } } }); t.start(); Thread.sleep(1000); t.interrupt(); //t的线程打断标记置为true } }

扩展:interrupt还可以打断park线程

public class ThreatInterrupt { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { System.out.println("park线程执行.."); LockSupport.park(); //调用该park方法,执行到此处不会继续执行! System.out.println("park线程结束!"); }); t.start(); Thread.sleep(1000); t.interrupt(); //打断park造成的状态! } } //park线程执行.. 1秒后 park线程结束! //打断标记为true时,LockSupport.park();执行不会生效!

1|0设计模式—两阶段终止模式

在线程t1中,优雅的终止线程t2 ,优雅:指的是给t2,线程一个料理后事的机会! 不像t2.stop(过时)使得线程t2立刻终止!

也不像System.exit(int)方法,直接停止进程,线程2也同样停止

两阶段终止模式:分别是终止,正常阶段、阻塞阶段,不同的阶段打断的产生的结果不同!

流程:当执行interrupt方法时,如果线程中执行的时正常代码,会将打断标记置为true,我们可以根据打断标记处理,如果线程是处于sleep等阻塞状态时,会抛出Interrupt Exception,结束阻塞状态,此时打断标记为false ,我们需要通过捕捉异常,来重新设置打断标记为true,然后通过打断标记来继续判断!

可以写一个后台监控程序,测试如上步骤!

1|0守护线程

守护线程一般在默默运行,用户线程全部结束,守护线程也会自动结束!

线程分为两类:

  1. 用户线程
  2. 守护线程

创建一个线程,直接设置为守护线程即可

t.setDeamon(true) //讲当前t线程设置为守护线程!

例如:垃圾回收线程就是是一个守护线程!

1|0线程的状态

关于状态有两种说法,一种是说五种状态、一种是六种状态,分别说明!

五种状态:

**六种状态:**根据Thread. State枚举划分(更偏向Java)

  • NEW(初始):线程被创建后尚未启动。(new 出来线程,未start())
  • RUNNABLE(运行):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系资源,如等待CPU为它分配时间片。(在这种状态分类情况加,获取键盘的读入,被看作为可运行状态)
  • BLOCKED(阻塞):线程阻塞于锁。(等待t1线程结束,但是t1一直不结束)
  • WAITING(等待):线程需要等待其他线程做出一些特定动作(通知或中断)。(线程同步机制,等待另一个线程归还对象锁!)
  • TIME_WAITING(超时等待):该状态不同于WAITING,它可以在指定的时间内自行返回。(sleep、wait等状态,有时间限制的休眠)
  • TERMINATED(终止):该线程已经执行完毕。

1|3三、共享模型之管程

管程就是Monitor锁

1|0线程安全问题产生的原因

举个例子:线程t1、t2 对共享的静态变量i,分别执行i ++ , i -- 操作,由于并发会产生一下两种情况,导致线程静态变量 I不正确

  • I ++ 的原代码被字节码化后,代码分为4个步骤:获取 i、准备常量1、自增、写入自增后的值
  • I – 的原代码被字节码化后,代码分为4个步骤:获取 i、准备常量1、自减、写入自减后的值

因此当我们两个线程并发执行的时候,如果在写入值之前,发生上下文切换,(指令交错)则会导入如下两种情况:

情况一:


t1、t2并发执行,结果出现负数

情况二:


t1、t2并发执行,结果出现正数

结论:多个线程对某个共享资源进行读操作没问题,写操作则会产生线程安全的问题,

1|0临界区

线程中我们把对共享资源修改的区域,称这块代码块为临界区

public class ThreadTest { static int count = 0 ; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { //临界区 { count ++ ; } }); Thread t2 = new Thread(() -> { { //临界区 count -- ; } }); t1.start(); t2.start(); t1.join(); t2.join(); } }

1|0竞态条件

多个线程在临界区内执行,由于执行顺序不同,导致结果无法预测,称之为发生了竞态条件!

1|0synchronized解决方案

1、保证静态变量的安全:

public class ThreadTest { static int count = 0 ; public static void main(String[] args) throws InterruptedException { Object obj = new Object(); Thread t1 = new Thread(() -> { synchronized(obj){ count ++ ; } }); Thread t2 = new Thread(() -> { synchronized (obj){ count -- ; } }); t1.start(); t2.start(); t1.join(); t2.join(); } }

2、保证实例变量的安全:

public class ThreadTest { public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -> { room.a(); }) ; Thread t2 = new Thread(() -> { room.b(); }) ; Thread t3 = new Thread(() -> { room.c(); }) ; } } class Room{ public synchronized void a(){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("a.begin..."); } public synchronized void b(){ System.out.println("b.begin..."); } public void c(){ System.out.println("c.begin..."); } } //结果: /* * c b 1s a * c 1s a b * b c 1s a * * */

具体参考“线程八锁”

1|0变量的线程安全问题

1、成员变量(实例变量、静态变量)

  • 如果成员变量,没有在线程间去共享,那没是线程安全的!

  • 如果成员变量,在线程间被共享:

    • 如果只有读操作,安全
    • 如果有写操作,不安全

2、局部变量

局部变量并不会产生线程安全问题

每个线程中的栈是私有的!方法入栈后创建的局部变量,其他线程访问不到!

1|0常见的线程安全类

  • String
  • Integer
  • String Buffer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent(JUC) 包下的类

线程安全的类其中的方法是线程安全的!

但是线程安全的方法,组合起来也许会产生线程不安全的情况

例如:Hashtable

Hashtable table = new Hashtable( ) ; //原子性,只能在单一方法中保证 if (table.get("Key") == null) { table.put("key") ; }

我们的get、put方法都是被synchronized修饰的线程安全的方法,但是上述代码依然可能会产生线程不安全的情况

流程:线程 t1 get到key == null ,此时线程t1 执行get方法完毕,此时线程 t2 获得时间片执行 get 同样返回null,然后接着发生上下文切换,t1执行put方法,放入<k1,v1>然后经过上下文切换t2也回到put阶段,线程2中的的此时得到的状态仍然是key == null ,因此重复添加<k1,v2>,发生覆盖!

不可变的线程安全类

例如:String、Integer 他们的值是不能被修改的,实例即使被共享,也不会有线程安全的问题

1|0Java对象头

每一个对象在JVM当中都被分为对象头和内容:(如下是32位的JVM)

例如Integer : 对象头占8个字节,内容为int类型占4个字节 ;

、

Klass部分保存的是对象的类型

Mark Word 保存的是的如下:(由于对象是否加锁,而不同!)

  • Normal : 不加锁 01
  • Biased:偏向锁 01
  • Lightweight Locked : 轻量锁 00
  • Heavyweight : 重量级锁 10
  • Marked for GC :垃圾回收锁

1|0Monitor锁

Monitor被翻译为监视器或管程

  • Monitor的结构图,Monitor是重量级锁!

1|0Monitor的工作原理 *

Monitor和obj的关联流程

详细的流程:

  • 当我们的obj对象被加上了synchronized锁之后,我们obj的对象头就会转化为一个指针指向Monitor(关联起来) ;
  • 当我们的线程去访问临界区代码的时候,回将线程Thread1设置为Monitor的所有者!(获取对象锁)
  • 当新的线程2、线程3 去访问临界区代码时,则会判断obj的对象锁是否有主人,如果有那么就会在EntryList阻塞等待,等待线程1执行完临界区代码块的内容归还锁,接着线程1归还锁后, 线程2、线程3再去争夺对象锁的所有权!

总结 : 我们所说的对象锁,也即是这个Monitor ;

1|0Synchronized优化原理 *

由于Monitor锁每次都需要与操作系统打交道,效率较低,因此我们需要对锁进行优化!如轻量级锁、偏向锁

1|01、轻量级锁

如果多个线程访问一个一个对象,但是这个访问时间隔开的,不是并发(没有竞争关系),那么就可以使用轻量级锁来优化

轻量级锁对使用者是透明的,依然可以使用synchronized

//测试代码: static final Object obj = new Object(); public static void method1(){ synchronized(obj){ method2(); } } public static void method2(){ synchronized(obj){ } }

加锁(为对象添加轻量级锁)

  • Thread-0调用method1,方法入栈,执行synchronized代码块,然后创建一个锁的记录对象Lock Record,

  • 让锁记录中的reference指向我们的对象,并尝试用cas替换Object中的Mark word部分与锁地址交换

  • 如果cas(原子性操作)替换成功!对象头中保存了,lock record和状态00,表示该线程为对象加锁!

如果cas失败,有两种情况:

  • 如果其他线程已经持有了该Object的轻量级锁,这表明有竞争,进入锁膨胀过程!

  • 如果是当前线程自己执行了锁重入(重复加锁),那么再加入一条Lock Record锁记录作为重入的计数!

解锁(为对象接触,轻量级锁)

  • 当退出我们的synchronized代码块的时候,解锁时,如果有Lock Record锁记录为null,直接移除,表示锁重入记录-1

当退出我们的synchronized的时候,锁记录的值不为null,此时使用cas将Mark word的值恢复给对象头,

  • 成功:解锁成功!
  • 失败:说明轻量级锁进行了锁膨胀,或者已经升级为重量级锁,这时进入重量级解锁流程!

1|02、锁膨胀

当退出我们的synchronized的时候,此时使用cas将Mark word的值恢复给对象头失败 :说明轻量级锁进行了锁膨胀,或者已经升级为重量级锁,这时进入重量级解锁流程!

static final Object obj = new Object(); public static void method1(){ synchronized(obj){ method2(); } }
  • 当我们的的Thread-1为obj加轻量级锁发现,obj已经被Thread-01加上轻量级锁了

这是Thread-1加轻量级锁失败,进入锁膨胀流程

  • 即为Object对象申请Monitor锁,让Object指向重量级锁地址 ;
  • 然后自己进入重量级锁中的EntryList中,等待!

此时当Thread-0执行完同步代码块,使用cas将Mark word的值恢复给对象头失败,进入重量级锁的解锁流程,即按照Monitor的地址找到Monitor对象,然后将其Owner设置为null,解锁成功,然后唤醒EntryList中的阻塞线程;执行重量级锁的加锁流程!

1|03、自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

(多核CPU自旋才有意义)

  • 自旋成功:

  • 自旋失败:

  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。

  • Java 7之后不能控制是否开启自旋功能

1|04、偏向锁

当我们的轻量级锁重入时,效率较低,因为每次重入都需要执行一次cas操作,让Lock Record中的地址与Object对象中的Mark Work比较一次,适合:就一个线程访问

Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word头,之后发现这个线程D是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有

回顾一下64位JVM中的对象

创建对象时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword值为Ox05即最后3位为101,这时它日thread、epoch、age都为0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX : BiasedLockingStartupDelay=e来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword值为Ox01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode 时才会赋值
  • 前54位表示的偏向锁的ThreadID ;
  • 偏向锁解锁后,线程ID依然会在Mark word中,直到下一个
  • 在上面测试代码运行时在添加VM参数-xx:-UseBiasedLocking 禁用偏向锁
  • 当一个可偏向的对象调用其本身hashcode方法,会禁止偏向锁 【 因为我们加了偏向锁的对象没有空间去保存32位的hashcode】
  • 其他线程访问,也会撤销偏向锁 【对象加偏向锁就是只适用于这个对象只有某一个线程访问】

批量重偏向

当我们的对象上的偏向锁,被撤销20次后,不会再变为轻量级锁,而是以后的对象上的偏向锁,全部偏向一个新的线程 ;

超过40次,我们的对象上的偏向锁,变为不可偏向 ;

总结 :

重量级锁:适合多个线程访问,支持并发 ;缺点:与操作系统交互,效率变低

轻量级锁 : 适合多个线程访问,但是多个线程时错开访问的 ;缺点:一个线程多次为一个对象加锁,锁重入 ;

偏向锁:适合单一线程访问 ,目的是解决锁重入 ; 缺点:只能单一线程访问

1|05、锁消除

JVM存在一个JTL即时编译 ,由于这个机制会对代码优化,默认锁消除的这个优化机制是开启的,可以手动取消!

//测试代码 : static int x ; public static void method1(){ x ++ ; } public static void method2(){ Object o = new Object(); //局部对象,不被共享,这个锁相当于每加! JTL会直接将其优化掉! synchronized (o){ x ++ ; } }

结论:我们的两个方法执行效率是差不多一样的

1|0Wait和Notify *

原理:

  • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING 状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放锁时唤醒
  • WAITNG线程会在Owner线程调用notify或notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需入EntryList重新竞争

wait : 线程拿到锁的使用权,但是放弃了,进入waiting区 ;

notify : 唤醒正在waiting中的某个线程,到EntryList中准备与其他阻塞中的线程去争抢时间片!(随机挑一个唤醒)

notifyAll : 唤醒waiting中的所有正在等待唤醒的线程

Wait方法重载:

public final void wait() throws InterruptedException { //无限期等待 wait(0); } public final native void wait(long timeout) throws InterruptedException; //设置等待时长,一段时间后会自动唤醒 public final void wait(long timeout, int nanos) throws InterruptedException

1|0Wait和Notify的使用

先了解一下Wait和Sleep的区别

Wait和Sleep的区别

  • Sleep是Thread的方法,Wait是Object的方法 ;
  • Sleep可以在任何时候使用,Wait与Synchronized联用 ;
  • Sleep不会释放对象锁,wait会释放对象锁 ;

总结 :

synchronized(lock){ while(条件不成立){ lock.wait() ; } //满足条件,干活 } synchronized(lock){ lock.notifyAll() ; //使用notify可能会产生虚假唤醒! }

1|0设计模式—保护性暂停模式

一个结果需要从一个线程传到另外一个线程,让他们关联同一个GuardedObject (保护对象);

  • 如果结果不断地从一个线程到另外一个线程 ;那么可以使用消息队列 ;
  • JDK中join得实现、Future的实现都是这种模式 ;
  • 因为要等待另一方的结果,所有归结到同步模式

总结:在两个线程之间通过一个共享对象,线程1通过该共享对象保存当前线程中需要保存的结果 ;线程2则可以通过该对象获取到这个结果 ;

  • 保护性暂停模式:一个线程等待另外一个线程的返回结果 ;
  • join:一个线程等待另外一个线程执行完毕 ;

扩展:可以在获取结果的时候添加超时 , 一旦超时,即使没有获取的结果依然结束等待 ;

1|0设计模式—生产者消费者模式

异步的原因:生产者产生的结果不会被立刻调用 ;

  • 与前面的保护性暂停中的GuardedObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果
  • 数据消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK中各种阻塞队列,采用的就是这种模式

1|0Park和unpark

类似与wait和notify ,但是park休眠的可以使用unpark提前唤醒

public class ParkandUnpark { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1线程开始"); System.out.println("t1线程暂停"); LockSupport.park(); System.out.println("t1线程结束"); },"t1"); t1.start(); Thread.sleep(2000); System.out.println("t1线程恢复"); LockSupport.unpark(t1); } }

与Object的 wait & notify 相比

  • wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必
  • park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark可以先unpark,而wait & notify不能先notify

park和unpark分析图:

总结:我们每个线程都可以比作一个汽车,每个汽车都含有一个park对象,包括counter(油的量),condition(停车、不停车),mutex

当我们的线程执行park的时候,先判断油量是否充足,如果充足,汽车不会停车 ;如果不充足则停车休息 ;

当我们的unaprk执行的时候,就是给汽车加油(有没有油都加一下)也就是counter = 1,加过油后,汽车发现有油了,汽车启动 ;

1|0重新理解线程的状态 *

以偏java的6种线程状态理解 Thread. Sate

假设有线程 Thread t ;

1|0情况一:NEW — > RUNNABLE

当调用t .start方法的时候,由NEW 到RUNNABLE ;

1|0情况二: RUNNABLE <—> WAITING *

1、线程t 获得obj的锁后调用wait ,由RUNNABLE 到 WAITING ;

然后再调用notifyALL 、notify 、interrupt ,唤醒线程,放入EntryList中竞争:

  • 竞争成功:WAITING 到RUNNABLE ;
  • 竞争失败:WAITING 到BLOCK ;

2、当前线程main调用t.join ,则当前线程由RUNNABLE 到 WAITING ;

当t线程结束,或者调用当前线程的interrupt 会由RUNNABLE 到 WAITING ;

1|0情况三:RUNNABLE <—> TIMED_WAITING

1、调用wait(long time),比情况2的方法1多一个超时时间 ;

2、调用t.join(long time) , 比情况2的方法2多一个超时时间 ;

3、调用Thread.sleep(long time)

1|0情况四:RUNNABLE <—> BLOCKED

当线程执行到synchronized(obj)的时候,发现obj的owner已经有线程存在了,那么会由RUNNABLE 到BLOCKED ;

当占有obj锁的线程执行完毕后,当前线程争夺到CPU时间片就会再由BLOCKED 到 RUNNABLE

1|0情况五:RUNNABLE <—>TERMINTED

线程中的代码执行完毕 ;

1|0死锁 *

1|0死锁现象

一个线程需要获得多个对象锁 ,容易发生死锁现象

package com.juc; public class DeadLockTest { public static void main(String[] args) { final Object lock2 = new Object() ; final Object lock1 = new Object() ; new Thread(() -> { synchronized (lock1){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"获得lock1"); synchronized (lock2){ System.out.println(Thread.currentThread().getName()+"获得lock2"); } } },"t1").start(); new Thread(() -> { synchronized (lock2){ System.out.println(Thread.currentThread().getName()+"获得lock2"); synchronized (lock1){ System.out.println(Thread.currentThread().getName()+"获得lock1"); } } },"t2").start(); } } //测试结果: //t2获得lock2 //t1获得lock1 发生死锁现象

可以使用 : 顺序加锁的方式解决! 线程1 线程2获取锁的顺序保持一致!例如:都是现加lock1再加lock2

1|0定位死锁

1|0方法一:jps + jstack + 进程号
E:\JavaSE笔记>jps //查看java所有的进程 ; 11760 7028 Launcher 17832 Jps 18168 RemoteMavenServer 14508 DeadLockTest E:\JavaSE笔记>jstack 14508 查看某个进程中线程的详细信息;

打印出死锁的信息 :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wkLGc4uf-1633835829460)(JUC并发编程.assets/image-20211008173900282.png)]

1|0方法二:jconsole

win + R : 输入jconsole

、

以上两种方法,及时的定位到死锁的位置 ;

1|0哲学家就餐问题

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有5根筷子,每位哲学家左右手边各有一根筷子。
  • ·如果筷子被身边的人拿着,自己就得等待

1|0活锁

两个线程互相改变对方条件,都无法执行完成 !

package com.juc; public class LiveLockTest { static volatile int count = 10 ; public static void main(String[] args) { new Thread(() -> { while (count > 0){ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } count-- ; System.out.println("count= "+ count); } },"t1").start(); new Thread(() -> { while (count < 20){ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } count++ ; System.out.println("count= "+ count); } },"t2").start(); } } //两个线程互相改变对方条件,都无法执行完成 ! //测试结果 : count= 10 count= 10 count= 11 count= 11 count= 10

1|0饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束

饥饿的情况不易演示,讲读写锁时会涉及饥饿问题

例如:我们的哲学家问题中,我们如果顺序加锁去解决死锁,会发现其中一个哲学家一直拿不到筷子,这个现象就是饥饿现象

如何解决 饥饿、死锁、活锁 这些问题 ?

答 : ReentrantLock

1|0ReentrantLock *

可重入锁 , 位于java.util.concurrent 包下

相对synchronized 它具备以下特点 :

  • 可以中断 : 线程一拿到对象锁,线程二可以打断
  • 可以设置超时时间 : 线程阻塞一段时间仍未获取对象锁,放弃多锁的获取 ;
  • 可以设置为公平锁 : 线程先到先得,而非按优先级分配
  • 支持多个条件变量 : 可以有多个WaitSet ,不需要全部唤醒

与synchronized一样支持锁重入

1|01、基本语法

ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); //获取锁 try { //临界区 }finally { reentrantLock.unlock(); //释放锁 }

1|02、可重入

当一个线程已经是锁的主人的时候,还可以再次去获取锁的owner,重复获取!

package com.juc; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest { private static ReentrantLock reentrantLock = new ReentrantLock(); public static void main(String[] args) { reentrantLock.lock(); //当前线程获取到锁,称为lock的主人 ; try{ System.out.println("进入了main方法"); m1(); }finally { reentrantLock.unlock(); } } public static void m1(){ reentrantLock.lock(); //锁重入 try{ System.out.println("进入了m1方法"); m2(); }finally { reentrantLock.unlock(); } } public static void m2(){ reentrantLock.lock(); //锁重入 try{ System.out.println("进入了m1方法"); }finally { reentrantLock.unlock(); } } }

1|03、可打断

必须是加的lockInterruptibly()可被打断锁,其他线程使用Interrupt可以进行打断操作!

package com.juc; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest02 { private static ReentrantLock reentrantLock = new ReentrantLock() ; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { reentrantLock.lockInterruptibly(); //线程加入可打断锁! } catch (InterruptedException e) { e.printStackTrace(); System.out.println("t1线程被打断"); } try{ //临界区 System.out.println("t1线程没有打断"); }finally { reentrantLock.unlock(); } },"t1"); reentrantLock.lock(); Thread.sleep(1000); t1.interrupt(); } }

1|04、锁超时

目的还是不让线程长时间陷入阻塞状态,如果等待一段时间t1线程仍然不释放锁,t2则不再等待 ;

package com.juc; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest02 { private static ReentrantLock reentrantLock = new ReentrantLock() ; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { if (reentrantLock.tryLock()){ //t1线程尝试获取锁 try{ System.out.println("获取到锁"); }finally { reentrantLock.unlock(); } } System.out.println("获取不到锁"); },"t1"); reentrantLock.lock(); //主线程占用lock锁 t1.start(); } } //获取不到锁

1|05、公平锁

当我们多个线程竞争的时候,会按照先到先得的原则,而非再去争抢 ;

默认我们的ReentrantLock是不开启公平锁的,因为会降低并发 ;

1|06、条件变量

synchronized 中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待ReentrantLock的条件变量比synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized是那些不满足条件的线程都在一间休息室等消息
  • 而ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
public class ReentrantLockTest03 { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Condition condition01 = lock.newCondition(); //创建两个休息室,类似WaitSet Condition condition02 = lock.newCondition(); lock.lock(); condition01.await(); //让线程去condition01休息室休息 condition01.signal(); //唤醒condition01休息室中的线程 } }

ReentrantLock与synchronized的区别:

  • ReentrantLock 是给锁实例加锁!而synchronized则是以关键字的形式给所有对象加锁!
  • 可以使用ReentrantLock,解决死锁问题 ;
  • ReentrantLock 需要在finally当中手动释放锁 ,而synchronized则是代码块结束,释放锁 ;

1|0交替输出的实现 *

  1. 使用wait、notify 实现

  2. 使用park、unpark 实现

  3. 使用await、signal 实现

    }
    }
    System.out.println(“获取不到锁”);

    },"t1"); reentrantLock.lock(); //主线程占用lock锁 t1.start();

    }
    } //获取不到锁

#### **5、公平锁** > 当我们多个线程竞争的时候,会按照先到先得的原则,而非再去争抢 ; 默认我们的ReentrantLock是不开启公平锁的,因为会降低并发 ; #### 6、条件变量 synchronized 中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待ReentrantLock的条件变量比synchronized 强大之处在于,它是支持多个条件变量的,这就好比 - synchronized是那些不满足条件的线程都在一间休息室等消息 - 而ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒 ```java public class ReentrantLockTest03 { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Condition condition01 = lock.newCondition(); //创建两个休息室,类似WaitSet Condition condition02 = lock.newCondition(); lock.lock(); condition01.await(); //让线程去condition01休息室休息 condition01.signal(); //唤醒condition01休息室中的线程 } }

ReentrantLock与synchronized的区别:

  • ReentrantLock 是给锁实例加锁!而synchronized则是以关键字的形式给所有对象加锁!
  • 可以使用ReentrantLock,解决死锁问题 ;
  • ReentrantLock 需要在finally当中手动释放锁 ,而synchronized则是代码块结束,释放锁 ;

1|0交替输出的实现 *

  1. 使用wait、notify 实现
  2. 使用park、unpark 实现
  3. 使用await、signal 实现

上篇完结,主要讲述的的是线程安全,常用方法,锁优化,synchronized、ReentrantLock,CAS操作
下篇,讲述有序性!


__EOF__

本文作者宋淇祥
本文链接https://www.cnblogs.com/qxsong/p/15837293.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   爪洼ing  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示

喜欢请打赏

扫描二维码打赏

支付宝打赏