18 - Java 线程

简介

进程(process)

对一个程序的运行状态, 以及在运行中所占用的资源(内存, CPU)的描述;

一个进程可以理解为一个程序; 但是反之, 一个程序就是一个进程, 这句话是错的。

进程的特点:

  • 独立性: 不同的进程之间是相互独立的, 相互之间资源不共享;
  • 动态性: 进程在程序中不是静止不动的, 而是一直是活动状态;
  • 并发性: 多个进程可以在一个处理器上同时运行, 互不影响;

线程(thread)

是进程的一个组成部分, 一个进程中可以包含多个线程, 每一个线程都可以去处理一项任务;

进程在开辟的时候, 会自动的创建一个线程, 这个线程叫做 主线程;

一个进程包含多个线程, 且至少是一个, 如果一个进程中没有线程了, 这个进程会被终止;

多线程的执行是抢占式的, 多个线程在同一个进程中并发执行任务, 其实就是CPU快速的在不同的线程之间进行切换。

进程与线程的关系和区别

  • 一个程序运行后, 至少有一个进程;
  • 一个进程包含多个线程, 至少一个线程;
  • 进程之间是资源不共享的, 但是线程之间是资源共享的;
  • 系统创建进程的时候, 需要为进程重新分配系统资源, 而创建线程则容易很多, 因此使用多线程在进行并发任务的时候, 效率比多进程高;

区别并行和并发

并行:指在同一时刻,有多条指令在多个处理器上同时执行 ;

并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行;

创建线程

实现 Runnable 接口

步骤
  • 声明类 implements Runnable 接口;
  • 重写 Runnable 接口中的 run() 方法,线程执行体;
  • 创建 Runnable 实现类的对象,并把这个对象作为 Thread 的 target 进行 Thread 对象的实例化,这个 Thread 对象才是真正的线程对象 ;采用 Runnable 接口的方式创建的多个线程可以共享同一个 target 对象的实例变量;
  • 调用 start 方法, 来启动线程 ;
java 复制代码
//自定义线程执行任务类
public class MyRunnable implements Runnable{
    //定义线程要执行的run方法逻辑
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我的线程:正在执行!"+i);
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        //创建线程执行目标类对象
        Runnable runn = new MyRunnable();
        //将Runnable接口的子类对象作为参数传递给Thread类的构造函数
        Thread thread = new Thread(runn);
        Thread thread2 = new Thread(runn);
        //开启线程
        thread.start();
        thread2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:正在执行!"+i);
        }
    }
}

继承 Thread 类

Thread 是所有线程类的父类, 实现了对线程的抽取和封装;

方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

步骤
  • 继承 Thread 类, 写一个 Thread 的子类 ;
  • 在子类中, 重写父类中的 run 方法, run 方法就代表了这个线程需要处理的任务(希望这个线程处理什么任务,就把这个任务写到 run 方法中), 因此, run 方法也被称为线程执行体 ;
  • 实例化这个子类对象,即是开辟了一个线程 ;
  • 调用 star t方法, 来执行这个线程需要处理的任务(启动线程);
java 复制代码
//自定义线程类
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 Demo{
    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);
        }
    }
}

// 使用匿名内部类的方式创建
new Thread() {
    public void run() {
            
    }
}.start();

通过 Callable 和 Future 创建线程

Callable 接口
  • Callable 接口提供了一个 call() 方法(可以有返回值,可以声明抛出异常)可以作为线程执行体,Callable 接口里的泛型形参类型与 call() 方法返回值类型相同。
  • V call():计算结果,如果无法计算结果,则抛出一个异常。
Future 接口
  • Future 接口代表 Callable 接口里 call() 方法的返回值,表示异步计算的结果;
  • Future 接口的常用方法 V get():返回 Callable 任务里 call() 方法的返回值,如果计算抛出异常将会抛出 ExecutionException 异常,如果当前的线程在等待时被中断将会抛出 InterruptedException 异常(调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值);
  • V get(long timeout, TimeUnit unit):返回 Callable 任务里 call() 方法的返回值,该方法让程序最多阻塞 timeout 和 unit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常
  • boolean cancel(boolean maylnterruptlfRunning):试图取消该 Future 里关联的 Callable 任务 boolean isCancelled():如果在 Callable 任务正常完成前被取消,则返回 true ;
  • boolean isDone():如果 Callable 任务已完成,则返回 true;
