Skip to content

Java并发编程_基础概念

大纲

  • 线程基础概念
  • Java 线程
  • 生命周期
  • 创建线程
  • 线程的调用方法
  • Synchronized 和 Lock

线程

image.png

1、基础概念

进程和线程

进程

在计算机i中,程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。

进程就是用来加载指令、管理内存、管理 IO 的。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

线程

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行,一个进程之内可以分为一到多个线程。

总结:

线程是程序执行的最小单位,它是进程的一部分,共享进程的资源。

进程是操作系统分配资源和调度的基本单位,每个进程至少包含一个线程。

区别:进程有独立的内存空间,而线程共享进程的内存空间;进程间通信(IPC)成本较高,上下文切换较慢,线程间通信和切换成本较低;线程是实现多任务的最小单位,进程是拥有资源的最小单位。

image.png


并行和并发

  • 并行:两个及两个以上的作业在同一 时刻 执行。
  • 并发:两个及两个以上的作业在同一 时间段 内执行。

最关键的点是:是否是 同时 执行


解释2:并行和并发有什么区别?

在单核CPU的情况下:

  • 单核CPU下线程实际还是串行执行的
  • 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。
  • 总结为一句话就是:微观串行,宏观并行

一般会将这种线程轮流使用 CPU的做法 称为并发(concurrent)

在多核 CPU 的情况下:

每个核(core)都可以调度运行线程,这时候线程可以是并行的。

image.png

总结

现在都是多核CPU,在多核CPU下

  • 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
  • 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程

同步和异步

同步和异步的区别

  • 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回。

2、什么是Java 线程

在 JDK 1.2 及以后,Java 线程基于原生线程(Native Threads)实现, JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。

用一句话概括 Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程

在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,一个 Java 线程对应一个系统内核线程。

3、线程的生命周期及五种基本状态

五种基本状态:新建,就绪,阻塞,运行,死亡

image.png

关于Java中线程的生命周期,首先看一下下面这张较为经典的图:

image.png

上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。主要包括:

Java线程具有五中基本状态

  • 新建状态(New)
    • 当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
  • 就绪状态(Runnable)
    • 当调用线程对象的start()方法(t.start();),线程即进入就绪状态。
    • 处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
  • 运行状态(Running)
    • 当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
    • 注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中
  • 阻塞状态(Blocked)
    • 处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。
    • 根据阻塞产生的原因不同,阻塞状态又可以分为三种
      • 1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态
      • 2.同步阻塞 : 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
      • 3.其他阻塞 : 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态(Dead)
    • 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

Java多线程的就绪、运行和死亡状态

就绪状态转换为运行状态:当此线程得到处理器资源;

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。


在 Java 的 Thread 类中,有一个 State 的枚举

    /**
     * A thread state.  A thread can be in one of the following states:
     * <ul>
     * <li>{@link #NEW}<br>
     *     A thread that has not yet started is in this state.
     *     </li>
     * <li>{@link #RUNNABLE}<br>
     *     A thread executing in the Java virtual machine is in this state.
     *     </li>
     * <li>{@link #BLOCKED}<br>
     *     A thread that is blocked waiting for a monitor lock
     *     is in this state.
     *     </li>
     * <li>{@link #WAITING}<br>
     *     A thread that is waiting indefinitely for another thread to
     *     perform a particular action is in this state.
     *     </li>
     * <li>{@link #TIMED_WAITING}<br>
     *     A thread that is waiting for another thread to perform an action
     *     for up to a specified waiting time is in this state.
     *     </li>
     * <li>{@link #TERMINATED}<br>
     *     A thread that has exited is in this state.
     *     </li>
     * </ul>
     *
     * <p>
     * A thread can be in only one state at a given point in time.
     * These states are virtual machine states which do not reflect
     * any operating system thread states.
     *
     * @since   1.5
     * @see #getState
     */
    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

分析流程图

image.png

面试题:1.线程包括哪些状态

新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、时间等待(TIMED_WALTING)、终止(TERMINATED)

面试题:2.线程状态之间是如何变化的

  • 创建线程对象是新建状态
  • 调用了stat()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结束是终止状态
  • 在可执行状态的过程中,如果没有获取 CPU 的执行权,可能会切换其他状态
    • 如果没有获取锁(synchronized 或Iock)进入阻塞状态,获得锁再切换为可执行状态
    • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
    • 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态

4、创建线程

线程创建 🚩

在Java中,有三种常见的线程创建方式:使用Thread类、实现Runnable接口和实现Callable接口。

  1. Thread类:Thread类是 Java 提供的一个线程类,我们可以通过继承Thread类来创建线程。通过重写Thread类的run()方法来定义线程的执行逻辑
java
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程的执行逻辑
    }
}

