JUC知识的学习

今天正式开始JUC得学习,

对于本次第一次接触JUC是使用得周阳老师得课程学习得,

纸上得来终觉浅,绝知此事要躬行。

https://www.bilibili.com/video/BV1vE411D7KE?from=search&seid=8977535818223646090

JUC

1、回顾

1、明确什么是进程,线程,并行,并发
	对我来说这个其实已经背烂了,记录一下最近看到的一个例子帮助理解,你打开一个qq.exe,这就是一个进程,但是qq打开后你能发消息,能看空间,同时上面还飘着天气信息,这都是同时及进行着的,这就是一个个线程在工作,他们共享进程的资源,但是计算机操作系统分配资源是以进程为单位的,线程共享资源,但是也保留自己的一些资源,比如计时器,堆栈信息之类的。

我在此处先操作一下以前的卖票程序,看一下多线程在企业实战中的应用。
package com.lwq.juc.p39;

/**
 * @description:juc开始于复习买票的功能实现
 * @projectName:juclearn
 * @see:com.lwq.juc.p39
 * @author:LWQ
 * @createTime:2020-12-19 14:07
 * @version:1.0
 */
/**
 * 多线程这里有一个框架的概念就是资源类
 *
 * **/
class Ticket{
    //总票数
    private int number = 20;
    //资源类暴露在外面的方法
    //synchronized会降低性能,因为一旦加锁,同一时间就只有一个线程才能进来,势必会降低性能,因为它加在哪里就锁住所有,这会导致性能的降低
    public synchronized void saleTicket(){
        if(number > 0){
            System.out.println(Thread.currentThread().getName() + "卖出第" + number-- + "张票还剩" + number + "张票。" );
        }
    }
}

/**
 题目:三个售票员     卖出   30张票
 多线程在企业实战中的一个套路模板是
 1、在高内聚低耦合的情况下,分为三块
 线程                    操作(对外暴露调用方法)         资源类
 我写一下我的理解:
 线程这块就是:线程是最后对业务的调用,最后就是在这里实现一个多线程的处理(new Thread(new Runnable() )
 而线程处理调用的就是资源类,资源类是最终对业务的实现,(Ticket)
 操作就是资源类暴露出来的操作接口(saleTicket()方法)

 我解释一下为什么这样做,这样做法最后会实现我们一直追求的高内聚和低耦合,我举个例子,
 线程就是人,资源就是空调,操作就是遥控器。实现的功能是空调自己带的,不是人的,谁都能通过遥控去开空调,这就解开了人和空调资源的耦合
 实现都在空调内部自己实现功能,这就是内聚,空调的功能都是自己实现,不依赖别人,这就是自己的内聚,完成最终的高内聚。oop原则。
 **/
public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    ticket.saleTicket();
                }
            }
        }, "售票员A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    ticket.saleTicket();
                }
            }
        }, "售票员B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    ticket.saleTicket();
                }
            }
        }, "售票员C").start();
    }
}

2、锁

上面看到了synchronized锁对于程序的影响,其实就是加上synchronized的程序代码块被上锁,然后同一时间不允许被多个线程同时访问,但是我们很清楚,有时候在程序中可能只有那么几行代码需要加锁进行同步,此时再使用synchronized就有点浪费性能了,所以jdk提供了其他可扩展的更加细粒度的锁帮助我们控制这种同步操作的行为。

在这里插入图片描述

2.1、ReentrantLock(可重入锁)

JDK官方API上的介绍:
public class ReentrantLock
extends Object
implements Lock, Serializable
一个可重入的互斥 Lock具有相同的基本行为和语义为隐式监控锁使用 synchronized方法和报表访问,但扩展功能。 
一个ReentrantLock是由线程最后成功锁定,但尚未解锁它。一个线程调用lock将返回,成功获取锁,当锁不是由另一个线程拥有。如果当前线程已经拥有锁,该方法将立即返回。这可以使用方法isHeldByCurrentThread()检查,并getHoldCount()。