FutureTask 类
  • FutureTask 实现类实现了 RunnableFuture 接口(RunnableFuture接口继承了 Runnable 接口和Future 接口)
  • 构造器:FutureTask(Callable callable)、FutureTask(Runnable runnable, V result)(指定成功完成时 get 返回给定的结果为 result)
步骤
  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值;
  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值;
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程;
  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值;
java 复制代码
public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

创建线程的三种方式对比

继承 Thread 类

  • 线程类已经继承了 Thread 类,不能再继承其它父类
  • 如果需要访问当前线程,直接使用 this 即可获得当前线程
  • 多个线程之间无法共享线程类中的实例变量

实现 Runnable、Callable 接口的方式创建多线程

  • 线程类只是实现了 Runnable 接口,还可以继承其它类
  • 如果需要访问当前线程,则必须使用 Thread. currentThread() 方法
  • 所创建的 Runnable 对象只是线程的 target,而多个线程可以共享同一个 target 对象的实例变量,所以适合多个相同线程来处理同一份资源的情况

strart() 和 run() 的区别

start() 方法会开辟一个线程, 然后在这个新的线程中执行 run() 中的逻辑,但是如果直接调用 run(),则表示需要在当前的线程中执行逻辑。

Runnable与Callable

相同点

  • 都是接口,都可以编写多线程程序,都采用Thread.start()启动线程;

不同点

  • Runnable没有返回值;Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果;
  • Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛;
  • Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。;

线程的生命周期

操作系统中线程的生命周期

操作系统中线程的生命周期通常包括以下五个阶段:

  • 新建状态(New):使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态(Runnable):当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态(Running):如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态(Blocked):如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当 sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 终止状态(Terminated):一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

Java 中线程的生命周期

Java 中线程的生命周期可以细化为以下几个状态:

  • New(初始状态):线程对象创建后,但未调用 start()方法。
  • Runnable(可运行状态):调用 start()方法后,线程进入就绪状态,等待 CPU 调度。
  • Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。
  • Waiting(等待状态):线程进入等待状态,需要被显式唤醒才能继续执行。
  • Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会被唤醒。
  • Terminated(终止状态):线程执行完成或因异常退出。

线程优先级

Java 程序中的每个线程并不是平均分配CPU时间的,为了使得线程资源分配更加合理,Java采用的是抢占式调度方式,优先级越高的线程,优先使用CPU资源。

希望CPU花费更多的时间去处理更重要的任务,而不太重要的任务,则可以先让出一部分资源。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY ),默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

线程的优先级一般分为以下三种:

  • MIN_PRIORITY 最低优先级
  • MAX_PRIORITY 最高优先级
  • NOM_PRIORITY 常规优先级
java 复制代码
public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("线程开始运行!");
    });
    t.start();
    t.setPriority(Thread.MIN_PRIORITY);  //通过使用setPriority方法来设定优先级
}

优先级越高的线程,获得CPU资源的概率会越大,并不是说一定优先级越高的线程越先执行。

线程安全

保证多线程环境下共享的、可修改的状态的正确性。

线程安全需要保证几个基本特性:

常用的线程安全措施

  • 同步锁:通过 synchronized 关键字或 ReentrantLock 实现对共享资源的同步控制。
  • 原子操作类:Java 提供的 AtomicInteger、AtomicReference 等类确保多线程环境下的原子性操作。
  • 线程安全容器:如ConcurrentHashMap、CopyOnWriteArrayList 等,避免手动加锁。
  • 局部变量:线程内独立的局部变量天然是线程安全的,因为每个线程都有自己的栈空间(线程隔离)。
  • ThreadLocal:类似于局部变量,属于线程本地资源,通过线程隔离保证了线程安全。

