JUC基础

JUC简述

JUC实际上就是我们对于jdk中java.util.concurrent工具包的简称。这个包下的类都是和 **Java多线程开发 **相关的类。

线程与进程

  • 程序:为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。

  • 软件:1个或多个应用程序+相关的素材和资源文件等构成一个软件系统。

  • 进程是对一个程序运行过程(创建-运行-消亡)的描述,系统会为每个运行的程序建立一个进程,并为进程分配独立的系统资源,比如内存空间等资源。

  • 线程:线程是进程中的一个执行单元,负责完成执行当前程序的任务,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这时这个应用程序也可以称之为多线程程序。多线程使得程序可以并发执行,充分利用CPU资源。

    面试题:进程是操作系统调度和分配资源的最小单位,线程是CPU调度的最小单位。不同的进程之间是不共享内存的。进程之间的数据交换和通信的成本是很高。不同的线程是共享同一个进程的内存的。当然不同的线程也有自己独立的内存空间。对于方法区,堆中中的同一个对象的内存,线程之间是可以共享的,但是栈的局部变量永远是独立的。

线程调度

指CPU资源如何分配给不同的线程。常见的两种线程调度方式:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java采用的是抢占式调度方式

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

      实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
      其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

线程的创建方式

java虚拟机是支持多线程的,当运行Java程序时,至少已经有一个线程了,那就是main线程。

继承Thread类

Java中java.lang.Thread是表示线程的类,每个Thread类或其子类的实例代表一个线程对象。

通过继承Thread类来创建并启动多线程的步骤:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

自定义线程类:

