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

JUC并发编程

一、前言篇

一、进程和线程

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

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

二、并发和并行

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

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

三、同步和异步

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

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

多线程能否提升效率?

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

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

二、Java线程篇

线程创建的方式

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

Lambda表达式

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

/*
        传统写法:
        Runnable runnable = new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread()+"------>执行");
            }
        } ;
*/

/*alt + enter 自动替换为lambda表达式

        Runnable runnable = () -> {

        };

 */

Thread和Runnable的关系

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

、

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

查看进程、线程的方法

windows

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

Linux

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

Java

  • jps : 查看java的所有进程

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

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


    win+r 组合,输入jconsole

线程运行原理

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

    、

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

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

    、

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

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

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

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

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

线程的常见方法

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

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

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

interrupt : 打断线程休眠

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

join :等待线程结束

线程的优先级

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

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

sleep方法应用

为的是避免由于空转导致的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();

join方法详解

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

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

Interrupt方法详解

打断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();执行不会生效! 

设计模式—两阶段终止模式

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

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

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

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

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

守护线程

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

线程分为两类:

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

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

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

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

线程的状态

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

五种状态:

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

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

三、共享模型之管程

管程就是Monitor锁

线程安全问题产生的原因

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

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

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

情况一:


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

情况二:


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

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

临界区

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

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();
    }
}

竞态条件

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

synchronized解决方案

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、成员变量(实例变量、静态变量)

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

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

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

2、局部变量

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

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

常见的线程安全类

  • 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 他们的值是不能被修改的,实例即使被共享,也不会有线程安全的问题

Java对象头

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

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

、

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

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

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

Monitor锁

Monitor被翻译为监视器或管程

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

Monitor的工作原理 *

Monitor和obj的关联流程

详细的流程:

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

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

Synchronized优化原理 *

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

1、轻量级锁

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

轻量级锁对使用者是透明的,依然可以使用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的值恢复给对象头,

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

2、锁膨胀

当退出我们的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中的阻塞线程;执行重量级锁的加锁流程!

3、自旋优化

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

(多核CPU自旋才有意义)

  • 自旋成功:

  • 自旋失败:

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

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

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

4、偏向锁

当我们的轻量级锁重入时,效率较低,因为每次重入都需要执行一次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次,我们的对象上的偏向锁,变为不可偏向 ;

总结 :

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

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

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

5、锁消除

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

//测试代码 : 
    static int x  ;

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

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

Wait和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

Wait和Notify的使用

先了解一下Wait和Sleep的区别

Wait和Sleep的区别

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

总结 :

synchronized(lock){
    while(条件不成立){
    	lock.wait() ;
	}
    //满足条件,干活
}

synchronized(lock){
    lock.notifyAll() ;   //使用notify可能会产生虚假唤醒!
}

设计模式—保护性暂停模式

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

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

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

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

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

设计模式—生产者消费者模式

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

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

Park和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,加过油后,汽车发现有油了,汽车启动 ;

重新理解线程的状态 *

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

假设有线程 Thread t ;

情况一:NEW — > RUNNABLE

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

情况二: 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 ;

情况三:RUNNABLE <—> TIMED_WAITING

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

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

3、调用Thread.sleep(long time)

情况四:RUNNABLE <—> BLOCKED

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

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

情况五:RUNNABLE <—>TERMINTED

线程中的代码执行完毕 ;

死锁 *

死锁现象

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

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

定位死锁

方法一: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)]

方法二:jconsole

win + R : 输入jconsole

、

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

哲学家就餐问题

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

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

活锁

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

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

饥饿

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

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

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

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

答 : ReentrantLock

ReentrantLock *

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

相对synchronized 它具备以下特点 :

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

与synchronized一样支持锁重入

1、基本语法

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

2、可重入

当一个线程已经是锁的主人的时候,还可以再次去获取锁的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();
        }
    }
}

3、可打断

必须是加的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();

    }
}

4、锁超时

目的还是不让线程长时间陷入阻塞状态,如果等待一段时间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();
    }
} //获取不到锁

5、公平锁

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

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

6、条件变量

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. 使用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. 使用wait、notify 实现
  2. 使用park、unpark 实现
  3. 使用await、signal 实现

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

posted @ 2022-01-23 20:35  爪洼ing  阅读(53)  评论(0编辑  收藏  举报