线程同步

  • 原子操作(atomic operation):不可被中断的一个或一系列操作。
  • 只需要对那些会改变共享资源的、不可被中断的操作进行同步即可。
  • 保证在任一时刻只有一个线程可以进入修改共享资源的代码区,其它线程只能在该共享资源对象的锁池中等待获取锁。
  • 在 Java 中,每一个对象都拥有一个锁标记(monitor),也称为监视器。
  • 线程开始执行同步代码块或同步方法之前,必须先获得对同步监视器的锁定才能进入同步代码块或者同步方法进行操作。
  • 当前线程释放同步监视器:当前线程的同步代码块或同步方法执行结束,遇到 break 或 return 语句,出现了未处理的 Error 或 Exception,执行了同步监视器对象的 wait() 方法或 Thread.join() 方法。
  • 当前线程不会释放同步监视器:当前线程的同步代码块或同步方法中调用 Thread. sleep()、Thread.yield() 方法其它线程调用了该线程的 suspend() 方法。

synchronized

同步代码块

通常推荐使用可能被并发访问的共享资源作为同步监视器对象。

java 复制代码
//同步代码块
synchronized(同步监视器对象) { // 得到对象的锁,才能操作同步代    码
    需要被同步代码;
}
同步方法
  • 使用 synchronized 关键字来修饰某个方法,就相当于给调用该方法的对象加了锁
  • 对于实例方法,同步方法的同步监视器是 this,即调用该方法的对象
  • 对于类方法,同步方法的同步监视器是当前方法所在类的字节码对象(如 ArrayUtil.class)
  • 不要使用 synchronized 修饰 run() 方法,而是把需要同步的操作定义在一个新的同步方法中,再在 run() 方法中调用该方法
java 复制代码
//同步方法
public synchronized void m(String name){
    需要被同步代码;
}
java 复制代码
public class SellTicket {
    public static void main(String[] args) {

        SellTicket03 sellTicket03 = new SellTicket03();
        new Thread(sellTicket03).start();//第1个线程-窗口
        new Thread(sellTicket03).start();//第2个线程-窗口
        new Thread(sellTicket03).start();//第3个线程-窗口

    }
}


//实现接口方式, 使用synchronized实现线程同步
class SellTicket03 implements Runnable {
    private int ticketNum = 100;//让多个线程共享 ticketNum
    private boolean loop = true;//控制run方法变量
    Object object = new Object();


    //同步方法(静态的)的锁为当前类本身
    //1. public synchronized static void m1() {} 锁是加在 SellTicket03.class
    //2. 如果在静态方法中,实现一个同步代码块.
    /*
        synchronized (SellTicket03.class) {
            System.out.println("m2");
        }
     */
    public synchronized static void m1() {
    

    }
    public static  void m2() {
        synchronized (SellTicket03.class) {
            System.out.println("m2");
        }
    }
    
    //1. public synchronized void sell() {} 就是一个同步方法
    //2. 这时锁在 this对象
    //3. 也可以在代码块上写 synchronize ,同步代码块, 互斥锁还是在this对象
    public /*synchronized*/ void sell() { //同步方法, 在同一时刻, 只能有一个线程来执行sell方法

        synchronized (/*this*/ object) {
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                loop = false;
                return;
            }

            //休眠50毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                    + " 剩余票数=" + (--ticketNum));//1 - 0 - -1  - -2
        }
    }

    @Override
    public void run() {
        while (loop) {

            sell();//sell方法是一共同步方法
        }
    }
}

synchronized 使用方式

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结

  • synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
  • synchronized 关键字加到实例方法上是给对象实例上锁。
  • 尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能。

锁(Lock)

锁的分类名词

锁的分类名词,有的是指锁的状态、有的指锁的特性、有的指锁的设计。

公平锁/非公平锁

公平锁是指多个线程按照申请所的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能是后申请的线程比先申请的线程有限获取锁。有可能,会造成优先级反转或者饥饿现象。

非公平锁的优点在于吞吐量比公平锁大。

在Java中,synchronized是一种非公平锁。

ReentrantLock则可以通过构造函数指定该锁是否公平锁,默认是非公平锁。ReentrantLock通过AQS来实现线程调度,实现公平锁。

可重入锁