public class MyThread extends Thread {
	//定义指定线程名称的构造方法
	public MyThread(String name) {
		//调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
	/**
	 * 重写run方法,完成该线程执行的逻辑
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}

测试类:创建线程对象并启动线程

public class Demo01 {
	public static void main(String[] args) {
		//创建自定义线程对象
		MyThread mt = new MyThread("新的线程!");
		//开启新线程
		mt.start();
		//在主方法中执行for循环
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程!"+i);
		}
	}
}

注意事项:

  • 手动调用run方法不是启动线程的方式,只是普通方法调用。

  • start方法启动线程后,run方法会由JVM调用执行。

  • 不要重复启动同一个线程,否则抛出异常IllegalThreadStateException

  • 不要使用Junit单元测试多线程,不支持,主线程结束后会调用System.exit()直接退出JVM;

实现Runnable接口

Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法

通过实现Runnable接口创建线程并启动的步骤:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
    的线程对象。
  3. 调用线程对象的start()方法来启动线程。

自定义线程任务类:

  public class MyRunnable implements Runnable{
  	@Override  
      public void run() {
          for (int i = 0; i < 20; i++) {
          	System.out.println(Thread.currentThread().getName()+" "+i);         
  		}       
  	}    
  }

测试类:创建线程对象并启动线程

  public class Demo {
      public static void main(String[] args) {
          //创建自定义类对象  线程任务对象
          MyRunnable mr = new MyRunnable();
          //创建线程对象
          Thread t = new Thread(mr, "小强");
          t.start();
          for (int i = 0; i < 20; i++) {
              System.out.println("旺财 " + i);
          }
      }
  }

两种创建线程方式比较

  • Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务。

    public class Thread implements Runnable {}
    
  • 因为Java类是单继承的,所以继承Thread的方式有单继承的局限性,但是使用上更简单一些。

  • 实现Runnable接口的方式,避免了单继承的局限性,并且可以使多个线程对象共享一个Runnable实现类(线程任务类)对象,从而方便在多线程任务执行时共享数据。

利用Callable接口、FutureTask对象实现。

可以得到线程执行的结果 通过 FutureTask对象. get()

①、得到任务对象

​ 1.定义类实现Callable接口,重写call方法,封装要做的事情。

import java.util.concurrent.Callable;

public class CallableThread implements Callable {
    @Override
    public Object call() throws Exception {
        int sum =0;
        for (int i = 1; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}

​ 2.用FutureTask把Callable对象封装成线程任务对象。

CallableThread ct = new CallableThread();
FutureTask fk = new FutureTask<>(ct);

②、把线程任务对象交给Thread处理。

③、调用Thread的start方法启动线程,执行任务

Thread thread = new Thread(futureTask);
thread.start();

④、线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

System.out.println(futureTask.get()); //get()会堵塞,直到子线程执行结束

System.out.println(futureTask.get(6, TimeUnit.SECONDS));//当前主线程获取子线程的结果,默认阻塞等待,最多只阻塞等待6s
//callable类
import java.util.concurrent.Callable;
public class CallableThread implements Callable {
    @Override
    public Object call() throws Exception {
        int sum =0;
        for (int i = 1; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}

//test
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableThread callableThread = new CallableThread();
        FutureTask futureTask = new FutureTask<>(callableThread);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

创建线程3种方式总结

1、直接继承Thread类,线程和任务合并在一起,代码简单,但扩展性差,因为Java是单继承。

2、实现Runnable接口或者Callable接口。线程和任务进行了分离,扩展性强,我们的任务类还可以继续继承某一个类。

3、Runnable接口中的run方法没有返回值也没有异常,Callable中的call方法存在返回值也声明了异常。

Thread类核心API

  • public void run() :此线程要执行的任务在此处定义代码。

  • public String getName() :获取当前线程名称。

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

  • public final int getPriority() :返回线程优先级

  • public final void setPriority(int newPriority) :改变线程的优先级

    • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
    • MAX_PRIORITY(10):最高优先级
    • MIN _PRIORITY (1):最低优先级
    • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

线程名称

public final String getName()							// 获取线程名称
public final void setName(String name)					// 调用setName方法设置线程名称
public Thread(String name)							    // 构造方法设置线程名称

线程对象

获取当前正在执行该方法的线程对象:

public static native Thread currentThread();			// 获取当前执行该线程体的线程对象

线程休眠

public static void sleep(long time)        				// 让线程休眠指定的时间,单位为毫秒
TimeUnit.时间单位.sleep(时间值);						    // 使用时间枚举类让线程休眠

线程加入

把某一个线程加入到当前线程的执行流程中。

public final void join() throws InterruptedException	

当某一个程序执行流程中调用了其他线程的join()方法,调用线程暂停执行,直到被join()方法加入的join线程执行完成为止。

注: 需要在线程启动以后在进行加入才有效

比如现在存在两个线程,一个t1线程 , 一个是t2线程,当我们t1线程执行到某一个时刻的时候,我们在t1线程的执行流中添加了t2线程,那么此时t1线程暂停执行,直到t2线程执行完毕以后t1线程才可以继续执行。

public class ThreadDemo01 {
    public static void main(String[] args) {
        // 我们在主线程的执行流中加入其它线程
        for(int x = 0 ;  x < 100 ; x++) {
            // 当x的值等于20的执行加入其它线程
            if(x == 10) {

                // 创建MyThread线程对象
                MyThread myThread = new MyThread();
                myThread.setName("atguigu-01");
                myThread.start();

                // 调用join方法进行线程加入
                try {
                    myThread.join();                    // 需要在线程启动以后在进行加入才有效
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 主线程执行代码
            System.out.println(Thread.currentThread().getName() + "--------->>" + x);
        }
    }
}

线程中断

interrupt方法

当调用线程的sleep方法时,可以让该线程处于等待状态,调用该线程的interrupt()方法就可以打断该阻塞状态,中断阻塞状态以后,继续执行(前提是别throw),而不是让线程结束,并且此方法会抛出一个InterruptedException异常。

public void interrupt();	// 中断线程的阻塞状态

案例:演示中断sleep的等待状态

线程类:

public class MyThread extends Thread {

    @Override
    public void run() {

        for(int x = 0 ; x < 100 ; x++) {
            System.out.println(Thread.currentThread().getName() + "----" + x );
            if(x == 10) {
                try {
                    TimeUnit.SECONDS.sleep(10000);     // 线程休眠以后,该线程就处于阻塞状态
                } catch (InterruptedException e) {
                    e.printStackTrace();//如果这里throw,则当前线程的sleep被中断后,后续代码也就无法执行了
                }
            }
        }
    }
}

测试类:

public class ThreadDemo1 {

    public static void main(String[] args) {

        // 创建MyThread线程对象
        MyThread t1 = new MyThread();
        t1.setName("chs-01");

        // 启动线程
        t1.start();

        try {
            // 主线程休眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 中断t1线程的休眠
        t1.interrupt();

    }

}

控制台输出结果

...
atguigu-01----10
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:339)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at com.atguigu.javase.thread.api.demo14.MyThread.run(MyThread.java:14)
atguigu-01----11
...

通过控制台的输出结果,我们可以看到interrupted方法并没有去结束当前线程,而是将线程的阻塞状态中断了,中断阻塞状态以后,线程chs-01继续进行执行。

stop方法

调用线程的stop方法可以让线程终止执行,没有异常。

public final void stop()  // 终止线程的执行

线程类

public class MyThread extends Thread {

    @Override
    public void run() {

        for(int x = 0 ; x < 100 ; x++) {
            System.out.println(Thread.currentThread().getName() + "----" + x );
            if(x == 10) {
                try {
                    TimeUnit.SECONDS.sleep(10000);     // 线程休眠以后,该线程就处于阻塞状态
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

测试类

public class ThreadDemo1 {

    public static void main(String[] args) {

        // 创建MyThread线程对象
        MyThread t1 = new MyThread();
        t1.setName("chs-01");

        // 启动线程
        t1.start();

        try {
            // 主线程休眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 终止线程t1的执行
        t1.stop();

    }

}

控制台输出结果

...
chs-01----9
chs-01----10

控制台没有任何异常输出,程序结束,"chs-01"线程没有继续进行执行。

总结:interrupt方法、stop方法区别

  • interrupt 用于优雅地请求线程中断,同时让线程决定如何处理该请求;它是安全的,推荐使用。
  • stop 是直接强制终止线程,不安全,可能导致数据不一致,已被弃用,不建议使用。

选择使用 interrupt 方法来管理线程的生命周期和控制是多线程编程的推荐做法。

守护线程

有一种线程是在后台运行的,它的任务就是为其他的线程提供服务,这种线程被称之为"后台线程",又被称之为"守护线程"。

JVM的垃圾回收线程就是典型的后台线程。

后台线程的特征:如果所有的前台线程都结束,后台线程会自动结束,前后台线程都结束了,JVM就退出了。

常见的前台线程:主线程、之前创建的自定义线程...

创建守护线程

创建普通线程,调用其setDaemon(true)方法,即创建了守护线程。

public final void setDaemon(boolean on)    // 将某一个线程设置为后台/守护线程

测试类

public class ThreadDemo01 {

    public static void main(String[] args) {

        // 开启两个线程
        MyThread t1 = new MyThread();
        t1.setName("关羽");

        MyThread t2 = new MyThread();
        t2.setName("张飞");

        // 将关羽线程设置为守护线程(将某一个线程设置为守护线程,必须在启动线程之前)
        t1.setDaemon(true);
        t2.setDaemon(true);

        // 启动线程
        t1.start();
        t2.start();

        // 在主线程中编写代码
        Thread.currentThread().setName("---------------刘备");
        for(int x = 0 ;  x < 5 ; x++) {
            System.out.println(Thread.currentThread().getName() + "-----" + x);
        }

        /**
         * 主线程 == 前台线程
         * t1和t2 == 守护线程
         * 当主线程执行完毕以后,剩下的线程都是守护线程了jvm就会终止
         */
    }

}

