基于《狂神说Java》JUC并发编程--学习笔记

前言:


本笔记仅做学习与复习使用,不存在刻意抄袭。

------------------------------------------------------------------------------------------------------------

给各位学友强烈推荐《遇见狂神说》他的整套Java学习路线使我获益匪浅!!!

点击跳转至遇见狂神说哔哩哔哩首页

如果你也是狂神的小迷弟,可以加我好友一起探讨学习。
 

JUC简述

JUC是什么?

JUC就是java.util.concurrent包,俗称java并发包,是Java开发工程师学习并发的时候需要掌握的内容。

为什么要学习JUC?

我认为起码有以下几点:

  1. 一个初级java程序员进阶学习的必经之路;
  2. 让我们深刻了解并发的思想,为以后的微服务,分布式打好基础;
  3. 能在脑海中构建一个计算机底层处理问题的大概模型,这对研究算法是很有帮助的;
  4. 最后一点,也是最现实的一点,这是面试高频考点,学习JUC并发编程,除了能丰富知识,还能提高今后面对面试的底气。

接下来就是《狂神说Java》JUC并发编程的课程笔记和我自己的总结了,或许,没有看视频的学友看着会觉得有些混乱或者逻辑错误。这里还是强调一点,各位最好是配合视频学习,笔记只当作初学或者今后复习就好。
 

一、学习方式

源码+官方文档(面试高频)

这里简述一下juc在jdk帮助文档的位置(jdk帮助文档请自行百度下载):

我们打开jdk帮助文档

点击左侧目录:

 

二、回忆多线程的学习内容

2.1、实现多线程的三种方式

  • 继承Thread类(其实Thread类根本上也实现了Runnable接口)

我们按住ctrl加鼠标左键,点击Thread :

跳转到:

 可以看到其实现了Runnable接口。

这里简单写个小例子回忆一下继承Thread类如何使用。

package com.example.demo1.demo02;
/**
 * @author liar
 */
public class TestThread {
    public static void main(String[] args) {
        Eat eat = new Eat();
        Out out = new Out();
        eat.start();
        out.start();
    }
}
class Eat extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.println( "我在吃饭呢");
        }
    }
}
class Out extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.println("我在拉屎呢");
        }
    }
}

其核心是重写run方法;其运行结果是:

  • 实现Runnable接口
package com.example.demo1.demo02;

/**
 * @author liar
 */
public class TestRunnable {
    public static void main(String[] args) {
        Who who = new Who();
        FBI fbi = new FBI();
        Thread who1 = new Thread(who,"who");
        Thread fbi1 = new Thread(fbi,"FBI");
        who1.start();
        fbi1.start();
    }
}
class Who implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Who are you ?");
        }
    }
}
class FBI implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Open the door!!!FBI!!!");
        }
    }
}

 运行结果也如预期。

  • 实现Callable接口
package com.example.demo1.demo02;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class TestCallable {
    public static void main(String[] args) {
        Hit hit = new Hit();
        Connection connection = new Connection();
        FutureTask futureTask1 = new FutureTask(hit);
        FutureTask futureTask2 = new FutureTask(connection);
        Thread t1 = new Thread(futureTask1);
        Thread t2 = new Thread(futureTask2);
        t1.start();
        t2.start();
        try {
            //get()方法的返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            Object o1 = futureTask1.get();
            Object o2 = futureTask2.get();
            System.out.println(o1);
            System.out.println(o2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class Hit implements Callable{
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 500; i++) {
            System.out.println("怎么样!!你打我啊!!!");
        }
        return "hit";
    }
}
class Connection implements Callable{
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 500; i++) {
            System.out.println("小伙子,出来混,是要讲人脉的!!!");
        }
        return "connection";
    }
}

其运行结果:

 其与前两者的区别是,可以通过FutureTask获取返回值的。

 

 2.2、总结

普通的线程代码:继承Thread类

Runnable接口:没有返回值,效率相比于Callable接口较低

2.3、线程和进程

关于线程和进程,如果不能用一句话说出来,说明你掌握的还不够扎实!

进程:

  • 一个正在执行的程序
  • 例如:QQ.exe;是运行程序的集合
  • 一个进程可以包含多个线程且至少包含一个线程

Java真的能开启线程吗?

  • 不能开启线程
  • 分析Thread类的原码了解,最后该类还是调用了本地方法(native)
  • Java的底层是c++

2.4、并发和并行

并发:(多线程操作同一资源)

  • CPU一核,模拟出来多线程;天下武功唯快不破,快速交替

并行:(多个人同时走路)

  • CPU多核,多个线程同时执行

并发编程的本质:充分利用CPU的资源

2.5、线程有几个状态

public enum State {
  NEW,//新生

  RUNNABLE,//运行

  BLOCKED,//阻塞

  WAITING,//等待,一直等待

  TIMED_WAITING,//超时等待,过期不候

  TERMINATED;//终止
}

2.6、wait和sleep的区别

来自不同的类:

  • wait==>Object
  • sleep==>Thread

关于锁的释放

  • wait:会释放锁
  • sleep:“抱着锁睡觉,不会释放锁”

使用的范围是不同的

  • wait:必须在同步代码块中使用
  • sleep:任何地方

是否需要捕获异常

  • wait:不需要捕获异常(什么叫中断异常?)
  • sleep:需要捕获异常(存在超时等待的情况)
     