可重入锁又名递归锁,是指在同一个线程在持有锁的前提下,再遇到需要申请同一个锁的情况时可自动获取锁。而非可重入锁遇到这种情况会形成死锁,也就是"我申请我已经持有的锁,我不会释放锁也申请不到锁,所以形成死锁。"

Java中,synchronized在JDK 1.6优化后,属于可重入锁。

ReentrantLock,即Re entrant Lock,可重入锁。

java 复制代码
synchronized void A(){
    System.out.println("A获取锁!");
    B();
}
synchronized void B(){
    System.out.println("B锁重入成功!");
}

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有,共享锁是指该锁可被多个线程所持有。

在Java中,

synchronized属于独享锁。

ReentrantLock也属于独享锁。

而Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证高效的并发读,但是读写、写读、写写的过程是互斥的,防止脏读、数据丢失。独享锁和共享锁也是通过AQS实现的。

互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是ReentraLock。

读写锁在Java中的具体实现就是ReadWriteLock。

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁认为对同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式,悲观地认为,不加锁的并发操作一定会出现问题。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断更新的方式更新数据,乐观地认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

悲观锁在Java中的使用就是利用各种锁。

乐观锁在Java中的使用就是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为segment,它类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道它要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不死放在一个酚酸中,就实现了真正的并行插入。

但是在统计size的时候,即获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的就是细化锁的粒度,当操作不需要更新整个数组的时候,就针对数据的一项进行加锁操作。

偏向锁/轻量级锁/重量级锁

这三种所是指锁的状态,并且是针对synchronized。在 java 6通过引入锁的升级机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

在Java中。自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

死锁

死锁是一个经典的多线程问题。避免死锁重要吗?一旦一组Java线程发生死锁,那么这组线程及锁涉及其已经持有锁的资源区将不再可用--除非重启应用。

死锁是设计上的bug,它并不一定发生,但它有可能发生,而且发生的情况一般出现在极端的高负载的情况下。

那么有什么办法为了避免死锁?

  1. 让程序每次至多只能获得一个锁。但这个在多线程环境下通常不现实。
  2. 设计时考虑清楚锁的顺序,尽量减少潜在的加锁交互数量
  3. 避免使用synchronized,为线程等待设置等待上限,避免无限等待。

Java 中锁的使用

JDK 1.8 后的锁:

  • synchronized :非公平锁;可重入锁;独享锁;偏向锁/轻量级锁/重量级锁;
  • ReentrantLock:非公平锁;可重入锁;独享锁;互斥锁;
  • ReadWriteLock:读锁是共享锁,写锁是独享锁;读写锁;
  • StampedLock:读写锁;乐观锁;

在Java中,有多种方式可以实现线程锁,以下是一些常见的方式:

使用 synchronized 关键字

synchronized 关键字可以用来实现方法或者代码块的同步,确保同一时刻只有一个线程执行被同步的代码。

java 复制代码
public synchronized void synchronizedMethod() {
    // 需要同步的代码
}
 
// 或者使用同步代码块
 
public void nonSynchronizedMethod() {
    synchronized (this) {
        // 需要同步的代码
    }
}

使用 ReentrantLock

ReentrantLock 是一个可重入的锁,它比 synchronized 更灵活,提供了更多的功能,比如等待可中断、可实现公平锁等候时间最长的线程优先获取锁等。

java 复制代码
import java.util.concurrent.locks.ReentrantLock;
 
public class LockExample {
    private final ReentrantLock lock = new ReentrantLock();
 
    public void lockMethod() {
        lock.lock();
        try {
            // 需要同步的代码
        } finally {
            lock.unlock();
        }
    }
}

使用 StampedLock

StampedLock 是JDK8引入的新的锁机制,它提供了一种乐观锁的实现,相比于 ReentrantLock,它的读写锁机制更加灵活,可以有更高的并发。

java 复制代码
import java.util.concurrent.locks.StampedLock;
 
public class StampedLockExample {
    private final StampedLock lock = new StampedLock();
 
    public void readMethod() {
        long stamp = lock.tryReadLock();
        try {
            // 读操作
        } finally {
            lock.unlockRead(stamp);
        }
    }
 