注意:前台线程全部结束后,JVM会通知后台线程全部结束,但从它接收到指令到做出响应,需要一定时间,而在这一段时间内其他线程还可以继续执行。

线程安全问题

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,但是如果多个线程中对资源有读和写的操作,就会出现前后数据不一致问题,这就是线程安全问题。

解决问题思路

解决线程安全的思路: 就是将多个线程对共享数据的并发访问更改为串行访问

串行访问(同步访问)就是指:一个共享数据一次只能被一个线程访问,该线程访问完毕以后其他的线程才可以访问。

要实现共享数据的串行访问,我们就需要使用机制来完成。

相关概念

1、获取锁/申请锁:一个线程在访问共享数据之前,我们必须要申请锁,申请锁的这个过程我们将其称之为获取锁。

2、持有锁的线程: 一个线程获得了某一个锁,我们就将该线程称之为锁的持有线程。

3、临界区:获取锁 到 释放锁,这个区间称之为“临界区”,共享数据只能在临界区内进行访问,临界区一次只能被一个线程执行。

锁的持有线程可以对该锁所保护的共享数据进行访问,访问结束以后该线程就需要释放锁。

image-20241016103505751

隐式锁(synchronized)

synchronized 实现的加锁和释放锁是自动的,不需要显示执行,称之为“隐式锁”