// 创建线程
MyThread myThread = new MyThread();
myThread.start();
  1. Runnable 接口:Runnable接口是一个函数式接口,我们可以通过实现Runnable接口来创建线程。需要注意的是,Runnable接口并不是一个线程类,而是一个任务,需要通过Thread类来创建线程并执行任务。
java
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程的执行逻辑
    }
}

// 创建线程
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
  1. Callable接口:Callable接口也是一个函数式接口,与Runnable接口类似,可以通过实现Callable接口来创建线程。不同的是,Callable接口的call()方法可以返回一个结果,并且可以抛出异常
java
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 线程的执行逻辑
        return 42; // 返回一个结果
    }
}

// 创建线程
MyCallable myCallable = new MyCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(myCallable);
  • 使用Thread类创建线程是最直接的方式,但是由于Java不支持多继承,所以如果已经有一个父类,就不能再直接使用Thread类创建线程。
  • 实现Runnable接口是一种更加灵活的方式,可以避免单继承的限制,还可以共享数据。
  • Callable接口与Runnable接口类似,但可以返回一个结果,并且可以抛出异常。可以通过ExecutorService的submit()方法来执行Callable任务,并返回一个Future对象,可以通过该对象获取任务的结果。

补充:

Callable + FutureTask

Callable接口与Runnable接口类似,但它可以返回执行结果,并且可以抛出异常。使用Callable时通常配合FutureTask或线程池(ExecutorService)来使用。

示例:

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

public class CallableFutureTaskExample {
    public static void main(String[] args) {
        // 创建Callable对象
        Callable<Integer> callableTask = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("开始计算...");
                Thread.sleep(2000); // 模拟耗时计算过程
                return 123; // 返回计算结果
            }
        };

        // 将Callable与FutureTask关联
        FutureTask<Integer> futureTask = new FutureTask<>(callableTask);

        // 创建线程执行FutureTask
        Thread thread = new Thread(futureTask);
        thread.start();

        // 执行其他任务...
        System.out.println("执行其他任务...");

        try {
            // 获取计算结果,如果计算未完成则阻塞等待
            Integer result = futureTask.get();
            System.out.println("计算结果: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

FutureTaskFuture接口的一个实现,它封装了Callable任务的执行。

通过将Callable实例传递给FutureTask的构造器, 然后,创建一个新的线程来执行FutureTask


一般在项目中会使用线程池创建线程执行任务

使用线程池执行Runnable任务的示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交Runnable任务
        executor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("Asynchronous task");
            }
        });

        // 另一种提交Runnable任务的方式,使用Java 8的Lambda表达式
        executor.submit(() -> {
            System.out.println("Asynchronous task with lambda");
        });

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个固定大小为2的线程池,并提交了两个Runnable任务。使用线程池的好处是可以重用线程,减少线程创建和销毁的开销,以及可以控制并发线程的数量。


共有四种方式可以创建线程,分别是:

  • 继承Thread类
  • 实现 runnable 接口
  • 实现 Callable 接口
  • 线程池创建线程(项目中使用方式)

面试题: runnable和callable有什么区别?

参考回答:

  • 1.Runnable接口 run 方法没有返回值
  • 2.Callable接口 call 方法有返回值,是个泛型,和Future、FutureTaski配合可以用来获取异步执行的结果
  • 3.Callable接口的 call() 方法允许抛出异常;而Runnable接口的runO方法的异常只能在内部消化,不能继续上抛

面试题:线程的run() 和start() 有什么区别?

  • start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run():封装了要被线程执行的代码,可以被调用多次。

写法简化

写法简化(Java 8)

  • 方式一:
java
Thread thread = new Thread(){ 
    @Override
    public void run() { 
        System.out.println("thread run ...");
    }
};

thread.start();

简化后:

java
Thread thread = new Thread(() -> System.out.println("thread run ..."));

thread.start();
  • 方式二:
java
Thread thread = new Thread(new Runnable() { 
    @Override
    public void run() { 
        System.out.println("runnable run ...");
    }
});

thread.start();

简化后:

java
Thread thread = new Thread(() -> System.out.println("runnable run ..."));

thread.start();
  • 方式三:
java
Callable<Integer> callable = new Callable() { 
    @Override
    public Object call() throws Exception { 
        System.out.println("callable run ...");
        return 521;
    }
};
FutureTask futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();

简化后:

java
Thread thread = new Thread(new FutureTask(() -> { 
    System.out.println("callable run ...");
    return 521;
}));

thread.start();

常见面试题

顺序执行

常见的一道面试题:新建T1、T2、T3三个线程,如何保证它们按顺序执行?

要保证三个线程T1、T2、T3按顺序执行,即先执行T1,T1执行完毕后执行T2,T2执行完毕后执行T3,可以使用多种方法来实现。这里提供三种常见的方法:

方法1:使用join()方法

join()方法可以使当前线程等待另一个线程完成后再继续执行。