此类的构造函数接受一个可选的公平性参数。当设置true,争,锁青睐授予访问最长等待线程。否则,此锁不保证任何特定的访问顺序。使用许多线程访问的公平锁的程序可能会显示较低的整体吞吐量(即,比那些使用默认设置慢,往往要慢得多),但有较小的差异,在时间获得锁和保证缺乏饥饿。请注意,锁的公平性并不能保证线程调度的公平性。因此,使用一个公平锁的许多线程之一可能会获得它的连续多次,而其他活动线程不进展,而不是目前持有的锁。还要注意,不定时的tryLock()方法不尊重公平设置。如果锁是可用的,即使其他线程正在等待,它也会成功的。

这是推荐的做法总是紧跟一个电话lock与try块,最典型的是在前/后建设等:


 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

使用可重入锁以及lambda表达式对卖票程序做修改

package com.lwq.juc.p39;

/**
 * @description:juc开始于复习买票的功能实现
 * @projectName:juclearn
 * @see:com.lwq.juc.p39
 * @author:LWQ
 * @createTime:2020-12-19 14:07
 * @version:1.0
 */

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 多线程这里有一个框架的概念就是资源类
 *
 * **/
class Ticket{
    //总票数
    private int number = 20;
    //资源类暴露在外面的方法

    //多态操作可重入锁
    private  Lock lock = new ReentrantLock();

    //synchronized会降低性能,因为一旦加锁,同一时间就只有一个线程才能进来,势必会降低性能,因为它加在哪里就锁住所有,这会导致性能的降低
    public  void saleTicket(){
        lock.lock();//在指定代码块前面加锁
        try{
            if(number > 0){
                System.out.println(Thread.currentThread().getName() + "卖出第" + number-- + "张票还剩" + number + "张票。" );
            }
        }finally {
            lock.unlock();//释放锁
        }
    }
}

/**
 题目:三个售票员     卖出   30张票
 多线程在企业实战中的一个套路模板是
 1、在高内聚低耦合的情况下,分为三块
 线程                    操作(对外暴露调用方法)         资源类(不需要实现什么runnable接口,完全就是一个实现功能,其他的一概不沾,不和主线程挂钩,拿到彼得
 地方照样用,因为贼独立,空调和人没有任何关系,在遥控器没有的时候)
 我写一下我的理解:
 线程这块就是:线程是最后对业务的调用,最后就是在这里实现一个多线程的处理(new Thread(new Runnable() )
 而线程处理调用的就是资源类,资源类是最终对业务的实现,(Ticket)
 操作就是资源类暴露出来的操作接口(saleTicket()方法)

 我解释一下为什么这样做,这样做法最后会实现我们一直追求的高内聚和低耦合,我举个例子,
 线程就是人,资源就是空调,操作就是遥控器。实现的功能是空调自己带的,不是人的,谁都能通过遥控去开空调,这就解开了人和空调资源的耦合
 实现都在空调内部自己实现功能,这就是内聚,空调的功能都是自己实现,不依赖别人,这就是自己的内聚,完成最终的高内聚。oop原则。
 **/
public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(() ->{for (int i = 0; i < 30; i++) {ticket.saleTicket();}},"售票员A").start();
        new Thread(() ->{for (int i = 0; i < 30; i++) {ticket.saleTicket();}},"售票员B").start();
        new Thread(() ->{for (int i = 0; i < 30; i++) {ticket.saleTicket();}},"售票员C").start();
    }
}

2.2、补充一些关于lambda函数编程的知识

package com.lwq.juc.p41;

/**
 * lambda的公式总结:拷贝小括号,写死右箭头,落地大括号
 * 这句话的意思是,你要new一个接口声明,比如public int add(int x,int y);
 * 1、先拷贝小括号(int x,int y)
 * 2、写死右箭头 (int x,int y) ->
 * 3、落地大括号是说你最后的落地实现就是在大括号里的,就没有new了{
 *     接口实现的内容。
 * }
 * 最后就是Foo foo = () -> {
 *    System.out.println("************************lambda实现方式的new出的接口方法。。。");
 * };
 * @description:lambda语法的学习
 * @projectName:juclearn
 * @see:com.lwq.juc.p41
 * @author:LWQ
 * @createTime:2020-12-21 13:59
 * @version:1.0
 */

/*
 @FunctionalInterface是函数式的编程接口注解,加上这个以后就只能声明一个接口声明在里面了,不然就会报错
 但是里面可以有default声明的方法,可以有实现内容,这是jdk1.8之后加上去的,提高竞争力的。
 也可以有static的声明方法,因为static修饰的是属于类的,
 **/