synchronized概述

synchronized锁是Java中用于控制 多线程访问共享资源 的工具。

它可以用来修饰 代码块 或者 方法 ,确保在同一时刻只有一个线程可以执行被修饰的代码。

当线程尝试获取锁时,如果锁被其他线程持有,那么当前线程会被阻塞,直到锁被释放。

也称之为"进程内"的锁。同一个进程内的多个线程,使用synchronized进行并发安全控制。

注意:

synchronized属于jvm层面的一把锁,不同的进程使用的jvm是不一样的,所以不同的进程之间的多个线程是不能使用synchronized进行并发安全控制的。

同步代码块的格式

synchronized (对象) {
	// 在此代码块中访问共享数据
}
//该对象可以是任意的对象,这个对象可以简单的理解就是一把锁,但是需要保证多个线程在访问的时候使用的是同一个对象。

同步方法的格式

public synchronized void sellTicket(){...}
public static synchronized void sellTicket(){...}

锁对象研究

思考问题:同步代码块、同步方法、静态同步方法的锁对象分别是谁?

1、普通同步方法,是实例锁,锁是当前实例对象。

2、静态同步方法,是类锁,锁是当前类的Class对象。

3、同步代码块,锁是synchonized括号里配置的对象。

死锁现象

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

显式锁(Lock)

  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
  • Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
  • Lock属于api层面的一把锁,和synchronized都是属于进程内的一把锁。
方法名称 说明
public ReentrantLock() 获得Lock锁的实现类对象
方法名称 说明
void lock() 获取锁,如果锁已经被其他线程持有,调用线程将被阻塞直到锁可用
void unlock() 释放锁,必须在持有该锁的线程中调用
boolean tryLock() 尝试获取锁,如果锁可用则返回 true,否则返回 false。不会阻塞
boolean tryLock(long timeout, TimeUnit unit) 尝试获取锁,如果在给定的等待时间内能够获得锁,则返回 true,否则返回 false
Condition newCondition() 返回一个 Condition 变量,用于实现等待/通知机制
boolean isHeldByCurrentThread() 判断当前线程是否持有此锁
int getHoldCount() 返回当前线程保持此锁的次数,仅在 ReentrantLock 中有效
Thread getOwner() 返回当前持有锁的线程,仅在 ReentrantLock 中有效
static Lock lock = new ReentrantLock(); 
lock.lock()  //加锁
  try{
     //被锁的代码 
  }catch(Execption e){
      //处理异常
  }finally{
      lock.unlock(); //释放锁
  }

synchronized和Lock的区别:

1、前者属于jvm层面的锁,java中的一个关键字,锁数对像可以是某个实例对像,也可以是类对像Class,Lock属于api层面的一把锁。是juc包下的一个接口。

2、synchronized是非公平锁,不可中断锁,可重入的,悲观锁,独占锁,进程内锁。
Lock接口的实现类ReentrantLock,即可实现公平锁也可以实现非公平锁,可中断锁,可重入,悲观锁,进程内锁,独占锁(如果需要实现共享锁,需要使用其他的实现类ReentrantReadWriteLock).

3、并发量不高的情况下,两者的性能没有区别。高并发情况下,Lock(pi层面的)效率更高,并目Lock更灵活。

4、synchronized隐式锁,Lock显示锁。

分布式锁

解决多进程之间的多个线程安全问题使用

  1. 跨进程协作: 分布式锁能够在多个进程或服务器之间进行协作,确保同一时刻只有一个进程或服务器能获得锁。
  2. 高可用性: 分布式锁跟随分布式系统的高可用特性,通常锁的实现需要在多个节点上具备容灾能力。
  3. 租约机制: 为了避免死锁,很多分布式锁都有租约机制,设计为锁会在一定时间后自动释放,即使持有锁的进程崩溃或者未进行解锁操作。
  4. 性能考虑: 分布式锁通常需要在网络上传输相关请求,所以在性能方面要尽量优化,降低锁的粒度和持有时间。

基于数据库的锁:

  • 利用数据库的事务和锁机制,例如使用SELECT ... FOR UPDATE或乐观锁。
  • 缺点:会增加数据库的负担,并且可能受到数据库性能的限制。

基于 Redis 的锁:

  • 利用 Redis 的 SETNX 命令可以实现锁的获取,使用 TTL(过期时间)防止死锁。

  • Redis 的性能高、延迟低,适合用于分布式环境。

  • 示例代码(伪代码):

    if (SETNX("lock_key", "value")) {  
        // 获取锁成功  
        // 处理业务逻辑  
        DEL("lock_key"); // 释放锁  
    }  
    

基于 Zookeeper 的锁:

  • Zookeeper 用于维护分布式协调,通过创建临时节点的方式来实现分布式锁。

  • 当持有锁的客户端崩溃时,临时节点将被删除,其他客户端可以获取到锁。

  • 示例代码(伪代码):

    CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", new ExponentialBackoffRetry(1000, 3));  
    InterProcessSemaphoreV2 lock = new InterProcessSemaphoreV2(client, "/lock_path", 1);  
    lock.acquire(); // 获取锁  
    // 处理业务逻辑  
    lock.release(); // 释放锁  
    

基于 Etcd 的锁:

  • Etcd 是一个分布式键值存储,可以利用其原子性和可用性特性实现分布式锁。

使用分布式锁的场景

  • 防止重复操作: 在多个请求同时处理时,防止同一操作被多次执行,如创建订单、发放优惠等。
  • 限流: 对某些资源的访问频率进行限制。
  • 数据一致性的保障: 确保在执行某些操作时数据状态的准确性,比如库存扣减时。

锁的分类

可重入锁和不可重入锁

Java的synchronized关键字和ReentrantLock类提供的锁都是可重入的。

可重入锁可以有效避免因先后获取同一把锁而导致的死锁。

可重入锁也叫作递归锁,指的是一个线程可以多次抢占同一个锁。

例如,线程A在进入外层函数抢占了锁之后,当线程A继续进入内层函数时,线程A依然可以再抢到这把锁

image-20241016112717411

不可重入锁与可重入锁相反,指的是一个线程只能抢占一次同一个锁

悲观锁和乐观锁

synchronized和ReentrantLock都是悲观锁.

悲观锁,每次进入临界区操作数据的时候都认为别的线程会修改,有安全问题,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直等到拿到锁。

总体来说,悲观锁适用于写多读少的场景,遇到高并发写时性能高。

乐观锁,每次进入临界区操作数据的时候都认为别的线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败就要重复 “读-比较-写”的操作。

总体来说,乐观锁适用于读多写少的场景,遇到高并发写时性能低。

扩展:

mybatis-plus实现了乐观锁(版本控制),@Version标注一个字段int类型的version字段。

约定:任何线程修改了数据都必须将version+1.

例:
get oldVersio where id=1得到0

update xxx set ageage+1,version version +1 where id 1 and version oldVersion

如果update操作返回的影响行数=0,表示本次修改的过程中,其他的线程先行一步,将数据值进行了修改,当前线程就应该重试(重新获取oldVersion,重新执行update,直到执行成功为止)。

缺点:乐观锁的这种重试机制(自旋),可能会导致cpu的利用率标高(每次修改都失败了)

公平锁和非公平锁

//synchronized 是非公平锁,ReentrantLock既可以实现公平锁也可以非公平锁。

//ReentrantLock默认是非公平锁
static Lock lock = new ReentrantLock(true); //公平锁
static Lock lock = new ReentrantLock(false); //非公平锁

公平锁,多个线程按照申请锁的顺序来获取锁,先到先得。有一个等待锁的队列,申请锁的线程进入到队列中去队等待,队列中的第一个线程才能获得锁。

非公平锁,多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待,但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