public class ThreadOrder {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> System.out.println("T1 is running"));
        Thread t2 = new Thread(() -> System.out.println("T2 is running"));
        Thread t3 = new Thread(() -> System.out.println("T3 is running"));
        
        try {
            t1.start();
            t1.join(); // 等待t1执行完毕
            
            t2.start();
            t2.join(); // 等待t2执行完毕
            
            t3.start();
            t3.join(); // 等待t3执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

方法2:使用wait()notify()/notifyAll()方法

通过对象监视器的wait()notify()方法来控制线程的执行顺序。这种方法相对复杂,需要确保wait()notify()调用在同步块中。

public class ThreadOrder {
    public static void main(String[] args) {
        final Object lock = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("T1 is running");
                lock.notify(); // 唤醒等待lock对象的一个线程
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait(); // 使当前线程等待直到lock对象被唤醒
                    System.out.println("T2 is running");
                    lock.notify(); // 唤醒等待lock对象的一个线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        Thread t3 = new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait(); // 使当前线程等待直到lock对象被唤醒
                    System.out.println("T3 is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            t2.start();
            t3.start();
            Thread.sleep(100); // 确保t2和t3启动并进入wait状态
            t1.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

方法3:使用CountDownLatch

CountDownLatch是一个同步辅助类,用于延迟线程的进度直到其达到终止状态。

import java.util.concurrent.CountDownLatch;

public class ThreadOrder {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch1 = new CountDownLatch(1);
        CountDownLatch latch2 = new CountDownLatch(1);
        
        Thread t1 = new Thread(() -> {
            System.out.println("T1 is running");
            latch1.countDown(); // 减少计数
        });
        
        Thread t2 = new Thread(() -> {
            try {
                latch1.await(); // 等待latch1计数到达0
                System.out.println("T2 is running");
                latch2.countDown(); // 减少计数
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        Thread t3 = new Thread(() -> {
            try {
                latch2.await(); // 等待latch2计数到达0
                System.out.println("T3 is running");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        t1.start();
        t2.start();
        t3.start();
    }
}

这三种方法各有特点,可以根据具体场景选择最适合的一种来保证线程按顺序执行。


面试题:新建T1、T2、T3三个线程,如何保证它们按顺序执行?

可以使用线程中的 join 方法解决

image.png

停止线程

如何停止一个正在运行的线程?

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程
    • 打断阻塞的线程(sleep,wait,join) 的线程,线程会抛出 InterruptedException 异常
    • 打断正常的线程,可以根据打断状态来标记是否退出线程

5、线程基本方法

注意:标黄色的方法代表是 static​ 方法,可直接类名调用,无需创建对象。

名称描述注意事项
start()启动一个新线程,
在新的线程运行 run 方法
start 方法只是让线程进入就绪,里面代码不一定立刻
运行(CPU 的时间片还没分给它)。每个线程对象的
start方法只能调用一次,如果调用了多次会出现
IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则
线程启动后会调用 Runnable 中的 run 方法,否则默
认不执行任何操作。但可以创建 Thread 的子类对象,
来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,
最多等待 n 毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String name)修改线程名
getPriority()获取线程优先级
setPriority(int priority)修改线程优先级Java 中规定线程优先级是1~10 的整数,较大的优先级
能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:
NEW, RUNNABLE, BLOCKED, WAITING,
TIMED_WAITING, TERMINATED
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被
打断的线程抛出 InterruptedException,并清除
打断标记;如果打断正在运行的线程,则会设置
打断标记;park 的线程被打断,也会设置打断标记
判断当前线程是否被打断会清除打断标记
isInterrupted()判断当前线程是否被打断不会清除打断标记
isAlive()判断当前线程是否存活
isDaemon()判断当前线程是否是守护线程
setDaemon(boolean on)设置当前线程为守护线程
获取当前正在执行的线程
让当前执行的线程休眠n毫秒,
休眠时让出 CPU 的时间片
给其它线程
提示线程调度器让出当前线程
对 CPU 的使用
主要是为了测试和调试,它的具体的实现依赖于
操作系统的任务调度器

常见面试题

面试题:notify()和notifyAll()有什么区别?

  • notifyAll:唤醒所有wait的线程
  • notify:只随机唤醒一个wait线程
wait 和 sleep

面试题:在java中wait和sleep方法的不同?

  • 共同点
    • wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
  • 不同点
    • 1.方法归属不同
      • sleep(long)是Thread的静态方法
      • 而wait(),wait(long)都是Object的成员方法,每个对象都有
    • 2、醒来时机不同
      • 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
      • wait(long)和wait0还可以被notify唤醒,wait()如果不唤醒就一直等下去
      • 它们都可以被打断唤醒
    • 3.锁特性不同(重点)
      • wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
      • wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
      • 而sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)
class WaitSleepExample {
    private static final Object LOCK = new Object();

    public static void main(String[] args) {
        // 创建一个线程执行等待操作
        Thread waitThread = new Thread(() -> {
            synchronized (LOCK) { // 必须在同步块内调用wait,这表示必须持有对象的锁
                try {
                    System.out.println("Wait Thread: Holding lock, now wait");
                    LOCK.wait(); // 调用wait方法后,当前线程会释放LOCK对象上的锁,并进入等待状态
                    System.out.println("Wait Thread: Exited wait");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 创建一个线程执行睡眠操作
        Thread sleepThread = new Thread(() -> {
            synchronized (LOCK) { // 进入同步块,持有LOCK对象的锁
                try {
                    System.out.println("Sleep Thread: Holding lock, now sleep");
                    Thread.sleep(1000); // 调用sleep方法,但不会释放LOCK对象上的锁
                    System.out.println("Sleep Thread: Exited sleep");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 创建一个线程执行通知操作
        Thread notifyThread = new Thread(() -> {
            synchronized (LOCK) { // 进入同步块,持有LOCK对象的锁
                // 发送通知,告诉等待在LOCK对象上的线程可以继续执行
                LOCK.notifyAll();
                System.out.println("Notify Thread: Sent notification");
            }
        });

        waitThread.start(); // 启动等待线程
        try {
            Thread.sleep(500); // 确保waitThread先执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sleepThread.start(); // 启动睡眠线程
        try {
            Thread.sleep(500); // 确保sleepThread有机会执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notifyThread.start(); // 启动通知线程
    }
}
  • 关于锁的不同处理
    • wait()方法:调用wait()时,线程必须持有对象的锁(这是通过在synchronized块内调用wait()来保证的)。一旦wait()被调用,线程会释放这个对象上的锁,允许其他线程获取这个锁。当其他线程在这个对象上调用notify()notifyAll()时,等待的线程才有机会重新获取锁并继续执行。
    • sleep()方法:与wait()不同,调用sleep()时线程不会释放任何持有的锁。即使是在synchronized块内调用sleep(),当前线程仍然会保持对锁的持有,直到sleep()完成。这意味着,如果一个线程在持有某个对象锁的同时调用了sleep(),其他想要访问这个同步块的线程会被阻塞,直到睡眠线程醒来并退出同步块,释放锁。

→ 使用 sleep 的时候同步块代码线程会被阻塞。

6、Synchronized 和 Lock 的使用

并发编程中,锁是经常需要用到的。这里讲述一下 Synchronized 和 Lock 的使用。

Synchronized 是 Java 并发编程 中很重要的关键字,另外一个很重要的是 volatile。

Syncronized 的目的是一次只允许一个线程进入由他修饰的代码段,从而允许他们进行自我保护。

Lock 是 Java并发编程中很重要的一个接口,它要比 Synchronized 关键字更能直译"锁"的概念,Lock需要手动加锁和手动解锁,一般通过 lock.lock() 方法来进行加锁, 通过 lock.unlock() 方法进行解锁。与 Lock 关联密切的锁有 ReetrantLock 和 ReadWriteLock。

Synchronized

在方法上使用 Synchronized

方法声明时使用,放在范围操作符之后,返回类型声明之前。即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。

java
private int number;
public synchronized void numIncrease(){
  number++;
}

在某个代码段使用 Synchronized

可以在某个代码块上使用 Synchronized 关键字,表示只能有一个线程进入某个代码段。

java
public void numDecrease(Object num){
  synchronized (num){
    number++;
  }
}

使用 Synchronized 锁住整个对象

synchronized后面括号里是一对象,此时线程获得的是对象锁。

java
public void test() {
  synchronized (this) {
    // ...
  }
}

Lock

Lock 是 Java并发编程中很重要的一个接口,相关方法如下:

java
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

常用方法

  • lock()
    • 用来获取锁。如果锁被其他线程获取,则进行等待。
    • 如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁
  • tryLock()
    • 方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,
    • 这个方法无论如何都会立即返回。在拿不到锁时不会一直等待。
  • tryLock(long time, TimeUnit unit)
    • 和tryLock()方法是类似
    • 在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
  • lockInterruptibly()
    • 去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
    • 当两个线程同时通过 lock.lockInterruptibly() 想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用 threadB.interrupt() 方法能够中断线程B的等待过程。
    • 由于 lockInterruptibly() 的声明中抛出了异常,所以 lock.lockInterruptibly() 必须放在try块中或者在调用lockInterruptibly() 的方法外声明抛出 InterruptedException。

代码示例:

lock()

java
Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

tryLock()

javascript
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     }
}else {
    //如果不能获取锁,则直接做其他事情
}

lockInterruptibly()

javascript
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {
     //.....
    }
    finally {
        lock.unlock();
    }
}

一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。 单独调用 interrupt() 方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。


参考: