JUC-01-JUC简介,进程和线程,Lock锁,生产消费问题

本文深入讲解Java并发编程的基础知识,包括多线程的创建方法、线程的状态与等待机制、锁的概念及其应用,同时对比了synchronized与Lock的不同之处,并通过生产者消费者问题展示了线程间的通讯。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

01.什么是JUC?

所谓JUC就是java并发工具包:java.util.concurrent,学习JUC主要学习三个包的使用:

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

02.回顾多线程

进程和线程

进程:

  • 进程是指在内存中运行的一个应用程序(例如:微信),每个进程都有自己独立的一块内存空间,即进程空间或(虚空间)。
  • 进程不依赖于线程而独立存在,一个进程中可以启动多个线程。

线程:

  • 线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如:Java程序默认有2个线程:mian线程和GC线程。
  • 线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。

java开启多线程的两种方法

第一种:通过继承Thread类创建线程类,步骤如下:

  • 1.定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的行体。
  • 2.创建该类的实例对象,即创建一个线程对象。
  • 3.使用线程对象调用start()方法来启动线程。
  • 注意:也可以使用匿名内部类的方式创建和启动线程
  • 一般的实现举例:
//使用这种方式不能达到资源共享的目的
public class Test {
    public static void main(String[] args) {
    	//第二步:
       MyThread myThread=new MyThread();
       //第三步:
       myThread.start();
    }
}
//第一步:
class MyThread extends  Thread{
    @Override
    public void run() {
        System.out.println("线程执行的内容。。。");
    }
}
  • 使用匿名内部类的方式实现
public class Test {
    public static void main(String[] args) {
        //方式一,使用匿名继承Thread
      new Thread("Thread_A") {
          @Override
          public void run() {
              System.out.println("Thread_A线程执行的内容。。。");
          }
      }.start();

      //方式二,使用lambda表达式
        new Thread(()->{
            System.out.println("Thread_B线程执行的内容。。。");
        },"Thread_B").start();
    }
}

第二种,通过实现Runnable接口创建线程类,步骤如下:

  • 1.定义一个类实现Runnable接口;
  • 2.创建该类的实例对象obj;
  • 3.将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
  • 4.调用线程对象的start()方法启动该线程;
//注意:使用这种方式可以达到资源共享的目的
public class Test {
    public static void main(String[] args) {
        //第二步:
        MyThread myThread=new MyThread();
        //第三步:
        Thread thread=new Thread(myThread,"Thread_A");
        //第四步:
        thread.start();

        //第三步和第四步的合并写法
        new Thread(myThread,"Thread_B").start();
    }
}
//第一步:
class MyThread implements  Runnable{
    @Override
    public void run() {
        System.out.println("线程执行的内容。。。");
    }
}

总结:

  • Thread类本身也实现了Runable接口,并重写了Runbale接口提供的run方法。
  • 第一种方式是继承Thread类,第二种方式是实现Runable接口。
  • 虽然上面说的是两种方式启动多线程,但是本质都是使用了JDK提供的Thread类来启动多线程的。
  • 第二种方式的实现是因为Thread类提供了入参为Runable类型的构造方法。

线程的状态:

Thread类中有一个内部枚举类state,在该类中列出了一个线程可有的状态。

public enum State {
       NEW,			//新建
       RUNNABLE,		//运行
       BLOCKED,		//阻塞
       WAITING,		//等待
       TIMED_WAITING,	//超时等待
       TERMINATED;		//终止
   }

线程等待:wait和sleep的对比

1.来自不同的类:

  • Object.wait() :是线程进入等待状态。线程会放弃对象锁,等待被其他线程唤醒。
  • Thread.sleep() :使线程暂停执行指定的时间,让出cpu给其它线程。但是它的监控状态依然保持,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。

2.关于锁被释放

  • wait会释放锁
  • sleep不会释放锁

3.使用的范围不同

  • wait必须在同步代码块中
  • sleep可以在任何地方休眠

03.java.util.concurrent.locks.Lock(Lock锁)

Synchronized

在说Lock锁之前,先说一下Synchronized关键字。

场景一:把资源放在线程类中,如下(不建议使用):

public class Test001 {
    public static void main(String[] args) {
        TestThread testThread=new TestThread();
        testThread.start();
    }
}

class TestThread extends Thread{
    private int ticket=50;//资源
    @Override
    public void run() {
        if(ticket>0){
            ticket--;
            System.out.println(Thread.currentThread().getName()+ticket);
        }
    }
}

场景二:把资源和线程类分开,使用的时候直接把资源丢进线程中即可,为了安全使用synchronized关键字给sale方法解锁,

public class SaleTicketDemo01 {
    public static void main(String[] args) {
        //并发:多线程操作同一个资源,把资源直接丢入线程
        Ticket ticket=new Ticket();
        new Thread(()->{//每个线程都买60次
            for (int i=0;i<60;i++){
                ticket.sale();
            }
        },"A").start();

        new Thread(()->{
            for (int i=0;i<60;i++){
                ticket.sale();
            }
        },"B").start();

        new Thread(()->{
            for (int i=0;i<60;i++){
                ticket.sale();
            }
        },"C").start();

    }
}
//资源类
class Ticket{
    //定义票数
    private int ticket=50;
    //synchronized 本质:排队
    public synchronized void sale(){
        if(ticket>0){
            System.out.println(Thread.currentThread().getName()+":sale:"+(ticket--)+" ticket,剩余:"+ticket);
        }
    }
}

Lock锁

Lock是java.util.concurrent.locks包下的一个接口,这个包下面一共三个接口
在这里插入图片描述

Lock接口有三个实现类
在这里插入图片描述

ReentrantLock :可重入锁

ReentrantReadWriteLock:可重入读写锁的两个内部类
- ReentrantReadWriteLock.ReadLock:可重入读写锁的读锁
- ReentrantReadWriteLock.WriteLock :可重入读写锁的写锁

使用方法:使用Lock接口的一个实现类ReentrantLock

使用Lock锁的三步:
1.Lock lock=new ReentrantLock();
2.lock.lock(); // 加锁
3.finally代码块中使用:lock.unlock();// 解锁

示例:

public class SaleTiketDemo02 {
    public static void main(String[] args) {
        //并发:多线程操作同一个资源,把资源直接丢入线程
        Ticket2 ticket=new Ticket2();
        new Thread(()->{//每个线程都买60次
            for (int i=0;i<60;i++){
                ticket.sale();
            }
        },"A").start();

        new Thread(()->{
            for (int i=0;i<60;i++){
                ticket.sale();
            }
        },"B").start();

        new Thread(()->{
            for (int i=0;i<60;i++){
                ticket.sale();
            }
        },"C").start();
    }
}
//资源类
class Ticket2{
    //定义票数
    private int ticket=50;

    //从底层源码可以看出,不加参数为非公平锁,加参数True为公平锁
    Lock lock=new ReentrantLock();//第一步