在选择公平锁和非公平锁时,通常需要考虑性能和公正性之间的权衡。如果你的应用程序对锁的公平性要求较高,并且能够承受一些额外的性能开销,可以选择公平锁;否则,默认的非公平锁一般情况下更为高效。

非公平锁的吞吐量更高(效率更高),非公平锁下,可以有效的避免一些线程的阻塞(挂起)和唤醒(减少系统开销)

可中断锁和不可中断锁

Java中的synchronized关键字实现的就是一种不可中断锁的机制。

Java中的ReentrantLock类支持可中断锁,即线程在等待获取锁时可以被中断。

当一个线程通过调用lock锁的lock.locklnterruptibly()方法去获取锁时,其他的线程中,可以调用当前线程的thread.interrupt()将等待锁的线程进行中断

lock.locklnterruptibly();

lock.lockInterruptibly() 是 Java ReentrantLock 类中的一个方法,用于尝试获取锁,并且可以响应中断。它与 lock() 方法的主要区别在于,当调用 lockInterruptibly() 的线程被中断时,当前阻塞状态会被终止,从而抛出 InterruptedException

这是一个非常有用的特性,尤其是在需要更高的线程控制和更复杂的线程处理场景中,比如在实现某些复杂的并发算法时,或在需要协作停止长时间等待的线程时。

可中断锁:线程在等待获取锁的过程中,如果收到中断信号,会立即响应中断,并结束等待状态,这种机制允许线程在等待期间执行其他任务或进行其他操作。

不可中断锁:线程一旦开始等待获取锁,除非成功获取到锁,否则不会被任何中断信号所打断。线程会一直等待,直到成功获取到锁或者线程本身被终止。在等待期间,线程无法响应中断信号,也无法执行其他任务。

共享锁和独占锁

独占锁(Exclusive Lock)和共享锁(Shared Lock)。

synchronized 是一种独占锁。
ReentrantLock可以是独占锁也可是共享锁。

共享锁(ReentrantReadWriteLock)
特点:ReentrantReadWriteLock 提供了一种读写锁机制,允许多个线程同时读取,而不允许写线程在有读线程时执行。也就是说,当有一个线程在写时,所有其他线程(读或写)都将被阻塞。

使用场景:适用于读操作频繁而写操作相对少的情况。

独占锁也叫互斥锁。特点就是同一时刻,多个线程中只能有一个线程获取到锁,当一个线程获得一个独占锁后,其他线程将无法获取该锁,直到该线程释放锁。

共享锁,允许多个线程同时获取同一个锁。

扩展

进程内锁和分布式锁

Lock和synchronized都是进程内锁,只有同一个进程内的多个线程才可以使用Lock和synchronized保证正并发安全。

如果需要跨进程的多个线程保证并发安全,这里就需要分布式锁,通过可以使用redis来实现分布式锁。就是使用redis中setnx命令(如果当前key在redis中不存在,则set成功,否则set失败)。

自旋锁

加锁的操作并不会阻塞,而是通过重试的方式,重新获取锁

image-20241016142738034

自旋锁优点,减少线程的阻塞起和唤醒的系统开销。
缺点,如果长时间没有获取到锁,则会导致cpu利用率标高。

ReentrantLock常用API

1、构造方法

  • ReentrantLock():创建一个非公平锁的实例。
  • ReentrantLock(boolean fair):如果 fairtrue,则创建一个公平锁;如果为 false,则创建一个非公平锁。

2. 锁的获取和释放

  • void lock():获取锁,如果锁已经被其他线程持有,则当前线程会被阻塞,直到锁被释放。

  • void lockInterruptibly():获取锁,【如果锁已经被其他线程持有,则当前线程会被阻塞,直到锁被释放】。但是,支持中断;如果当前线程在等待锁的过程中被中断,将抛出 InterruptedException

  • boolean tryLock():尝试获取锁,如果锁当前未被其他线程持有,则获取成功并返回 true;否则返回 false

  • boolean tryLock(long timeout, TimeUnit unit):尝试获取锁,在指定的时间内。如果在超时之前获取成功返回 true,否则返回 false

  • void unlock():释放锁。必须在获取锁后调用此方法,否则会抛出 IllegalMonitorStateException