    public void writeMethod() {
        long stamp = lock.writeLock();
        try {
            // 写操作
        } finally {
            lock.unlockWrite(stamp);
        }
    }
}

使用 ReadWriteLock

ReadWriteLock 接口定义了读写锁的两个锁,一个是只对数据进行读操作的锁,另一个是只对数据进行写操作的锁。

java 复制代码
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class ReadWriteLockExample {
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();
 
    public void readMethod() {
        r.lock();
        try {
            // 读操作
        } finally {
            r.unlock();
        }
    }
 
    public void writeMethod() {
        w.lock();
        try {
            // 写操作
        } finally {
            w.unlock();
        }
    }
}

线程控制

Java中,线程控制主要涉及到以下几个方面:

  • 线程的启动
java 复制代码
Thread t1 = new Thread(new Runnable() {
    public void run() {
        System.out.println("线程启动");
    }
});
t1.start();
  • 线程的等待
java 复制代码
Thread t2 = new Thread(new Runnable() {
    public void run() {
        try {
            Thread.sleep(1000); // 休眠1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程等待结束");
    }
});
t2.start();
  • 线程的中断
java 复制代码
Thread t3 = new Thread(new Runnable() {
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            // 执行任务
        }
        System.out.println("线程被中断");
    }
});
t3.start();
t3.interrupt();
  • 线程的优先级控制
java 复制代码
Thread t4 = new Thread(new Runnable() {
    public void run() {
        System.out.println("线程的优先级: " + Thread.currentThread().getPriority());
    }
});
t4.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
t4.start();
  • 线程的合并
java 复制代码
Thread t5 = new Thread(new Runnable() {
    public void run() {
        System.out.println("被合并的线程");
    }
});
t5.start();
t5.join(); // 等待t5线程结束,然后继续执行主线程
  • 线程的同步与协调
java 复制代码
final Object lock = new Object();
 