    public void sale(){
        lock.lock();//第二步:
        try {//业务代码
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+":sale:"+(ticket--)+" ticket,剩余:"+ticket);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock :可重入锁,看一下这个类的构造方法

//有两个构造方法
//第一个构造方法,不穿参数,默认是非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
//第二个构造方法,传参数,true为公平锁,false为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:有先来后到的说法,要排队
非公平锁:可以插队

Synchronized 和 Lock 区别

Synchronized和Lock都是Java中用于实现线程同步的机制,但它们在实现方式、功能和性能上存在显著差异。

特性SynchronizedLock(ReentrantLock)
实现层次JVM层面,Java内置关键字JDK层面,Java代码实现
锁的获取和释放自动获取,自动释放(代码块执行结束或者异常时自动释放)手动获取(Lock.lock),手动释放(在finally代码块中调用Lock.unlock释放)
锁类型非公平锁可选公平和非公平
锁状态查询无法判断获取锁的状态可以判断是否获取到了锁
锁等待机制获取不到一直等待可以设置获取锁的方式,lock()方法会一直等待,tryLock()方法完全不等待,tryLock(timeout, unit)有限时间等待
中断响应不支持中断等待支持
性能Java 6 后优化,性能近Lock高竞争情况下性能更好
代码灵活性简单但是不灵活灵活单逻辑复杂
使用场景同步少量代码块同步大量代码

04.线程之间的通讯(生产者和消费者问题)

生产者和消费者的问题,其实就是线程之间的通信问题。线程间通信是多线程编程中的核心问题,Java提供了多种机制来实现线程间的协调与数据交换。

synchronized版本

在这里插入图片描述

synchronized版本的生产消费问题:synchronized ,wait,notify

线程交替执行,四个线程ABCD操作一个变量num(值在0-1之间)
* 生成者线程AC执行:num+1
* 消费者线程BD执行:num-1

问题:
* 当生产者线程A生产完之后,可能会唤醒生产者C,这时候num的值就有可能为2
* 当消费者线程B消费完之后,可能会唤醒消费者D,这时候num的值就有可能为-1

解决办法:
* 方法一:如果使用【if】的话,只会判断一次,可能还会出现上面的问题(虚假唤醒)
* 方法二:如果使用【循环】的话,把等待放在循环中,每次其他线程执行完,唤醒一个线程的时候都会进行判断

结论:
*使用if会出现虚假唤醒解决不了上面的问题,使用while循环则可以

为何使用if不行的具体原因:if中的条件只会判断一次,而while则会每次都判断,具体如下
* 1.num初始值为0* 2.线程A拿到锁,此时线程A开始执行加1操作,加1完成之后唤醒其他线程。
* 3.假如唤醒了线程C,由于num的值为1,进入判断条件,调用wait方法,释放对象锁。
* 4.假如此时B拿到锁,执行减1操作,减1完成之后唤醒其他线程
* 5.假如此时唤醒了线程A,此时线程A开始执行加1操作,加1完成之后唤醒其他线程。
* 6.假如唤醒了线程C,此时num的值虽然为1,但是if条件不会再次判断,此时线程C还会对num进行加1操作,从而出现上面的为2的情况。
* 7.如果使用while则需要再次判断,就可以避免上面的问题。

示例代码:

public class test01 {
    public static void main(String[] args) throws InterruptedException{
        //并发:多个线程共享一个资源资源
        //创建多个线程,把资源直接丢入线程
        Data data=new Data();

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

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

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

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

/*等待,业务,通知*/
class Data{//数据类,资源类
    private int num=0;
    //+1
    public synchronized  void increment() throws InterruptedException{
        while(num!=0){
            this.wait();//等待
        }
        num++;//业务
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        this.notifyAll();//通知
    }
    //-1
    public synchronized  void decrement()throws InterruptedException{
        while(num==0){
            this.wait();//等待
        }
        num--;//业务
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        this.notifyAll();//通知
    }
}

虚假唤醒是如何产生的

while (num != 0) {}

换成 if (num == 0) {}

就会出现虚假唤醒。官方文档有标注;

为什么if判断会出现虚假唤醒?

1. 因为if只会判断一次

2.while每次都会判断

JUC 版本

在这里插入图片描述
JUC 版本的生产消费问题:Lock,await,single

Lock:代替synchronized关键字的作用 

通过lock.newCondition创建一个条件

condition.await:代替wait

condition.single:代替notify

通过刚才的演示可以发现,这种实现的效果和使用synchronized,wait,notify的效果是一样的

注意:里面也要使用while进行判断

任何一种新的技术绝不会只是对原来技术的一种覆盖,肯定有它的优势,和对原来技术的补充,
public class Test02 {
    public static void main(String[] args) throws InterruptedException{
        //并发:多个线程共享一个资源资源
        //创建多个线程,把资源直接丢入线程
        Data2 data=new Data2();
        new Thread(()->{
            for(int i=0;i<15;i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }},"A:producer").start();

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

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

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


    }
}

/*等待,业务,通知*/
class Data2{ //数据类,资源类
    private int num=0;
    Lock lock=new ReentrantLock();
    Condition condition=lock.newCondition();

    //+1
    public void increment() throws InterruptedException{
        try {
            lock.lock();
            while(num!=0) {
                condition.await();//等待
            }
            num++;//业务
            System.out.println(Thread.currentThread().getName()+"==>"+num);
            condition.signalAll();//通知
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    //-1
    public synchronized  void decrement()throws InterruptedException{
        try {
            lock.lock();
            while(num==0){
               condition.await();//等待
            }
            num--;//业务
            System.out.println(Thread.currentThread().getName()+"==>"+num);
            condition.signalAll();//通知
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

java.util.concurrent.locks.Condition(精准唤醒)

java.util.concurrent.locks一共有三个接口

Lock
Condition
ReadWriteLock 

示例:

//Condition:实现精准的通知和唤醒作用

/*A执行完调用B
* B执行完调用C
* C执行完调用A
* */
public class Test03 {
    public static void main(String[] args) throws InterruptedException{
        //并发:多个线程共享一个资源资源
        //创建多个线程,把资源直接丢入线程
        Data3 data=new Data3();
        new Thread(()->{
            for(int i=0;i<12;i++){
                try {
                    data.printA();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }},"Thread_A").start();

        new Thread(()->{
            for(int i=0;i<12;i++){
                try {
                    data.printB();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }},"Thread_B").start();

        new Thread(()->{
            for(int i=0;i<12;i++){
                try {
                    data.printC();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }},"Thread_C").start();

    }
}

/*等待,业务,通知*/
class Data3{ //数据类,资源类
    private int flag=1;//flag为1 A线程执行,2 B 线程执行,3 C 线程执行
    Lock lock=new ReentrantLock();
    Condition conditionA=lock.newCondition();
    Condition conditionB=lock.newCondition();
    Condition conditionC=lock.newCondition();

    public void printA(){
        try {
            lock.lock();
            while (flag!=1){
                conditionA.await();//等待
            }
            System.out.println(Thread.currentThread().getName()+".....AAAAA....");
            flag=2;//唤醒B线程
            conditionB.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB(){
        try{
            lock.lock();
            while (flag!=2){
                conditionB.await();//等待
                }
            System.out.println(Thread.currentThread().getName()+".....BBBBB....");
            flag=3;//唤醒B线程
        conditionC.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        try {
            lock.lock();
            while (flag!=3){
                conditionC.await();//等待
            }
            System.out.println(Thread.currentThread().getName()+".....CCCCC....");
            flag=1;//唤醒B线程
            conditionA.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

总结

  • 任何一种新的技术绝不会只是对原来技术的一种覆盖,肯定有它的优势,和对原来技术的补充。
  • synchronized版本如法实现精准唤醒,虽然JDK提供了notify方法和notifyAll方法,但是这两种方法都不能实现精准唤醒
  • notify方法:唤醒一个等待的线程,选择唤醒哪个线程不确定,也就是随机唤醒一个线程。
  • notifyAll方法:唤醒所有等待的线程,它们都有机会争夺资源,具体那个线程能争夺到资源也不确定。
  • JUC 版本可以实现synchronized版本的功能,还能在此基础上实现线程的精准唤醒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇宇小跟班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值