3. 查询状态

  • boolean isLocked():检查当前锁是否被任何线程持有。
  • boolean isHeldByCurrentThread():检查当前线程是否持有此锁。
  • int getHoldCount():返回当前线程持有此锁的次数。
  • int getQueueLength():返回等待获取此锁的线程数。
  • boolean hasQueuedThreads():判断是否有其他线程在等待获取此锁。

4. 其他

  • Condition newCondition():创建一个与此锁相关联的 Condition 实例。可以用来实现更复杂

Lock的读写锁

ReentrantReadWriteLock(读写锁)

java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。

用于提供读写锁的功能,从而允许控制对共享资源的访问。它的主要思想是,允许多个线程同时读取(共享访问),但在写入操作时必须独占访问,这样可以提高并发性能,尤其是在读操作较多、写操作较少的场景中。

读写锁的特点:

读锁(Shared Lock):多个线程可以同时获取读锁,只要没有任何线程持有写锁。

写锁(Exclusive Lock):只有一个线程可以持有写锁,同时在持有写锁的情况下,不能有任何其他线程获取读锁或写锁。

公平性

  • 公平锁:按照请求的顺序分配锁。
  • 非公平锁:可能会更快,但可能会导致某些线程长时间等待。
1、写写不可并发

2、读写不可并发

3、写读不可并发

4、读读可以并发

只要写线程出现,多个线程就开始使用写锁(独占锁),保证写的安全和读的一致性。
Lock writeLock().lock():返回写锁。

如果只有读线程,多个线程使用读锁(共享锁),保证并发读的效率。
Lock readLock().lock():返回读锁。

常用方法

  • 构造方法
    • ReentrantReadWriteLock():创建一个非公平的读写锁。
    • ReentrantReadWriteLock(boolean fair):如果 fairtrue,则创建公平锁;如果为 false,则创建非公平锁。
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); //创建一个非公平的读写锁
  • 获取锁
    • Lock readLock():返回读锁。
    • Lock writeLock():返回写锁。

锁降级

锁降级:锁降级就是从写锁降级成为读锁

在当前线程拥有写锁的情况下,之后再获取到读锁,随后释放写锁的过程就是锁降级。

锁降级使用场景:当多线程情况下,更新完数据后,立刻查询刚更新完的数据。

image-20241016145914216

注意:ReentrantReadWriteLock 支持锁降级,不支持锁升级。

线程间通信

线程间通讯概述:线程间通信指的就是让多个线程进行协同工作,来完成特定的任务。

线程间通信,方案一: synchronized + wait() + notify()/notifyAll() 方法二:Lock + Condition

单生产单消费

需求分析

需求:两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。

代码演示

// 线程
class ShareDataOne {
    
    private Integer number = 0;

    // 加1方法
    public synchronized void increment() throws InterruptedException {  
        // 1. 判断
        if (number != 0) {
            this.wait();//释放该对象的锁,并使自己进入等待状态
        }

        // 2. 干活
        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知(由于方法执行结束,所以自动释放锁)
        this.notifyAll();//所有等待的线程被唤醒,然后它们会竞争获取对象的锁
    }

    // 减1方法
    public synchronized void decrement() throws InterruptedException {
        
        // 1. 判断
        if (number != 1) {
            this.wait();
        }
        // 2. 干活
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        this.notifyAll();
    }
}
public class NotifyWaitDemo {

    public static void main(String[] args) {
        
        // 创建ShareDataOne对象
        ShareDataOne shareDataOne = new ShareDataOne();

        // 单线程对number变量进行+1操作10次
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AAA").start();
  
        // 单线程对number变量进行-1操作10次
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BBB").start();

    }
}

sleep和wait的区别

区别:

1、sleep是Thread类中方法,wait方法是Object类中的方法

2、sleep方法不会释放同步锁,wait方法会释放同步锁

多生产多消费

产生虚假唤醒问题

当前线程获取到锁之后,并不能直接去操作共享数据,还需要判断条件是否成立(判断是否可以去操作共享数据),判断出当前线程不具备操作共享数据的资格,此时当前线程就要等待并释放锁。当前线程之后再次获取到锁时,应该再次判断是否具有操作共享数据的资格,如果依然不具备,继续等待并释放锁。