Thread t6 = new Thread(new Runnable() {
    public void run() {
        synchronized (lock) {
            try {
                lock.wait(); // 线程等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("线程被唤醒");
    }
});
 
Thread t7 = new Thread(new Runnable() {
    public void run() {
        synchronized (lock) {
            lock.notify(); // 唤醒等待的线程
        }
    }
});
 
t6.start();
// 确保t6线程已经开始等待
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
t7.start();

用户线程和守护线程

  • 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束 。
  • 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束setDaemon(True) 。
  • 常见的守护线程:垃圾回收机制 。

线程组

  • ThreadGroup 类,表示一个线程的集合,可以对一组线程进行集中管理(同时控制这批线程)
  • 在默认情况下,子线程和创建它的父线程处于同一个线程组内

线程让步

  • 让执行的线程暂停,进入就绪状态。
  • 当某个线程调用了 yield() 方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。

同步机制与 ThreadLocal

  • 如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制。
  • 如果仅仅需要隔离多个线程之间的共享冲突,则可以使用 ThreadLocal。

线程通信

线程通信机制

|------|----------------------------------------|--------------------------------------|
| 并发模型 | 通信机制 | 同步机制 |
| 共享内存 | 线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信 | 同步是显式进行的,即必须显式指定某个方法或某段代码需要在线程之间互斥执行 |
| 消息传递 | 线程之间通过显式的发送消息来达到交互目的,如 Actor 模型 | 由于消息的发送必须在消息的接收之前,因此同步是隐式进行的 |

  • Java 的线程间通过共享内存的方式进行通信

使用 Object 类中的方法

  • Object 类中用于操作线程通信的实例方法
    • wait():调用该方法的当前线程会释放对该同步监视器(调用者)的锁定,JVM 把该线程存放到等待池中,等待其他的线程唤醒该线程(该方法声明抛出了 InterruptedException 异常)(为了防止虚假唤醒,此方法应始终在循环中使用,即被唤醒后需要再次判断是否满足唤醒条件)
    • notify():调用该方法的当前线程唤醒在等待池中的任意一个线程,并把该线程转到锁池中等待获取锁
    • notifyAll():调用该方法的当前线程唤醒在等待池中的所有线程,并把该线程转到锁池中等待获取锁
  • 这些方法必须在同步块中使用,且只能被同步监视器对象来调用,否则会引发 IllegalMonitorStateException 异常
java 复制代码
public class ShareResource {
    // 标识数据是否为空(初始状态为空)
    private boolean empty = true;
    // 需要同步的方法
    public synchronized void doWork() {
        try {
            while (!empty) { // 不空,则等待
                this.wait();
            }
            ... // TODO
            empty = false; // 修改标识
            this.notifyAll(); // 通知其它线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用 Condition 接口中的方法

  • java.util.concurrent.locks 包中,Condition 接口中的 await()、signal()、signalAll() 方法替代了 Object 监视器方法的使用(await() 方法也声明抛出了 InterruptedException 异常)
  • 通过 Lock 对象调用 newCondition() 方法,返回绑定到此 Lock 对象的 Condition 对象
java 复制代码
public class ShareResource {
    // 创建使用 private final 修饰的锁对象
    private final Lock lock = new ReentrantLock();
    // 获得指定 Lock 对象对应的 Condition
    private final Condition cond = lock.newCondition();
    // 标识数据是否为空(初始状态为空)
    private boolean empty = true;
    // 需要同步的方法
    public void doWork() {
        lock.lock(); // 进入方法后,立即获取锁
        try {
            while(!empty) { // 判断是否方法阻塞
                cond.await();
            }
            ... // TODO
            empty = false; // 修改标识
            cond.signalAll(); // 通知其它线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 使用 finally 块释放锁
        }
    }
}

线程常用方法

Thread 对象调用的方法(实例方法)

public void start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法,只能被处于新建状态的线程调用,否则会引发 IllegalThreadStateException 异常。

public void run():如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

public final void setName(String name):改变线程名称,使之与参数 name 相同,为线程设置名字,在默认情况下,主线程的名字为 main,用户启动的多个线程的名字依次为 Thread-0、Thread-1、Thread-2、...、Thread-n 等。

public final void setPriority(int priority):更改线程的优先级(范围是 1~10 之间)。

public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程,on 为"true"时,将该线程设置成守护线程,该方法必须在 start() 之前调用,否则会引发 IllegalThreadStateException 异常。

boolean isDaemon():判断该线程是否为守护线程。

public final void join(long millisec):等待该线程终止的时间最长为 millis 毫秒,而当前正在执行的线程进入阻塞状态(联合线程)(该方法声明抛出了 InterruptedException 异常)。

public void interrupt():中断线程。

public final boolean isAlive():测试线程是否处于活动状态。

Thread 类的静态方法(类方法)

public static void yield():暂停当前正在执行的线程对象,并执行其他线程,转入就绪状态(线程让步)。

public static void sleep(long millisec):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响,并进入阻塞状态(线程睡眠)(该方法声明抛出了 InterruptedException 异常)。

public static boolean holdsLock(Object x):当且仅当,当前线程在指定的对象上保持监视器锁时,才返回 true。

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

public static void dumpStack():将当前线程的堆栈跟踪打印至标准错误流。

线程相关方法

阻塞和唤醒的方法,是 Object 中的方法,必须在synchronized修饰的代码块或者方法内部才可以调用(因为要操作基于某个对象的锁的信息维护)。

wait():让获取 synchronized 锁资源的线程,进入锁的等待池,并且释放锁资源。

notify():让获取 synchronized 锁资源的线程,唤醒等待池中的线程,并且添加到锁池中。

相关推荐
考虑考虑17 分钟前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干26 分钟前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying1 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·1 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
Zz_waiting.2 小时前
Javaweb - 10.4 ServletConfig 和 ServletContext
java·开发语言·前端·servlet·servletconfig·servletcontext·域对象
全栈凯哥2 小时前
02.SpringBoot常用Utils工具类详解
java·spring boot·后端
兮动人2 小时前
获取终端外网IP地址
java·网络·网络协议·tcp/ip·获取终端外网ip地址
呆呆的小鳄鱼2 小时前
cin,cin.get()等异同点[面试题系列]
java·算法·面试