三、Lock锁(重点)

3.1、回忆"synchronized"

作用:

保证线程安全,例如当多个线程访问统一资源时,会发生数据紊乱问题;

怎么理解:

队列+锁

实例:没有synchronized修饰的方法

package com.example.demo1.demo02;
/**
 * @author liar
 */
public class UnsafeTicket {
    public static void main(String[] args) {
        BuyTicket ticket = new BuyTicket();
        new Thread(ticket,"倒霉的我").start();
        new Thread(ticket,"幸运的你们").start();
        new Thread(ticket,"可恶的黄牛党").start();
    }
}
class BuyTicket implements Runnable{
    private int tickets = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buy();
        }
    }
    //在方法名前加上"synchronized",那么该方法就变成了同步方法了
    //!!!锁的是"this"!!!
    //'synchronized'默认锁的是this
    private synchronized void buy(){
        if (tickets<=0){
            flag=false;
            //这个return放在这里,如果符合if条件,那么返回出去,不执行下面的sout语句
            return;
        }
        System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
    }
}

我们看一下运行效果:

 实例:有synchronized修饰的方法

package com.example.demo1.demo02;
/**
 * @author liar
 */
public class UnsafeTicket {
    public static void main(String[] args) {
        BuyTicket ticket = new BuyTicket();
        new Thread(ticket,"倒霉的我").start();
        new Thread(ticket,"幸运的你们").start();
        new Thread(ticket,"可恶的黄牛党").start();
    }
}
class BuyTicket implements Runnable{
    private int tickets = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buy();
        }
    }
    //在方法名前加上"synchronized",那么该方法就变成了同步方法了
    //!!!锁的是"this"!!!
    //'synchronized'默认锁的是this
    private synchronized void buy(){
        if (tickets<=0){
            flag=false;
            //这个return放在这里,如果符合if条件,那么返回出去,不执行下面的sout语句
            return;
        }
        System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
    }
}

 这下,就是线程安全的了。

3.2、Lock接口(Lock锁)

 

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

//基本的卖票例子
/*
    真正的多线程开发,公司中的开发,降低耦合性
    线程就是一个单独的资源类,没有任何附属操作
    1、属性、方法
 */
public class SaleTicketDemo2 {
    public static void main(String[] args) {
        //并发:多线程操作同一个类,把资源类丢入线程
        Ticket2 ticket = new Ticket2();
        //@FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{代码}
        new Thread( () ->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"A").start();
        new Thread( () ->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"B").start();
        new Thread( () ->{
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"C").start();

    }
}

//Lock三部曲
//1.new ReentrantLock();
//2. lock.lock();    加锁
//3.finally -->  lock.unlock(); 解锁
class Ticket2{
    //属性方法
    private int number = 50;
    Lock lock = new ReentrantLock();

    public  void sale(){
        lock.lock();    //加锁

        try{
            //业务代码
            if(number > 0){
                System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //解锁
            lock.unlock();
        }

    }
}

3.3、Synchronized和Lock的区别


1、Synchronized内置的Java关键字,Lock是一个Java类
2、Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
3、Synchronized是全自动的,会自动释放锁,Lock必须要手动释放锁,如果不释放锁,死锁
4、Synchronized 线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去
5、Synchronized可重入锁,不可以中断,非公平;Lock可重入锁,可以判断锁,非公平(可以自己设置)
6、Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码
 

3.5、思考

锁是什么?如何判断锁的是谁?

四、生产者和消费者问题

面试:单例模式、排序算法、生产者和消费者问题、死锁

4.1、synchronized版

package com.my.pc;
/*
* 线程之间的通信问题:生产者和消费者问题(等待唤醒,通知唤醒)
*
* 线程交替执行:
*   生产者和消费者操作同一个变量
*       生产者:+1
*       消费者:-1
* */
public class Test {
    public static void main(String[] args) {
        Data data = new Data();

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

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

//资源类
/*
* 编写资源类的思路:
*   判断等待、业务、通知
* */
class Data{
    private int number = 0;

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

}

这样会出现问题!!–虚假唤醒

场景:当出现多个生产者和消费者时,会出现数据紊乱?

原因:就是这个if语句只判断一次:当消费者线程阻塞,来唤醒生产者线程时,多个生产者线程都会被唤醒,但是只有第二个生产者线程执行了if判断,然后第二个生产者线程阻塞(基于if语句的机理),因为第一个生产者线程能够正常执行,而剩余的其他生产者线程,则会执行if语句后面的方法(“意思就是后面的生产者线程在if判断前面生产者线程时,利用了if语句的bug捡了漏”)

(1)if判断流水线状态为空时,线程被阻塞,这时if判断就完成了,线程被唤醒后直接执行线程剩余操作

(2)while判断流水线状态为空时,线程被阻塞,这时的while循环没有完成,线程被唤醒后会先进行while判断

解决方法:

把if语句改为while语句

package com.my.pc;
/*
* 线程之间的通信问题:生产者和消费者问题(等待唤醒,通知唤醒)
*
* 线程交替执行:
*   生产者和消费者操作同一个变量
*       生产者:+1
*       消费者:-1
* */
public class Test {
    public static void main(String[] args) {
        Data data = new Data();

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

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

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

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

//资源类
/*
* 编写资源类的思路:
*   判断等待、业务、通知
* */
class Data{
    private int number = 0;

    public synchronized void increment() throws Interrupted
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值