条件判断时:如果使用f,当等待状态的线程再次获取到锁之后,并没有进行重新的条件判断,而是直接向下执行。这就会产生虚假唤醒问题

解决办法:if改成while,一个等待状态的线程,当再次获取到锁时,就是自旋,重新进行条件判断。

image-20241017105119095

解决办法==》if换成while

// 线程
class ShareDataOne {

    private Integer number = 0;

    // 加1方法
    public synchronized void increment() throws InterruptedException {

        // 1. 判断
        while (number != 0) {
            this.wait();
        }

        // 2. 干活
        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        this.notifyAll();
    }

    // 减1方法
    public synchronized void decrement() throws InterruptedException {

        // 1. 判断
        while (number != 1) {
            this.wait();
        }

        // 2. 干活
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        this.notifyAll();

    }

}

Lock+Condition实现通信

condition.await(); 线程等待

condition.signalAll(); 唤醒在此Condition上等待的所有线程

condition.signal(); 唤醒在此Condition上等待的任意一个线程

// 线程
class ShareDataOne {

    private Integer number = 0;
    
    //创建Lock锁
    private static final ReentrantLock reentrantLock = new ReentrantLock() ;
    
    //创建Lock锁对应的condition,用于线程等待和唤醒
    private static final Condition condition = reentrantLock.newCondition() ;

    // 加1方法
    public void increment() throws InterruptedException {
		
        //获取锁
        reentrantLock.lock();           		
        

        // 1. 判断
        while (number != 0) {
            condition.await();//释放锁
        }

        // 2. 干活
        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知。唤醒所有等待该Condition的线程(不会释放锁,所以下一步需要unlock)
        condition.signalAll();

        // 释放锁
        reentrantLock.unlock();
    }

    // 减1方法
    public void decrement() throws InterruptedException {

        reentrantLock.lock();           // 获取锁

        // 1. 判断
        while (number != 1) {
            condition.await();
        }

        // 2. 干活
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        condition.signalAll();

        // 释放锁
        reentrantLock.unlock();

    }

}

注意:每个线程都可以创建独立的condition对象(多个线程使用同一个lock,但是使用各自独立的condition)。这样可以实现指定唤醒

实现多线程的有序执行(有序唤醒)

涉及到的知识点,为每个线程创建独立的condition对象(多个线程使用同一个lock,但是使用各自独立的condition),当某个线程使用condition1对象的await方法之后,将来当前线程能够被唤醒必须也得使用condition1对象的signalAll()方法。

private static final Lock lock = new ReentrantLock();  
private static final Condition condition1 = lock.newCondition();  
private static final Condition condition2 = lock.newCondition();  
private static final Condition condition3 = lock.newCondition(); 

//等待
condition1.await();
//唤醒
condition1.await();

线程状态

java.lang.Thread类内部定义了一个枚举类用来描述线程的六种状态:

public enum State {
    /* 新建 */
    NEW , 

    /* 可运行状态 */
    RUNNABLE , 

    /* 阻塞状态 */
    BLOCKED , 

    /* 无限等待状态 */
    WAITING , 

    /* 计时等待 */
    TIMED_WAITING , 

    /* 终止 */
    TERMINATED;
    
}

每种线程状态的含义:

线程状态 具体含义
NEW 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
RUNNABLE 调用线程对象的start方法,此时线程进入了RUNNABLE状态。(就绪状态)
线程一经启动并不是立即得到执行,线程的运行与否要听令于CPU的调度,可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的调度。
BLOCKED 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;
当该线程获取到锁时,该线程将变成Runnable状态。
WAITING 一个正在等待的线程的状态,也称之为等待状态。
造成线程等待的原因有两种,分别是调用wait()、join()方法。
处于等待状态的线程,正在等待其他线程去执行一个特定的操作。
例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();
一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING 一个在限定时间内等待的线程的状态,也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:sleep(long)、wait(long)、join(long)。
TERMINATED 一个完全运行完成的线程的状态。也称之为终止状态、结束状态。

1571652681276

posted @ 2024-11-05 21:11  CH_song  阅读(129)  评论(0)    收藏  举报