@FunctionalInterface
interface Foo{
    //public void sayHello();
    public int add(int x,int y);

    default int sub(int x,int y){
        System.out.println("=====================================default");
        return x / y;
    }

    public static int mav(int x,int y){
        System.out.println("=====================================static");
        return  x*y;
    }
}



public class LambdaExpressDemo {
    public static void main(String[] args) {

         /*Foo foo = new Foo() {
            @Override
            public void sayHello() {
                System.out.println("************************匿名内部类的new出的接口方法。。。");
            }
        };
        foo.sayHello();*/

        /*Foo foo = () -> {
            System.out.println("************************lambda实现方式的new出的接口方法。。。");
        };
        foo.sayHello();*/

        /*Foo foo = (int x,int y)->{
            System.out.println("===================================" + (x+y));
            return x + y;
        };
        foo.add(1,3);*/

        Foo foo = (int x,int y)->{
            System.out.println("===================================" + (x+y));
            return x + y;
        };
        System.out.println(foo.sub(12, 3));

        System.out.println(Foo.mav(12, 12));
    }
}

2.3、生产者消费者初步实现

package com.lwq.juc.p42;

/**
 * @description:生产者消费者算法初步实现,线程之间做互相通信操作
 * @projectName:juclearn
 * @see:com.lwq.juc.p42
 * @author:LWQ
 * @createTime:2020-12-21 16:18
 * @version:1.0
 */


/**
 * 需求,一个空调机器,A,B两个人,初始温度是0,A按+1度,B按-1度 ,按个十轮,最后温度是0
 * 分析:我们看到这个需求,空调是个资源类,拥有加一和减一两个功能,暴露出来给两个人(也就是两个线程)去调用
 */

/**
 * 线程之间的通信,同步互斥基本就是以下框架
 * 判断               干活             (干完)通知
 */

class Airconditioner{
    private int number = 0;
    //加一度操作,synchronized是加锁,保持同步,它拿到锁后其他线程无法操作number。对于number的操作就进入等待池中
    public synchronized void increment() throws InterruptedException {

        if(number != 0){
            //线程阻塞,放弃锁
            this.wait();
        }
        //干活,实现操作
        number ++;

        //打印信息
        System.out.println(Thread.currentThread().getName() + ":" + number);

        //唤醒等待池中的其他所有线程
        this.notifyAll();
    }

    //减一度操作
    public synchronized void decrement() throws InterruptedException {
        if(number == 0){
            this.wait();
        }
        number --;
        System.out.println(Thread.currentThread().getName() + ":" + number);
        this.notifyAll();
    }

}

public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {
        Airconditioner airconditioner = new Airconditioner();
        new Thread(()->{//用户线程只负责调用,其他的一律不交互
            for (int i = 0; i < 10; i++) {
                try {
                    airconditioner.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    airconditioner.increment();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();
    }
}

运行结果:
B:1
A:0
B:1
A:0
B:1
A:0
B:1
A:0
B:1
A:0
B:1
A:0
B:1
A:0
B:1
A:0
B:1
A:0
B:1
A:0
明显的看到就是一个加一度,一个减一度,很正常的运行结果。
因为这个地方我们在生产者和消费者的两个地方用了synchronized锁,每次当一方进入方法的时候就占有了锁,也就占有了number这个变量,对方无法操作,直到操作结束才唤醒对方线程,使之具有重新竞争cpu资源的能力。所以就会交替出现,不会混乱。

2.4、线程的虚假唤醒

此时我们改变需求,增加消费者线程到两个,增加生产者线程到两个,看看会出现什么状况。

//修改代码:
package com.lwq.juc.p43;

/**
 * @description:生产者消费者算法初步实现,线程之间做互相通信操作
 * @projectName:juclearn
 * @see:com.lwq.juc.p42
 * @author:LWQ
 * @createTime:2020-12-21 16:18
 * @version:1.0
 */


/**
 * 需求,一个空调机器,A,B两个人,初始温度是0,A按+1度,B按-1度 ,按个十轮,最后温度是0
 * 分析:我们看到这个需求,空调是个资源类,拥有加一和减一两个功能,暴露出来给两个人(也就是两个线程)去调用
 */

/**
 * 线程之间的通信,同步互斥基本就是以下框架
 * 判断               干活             (干完)通知
 */

class Airconditioner{
    private int number = 0;
    //加一度操作,synchronized是加锁,保持同步,它拿到锁后其他线程无法操作number。对于number的操作就进入等待池中
    public synchronized void increment() throws InterruptedException {
        if(number != 0){
            //线程阻塞,放弃锁
            this.wait();
        }
        //干活,实现操作
        number ++;
        //打印信息
        System.out.println(Thread.currentThread().getName() + ":" + number);
        //唤醒等待池中的其他所有线程
        this.notifyAll();
    }

    //减一度操作
    public synchronized void decrement() throws InterruptedException {
        if(number == 0){
            //一个减法A到这里时间片到了,交给另一个减法B,B进来做了一次减法,后面A回来了,执行等待,但是A总会往下走的,因为if只判断一次,
            //A判断完就出去了,再减一就出现问题了,但是如果是while就不会,A在回来之后等待,完了还会拿到number执行一次判断,直到可以了
            this.wait();
        }
        //一个减法都到这了
        number --;
        System.out.println(Thread.currentThread().getName() + ":" + number);
        this.notifyAll();
    }

}

public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {
        Airconditioner airconditioner = new Airconditioner();
        new Thread(()->{//用户线程只负责调用,其他的一律不交互
            for (int i = 0; i < 10; i++) {
                try {
                    airconditioner.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    airconditioner.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();

        new Thread(()->{//用户线程只负责调用,其他的一律不交互
            for (int i = 0; i < 10; i++) {
                try {
                    airconditioner.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();//增加线程

        new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    airconditioner.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"D").start();//增加线程
    }
}

**运行结果**
......
B:0
C:1
B:0
A:1
C:2
A:3
C:4
A:5
D:4
D:3
D:2
D:1
D:0
C:1
......
	我们看到这个结果是超出我们预期要求的,出现了2345这种结果,意味着没有按照顺序去增减,有的加多了几次。
这就是多线程调用的线程的虚假唤醒。多线程问题是很诡异的,因为它的最终调度是在操作系统cpu内部完成的,不是由人为控制的,由于这种人为不可控性,所以多线程问题实际上需要你多理解,那我们就先来自己理解一下这个问题,不做深度的专业的研究,就单纯的从这个现象来理解一下,再进入下面专门的学习。
    我们之前正确的结果上面说过了,是因为加锁限制了每次只能有一个进入,进入发现对方占用就等待,操作完成唤醒对方完成操作,所以实现了一个接一个这种操作,同步操作得以完成。加法进去外面就是减法,减法进去外面就是加法,因为上了锁,一时间就只能有一个操作,所以不会乱,只有一个生产者和一个消费者的时候。
    现在多了一个加一度的线程(我们就以加一度生产者这个来举例子),在if判断里面执行到

解决虚假唤醒:在官方的jdk给出的api中提出,需要使用while循环做条件判断。

while(!(ok to proceed)){
	condition.await();
}
举个例子,一个生产者消费者模型的任务队列,一个生产者一次可能放入多个任务,然后用notifyAll通知消费者,但是并非所有被唤醒的消费者都能取到一个任务,那么队列被读空了之后的消费者肯定得继续await。如果你用if来判断,这个消费者第二次被notify的时候就不会再次判断!(ok to proceed)这个条件了,如果这个时候这个消费者又一次没抢到任务,但是代码还是往下执行了,轻则空指针异常,重了干出什么事情来都说不定了。

所以必须用while来检查!(ok to proceed),这样可以保证每次被唤醒都会检查一次条件。
package com.lwq.juc.p43;

/**
 * @description:生产者消费者算法初步实现,线程之间做互相通信操作
 * @projectName:juclearn
 * @see:com.lwq.juc.p42
 * @author:LWQ
 * @createTime:2020-12-21 16:18
 * @version:1.0
 */
/**
 * 多线程的三个基本原则:
 * 1、高内聚低耦合的情况下,使用线程操作资源类,完成操作
 * 2、多线程操作基本三步走,判断,干活,通知
 * 3、多线程一旦出现线程之间需要通信,就必须防止多线程的虚假唤醒(在三步走的判断环节使用while可以实现防止虚假唤醒)
 * 线程交互指的就是wait和notify  notifyall(后面看一下多线程基础,就知道了),就是需要对线程做唤醒通知的,一般出现在同步操作中,需要这样,火车站卖票是不需要的
 * 因为不涉及同步,就是先到先得,不是你先做什么我再做什么,(同步和互斥)
 **/

/**
 * 需求,一个空调机器,A,B两个人,初始温度是0,A按+1度,B按-1度 ,按个十轮,最后温度是0
 * 分析:我们看到这个需求,空调是个资源类,拥有加一和减一两个功能,暴露出来给两个人(也就是两个线程)去调用
 */

/**
 * 线程之间的通信,同步互斥基本就是以下框架
 * 判断               干活             (干完)通知
 */

class Airconditioner{
    private int number = 0;
    //加一度操作,synchronized是加锁,保持同步,它拿到锁后其他线程无法操作number。对于number的操作就进入等待池中
    public synchronized void increment() throws InterruptedException {
        while (number != 0){//改为while做一个重复判断
            //线程阻塞,放弃锁
            this.wait();
        }
        //干活,实现操作
        number ++;
        //打印信息
        System.out.println(Thread.currentThread().getName() + ":" + number);
        //唤醒等待池中的其他所有线程
        this.notifyAll();
    }

    //减一度操作
    public synchronized void decrement() throws InterruptedException {
        while(number == 0){//改为while做一个重复判断
            //一个减法A到这里时间片到了,交给另一个减法B,B进来做了一次减法,后面A回来了,执行等待,但是A总会往下走的,因为if只判断一次,
            //A判断完就出去了,再减一就出现问题了,但是如果是while就不会,A在回来之后等待,完了还会拿到number执行一次判断,直到while的条件为
            // true了才会走下去,才可以了。一旦中断回来后需要重新做一次判断重新判断,才能保证它不是被无脑唤醒了,唤醒的没意义,
            this.wait();
        }
        //一个减法都到这了
        number --;
        System.out.println(Thread.currentThread().getName() + ":" + number);
        this.notifyAll();
    }

}

public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {
        Airconditioner airconditioner = new Airconditioner();
        new Thread(()->{//用户线程只负责调用,其他的一律不交互
            for (int i = 0; i < 10; i++) {
                try {
                    airconditioner.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    airconditioner.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();

        new Thread(()->{//用户线程只负责调用,其他的一律不交互
            for (int i = 0; i < 10; i++) {
                try {
                    airconditioner.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    airconditioner.decrement();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"D").start();
    }
}
******运行结果******
发现正确了。

以上这就是线程的虚假唤醒,我们来最后归结一下具体原因:

首先我们假设有四个线程,生产1,生产2,消费1,消费2四个线程,生产者生产蛋糕,消费者拿走蛋糕。

首先生产1进入蛋糕房生产了一个蛋糕(number++),此时他出去了,然后生产者2又进来了,一看有蛋糕就wait,就释放锁,此时他呆着蛋糕房了,然后消费者1进来,一看有蛋糕就拿走了,然后生产者1进来一看没蛋糕了,就生产了一个,然后他出去了出去唤醒等待的,此时生产者2醒了,因为if就判断一次,所以它不会再判断,它就出去了,然后就执行生产代码,此时就出现多个的问题了,当然阳哥有自己的一个理解,我觉得我这个更合理,他对线程优先级调度那块说的不太好。至于3,4,5个同上分析。这就是线程的虚假唤醒,不应该唤醒的被唤醒了。所以就用while去判断,就像离开飞机再回来还要做一次安检一样。

2.5、新模式下的多线程(可重用锁)

2.6、顺序调度线程(信号量机制)

3、八锁理论

3.1、八锁问题

3.1.1、第一锁

正常访问,正常下,就是一个简单的访问,普通的资源类里的两个方法(锁),普通的对cpu资源的竞争。


//资源类,手机作为一个资源类,向外界提供各种方法

class Phone{
    public synchronized void sendEmail(){
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }

    public synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName() + "******************sendSMS*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {

        Phone phone = new Phone();
        //执行了start只是把线程处于唤醒阶段,具有竞争cpu资源的资格,并不是就立即执行了run方法,所以,代码实际上是从上往下走的,但是
        //都走完了start不一定谁先竞争到,所以会出现不确定谁执行的结果,但是我在两个线程之间加了sleep100ms就保证了A有足够的时间去拿到
        //cpu的使用权了
        new Thread(()->{
            phone.sendEmail();
        },"A线程").start();

        Thread.sleep(1000);

        new Thread(()->{
            phone.sendSMS();
        },"B线程").start();
    }
}

*******************************运行结果**************************
就是A先执行打印邮件信息,B后执行打印短信信息,很顺序。没什么难理解的。这是第一锁:
    

3.1.2 、第二锁

邮件方法暂停四秒

class Phone{
    public synchronized void sendEmail() throws InterruptedException {
        //TimeUnit新版写法,就是直接指定单位,后面写个数字,java8以后出的,纳秒什么秒都有
        TimeUnit.SECONDS.sleep(4);//邮件方法暂停四秒
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }
    public synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName() + "******************sendSMS*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A线程").start();

        Thread.sleep(1000);
        new Thread(()->{
            phone.sendSMS();
        },"B线程").start();
    }
}
*******************************运行结果**************************
等待四秒后...
A线程******************sendEmail*****************
B线程******************sendSMS*****************
A线程在B线程前面我们是知道的(因为睡了一秒等待调度,所以可以理解,上面说了),但是为什么是等待四秒,B执行呢,我们知道这等待的四秒是邮件方法内部等待了四秒(TimeUnit.SECONDS.sleep(4);),但是多线程为什么没有在邮件等待这四秒的时间内,让短信线程去执行呢,这就是第二锁:
因为 synchronized 这个加锁它虽然加在了方法上,但是锁的还是整个当前对象this,也就是说,它把这个资源类锁了,也就是说你一时间只能有一个线程进来,因为A先进来,所以,你要等四秒,才输出以上结果。绝对不可能一起进来。
类似于这个手机,一个时刻只能有一个人使用

3.1.3、第三锁

新增一个普通方法,资源类内部新增一个普通方法hello(),请问先打印邮件还是hello(),把短信换成hello().

class Phone{
    public synchronized void sendEmail() throws InterruptedException {
        //TimeUnit新版写法,就是直接指定单位,后面写个数字,java8以后出的,纳秒什么秒都有
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }
    public void hello(){
        System.out.println(Thread.currentThread().getName() + "******************hello*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A线程").start();

        Thread.sleep(1000);//卡顿节点
        new Thread(()->{
            phone.hello();
        },"B线程").start();
    }
}
*******************************运行结果**************************
B线程******************hello*****************
等待四秒后...
A线程******************sendEmail*****************
这个其实要从两方面去分析,我们看到是这么个结果,B先执行了,然后等了四秒A才执行,首先B在A前调度了,这个正常,因为我们的卡顿节点只有1秒,而邮件方法内部就停了4秒,你要是把卡顿调成10秒,其实还是A先执行,这个现象说明,实际上还是按顺序下来的,但是A在停那4秒的时候,B就执行了,这是第三锁,这就和第二锁不一样了:

3.1.4、第四锁

两个资源类,增加一部手机,两个资源类,一个发短信,一个发邮件。

class Phone{
    public synchronized void sendEmail() throws InterruptedException {
        //TimeUnit新版写法,就是直接指定单位,后面写个数字,java8以后出的,纳秒什么秒都有
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }
    public synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName() + "******************sendSMS*****************");
    }
    public void hello(){
        System.out.println(Thread.currentThread().getName() + "******************hello*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A线程").start();

        TimeUnit.SECONDS.sleep(1);//卡顿节点

        new Thread(()->{
            phone2.sendSMS();
        },"B线程").start();
    }
}
*******************************运行结果**************************
B线程******************sendSMS*****************
等待四秒
A线程******************sendEmail*****************
这个结果其实不是很出乎意料,首先我们就不说三锁的那个卡顿时间的问题了,这里B直接出来,A等了四秒,这个很正常我觉得。这就是第4锁:

3.1.5、第五锁

两个静态同步方法(一个手机),两个静态同步方法,一部手机,一个资源类,是怎么个执行顺序呢?

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {
        //TimeUnit新版写法,就是直接指定单位,后面写个数字,java8以后出的,纳秒什么秒都有
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }
    public static synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName() + "******************sendSMS*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A线程").start();

        TimeUnit.SECONDS.sleep(1);//卡顿节点

        new Thread(()->{
            phone1.sendSMS();
        },"B线程").start();
    }
}
*******************************运行结果**************************
等待四秒
A线程******************sendEmail*****************
B线程******************sendSMS*****************
在等待了四秒之后,执行了发邮件和发短信,我们知道我们通过卡顿节点去控制了AB的顺序,但是此时问题又回到了之前,为什么在A等待的那四秒B没有运行呢?这就是第5锁:

3.1.6、第六锁

两个静态同步方法(两个手机),在上面的基础上,加一个手机,一个发送邮件,一个发送短信

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {
        //TimeUnit新版写法,就是直接指定单位,后面写个数字,java8以后出的,纳秒什么秒都有
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }
    public static synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName() + "******************sendSMS*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A线程").start();

        TimeUnit.SECONDS.sleep(1);//卡顿节点

        new Thread(()->{
            phone2.sendSMS();
        },"B线程").start();
    }
}
*******************************运行结果**************************
等待四秒
A线程******************sendEmail*****************
B线程******************sendSMS*****************
在等待了四秒之后,执行了发邮件和发短信,我们知道我们通过卡顿节点去控制了AB的顺序,但是此时问题又回到了之前,为什么在A等待的那四秒B没有运行呢?这就是第6锁:

3.1.7、第七锁

一个静态同步,一个普通同步,一个手机,一个静态同步(有锁),一个普通同步(有锁),一部手机。

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {//静态同步
        //TimeUnit新版写法,就是直接指定单位,后面写个数字,java8以后出的,纳秒什么秒都有
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }
    public  synchronized void sendSMS(){//普通同步
        System.out.println(Thread.currentThread().getName() + "******************sendSMS*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A线程").start();

        TimeUnit.SECONDS.sleep(1);//卡顿节点

        new Thread(()->{
            phone1.sendSMS();
        },"B线程").start();
    }
}
*******************************运行结果**************************
B线程******************sendSMS*****************
等待四秒
A线程******************sendEmail*****************
先执行了发发短信的B,这个是非静态的,等了四秒去执行A,这就是第7锁。

3.1.8、第八锁

一个静态同步,一个普通同步,两个手机,一个静态同步(有锁),一个普通同步(有锁),两部手机。

class Phone{
    public static synchronized void sendEmail() throws InterruptedException {
        //TimeUnit新版写法,就是直接指定单位,后面写个数字,java8以后出的,纳秒什么秒都有
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName() + "******************sendEmail*****************");
    }
    public  synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName() + "******************sendSMS*****************");
    }
}

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            try {
                phone1.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A线程").start();

        TimeUnit.SECONDS.sleep(1);//卡顿节点

        new Thread(()->{
            phone2.sendSMS();
        },"B线程").start();
    }
}
*******************************运行结果**************************
B线程******************sendSMS*****************
等待四秒
A线程******************sendEmail*****************

3.2、八锁原理

1 标准访问,先打印短信还是邮件
2 停4秒在短信方法内,先打印短信还是邮件
3 普通的hello方法,是先打短信还是hello
4 现在有两部手机,先打印短信还是邮件
5 两个静态同步方法,1部手机,先打印短信还是邮件
6 两个静态同步方法,2部手机,先打印短信还是邮件
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
运行答案:
1、短信
2、短信
3、Hello
4、邮件
5、短信
6、短信
7、邮件
8、邮件

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
 
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。
 
 
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象
 
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
 
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
 
所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

4、容器的不安全问题

4.1、List不安全

4.2、Set不安全

4.3、Map不安全

5、Callable

3 普通的hello方法,是先打短信还是hello
4 现在有两部手机,先打印短信还是邮件
5 两个静态同步方法,1部手机,先打印短信还是邮件
6 两个静态同步方法,2部手机,先打印短信还是邮件
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
运行答案:
1、短信
2、短信
3、Hello
4、邮件
5、短信
6、短信
7、邮件
8、邮件

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!




# 4、容器的不安全问题

## 4.1、List不安全

## 4.2、Set不安全

## 4.3、Map不安全

# 5、Callable

















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值