Java

Java 知识量:11 - 45 - 220

6.2 Java对并发编程的支持><

线程的生命周期- 6.2.1 -

Java线程的生命周期可以分为五个阶段:

  1. 新建(New):线程被创建后,就进入了新建状态。此时,线程仅仅是一个空的线程对象,还没有开始执行。

  2. 就绪(Runnable):当线程调用start()方法后,线程就进入了就绪状态。处于就绪状态的线程,表明已经做好了准备,随时等待CPU调度执行。但是,并不是说执行了start()方法,线程立即就会执行,而是要等待CPU的调度。

  3. 运行(Running):当CPU开始调度处于就绪状态的线程时,线程才得以真正执行,即进入到运行状态。

  4. 阻塞(Blocked):当线程处于阻塞状态时,它无法继续执行。阻塞的情况可能发生在以下几种情况:等待I/O操作完成、等待锁、等待join()方法超时或等待其他线程终止。当这些等待条件满足后,线程会转入就绪状态,等待CPU调度。

  5. 终止(Terminated):当线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

可见性和可变性- 6.2.2 -

在Java中,可见性和可变性是描述对象状态的重要概念。

1、可见性:指当一个线程修改了一个共享变量的值后,其他线程能够立即得知这个修改。Java内存模型在变量修改后将新的变量值同步回主内存,在其他线程读取该变量之前从主内存刷新变量值来实现可见性。主要实现可见性的方式有三种:volatile、synchronized和final。

volatile关键字不能保证操作的原子性。使用synchronized可以保证同一时刻只有一个线程可以执行被synchronized修饰的方法或代码块,从而保证数据的一致性。final关键字修饰的变量在构造器中一旦初始化完成,并且没有发生this逃逸,那么其他线程就能看见final字段的值。

2、可变性:描述了一个对象的状态可以在创建后改变的能力。例如,当一个CPU修改了自己缓冲区的变量数据后,必需通知其他所有的用到此变量的核心,否则就会发生不可见的错误。

互斥和状态保护- 6.2.3 -

互斥(Mutual Exclusion)和状态保护(State Protection)是两个密切相关的概念。它们都是多线程编程中的重要技术,用于确保线程安全和避免数据竞争。

1、互斥(Mutual Exclusion):在Java中,互斥主要通过synchronized关键字实现。当一个线程进入一个synchronized方法或synchronized代码块时,它会获取一个锁,这个锁对应于互斥锁。其他任何尝试进入这个synchronized代码块的线程将会被阻塞,直到第一个线程执行完这个代码块并释放锁。这样,在任何给定的时间点,只有一个线程可以执行这个特定的synchronized代码块,从而避免了多个线程同时访问同一资源的情况,防止数据竞争和状态不一致的问题。

例如:

public synchronized void myMethod() {  
    // 这里的代码只能由一个线程同时执行  
}

2、状态保护(State Protection):状态保护是指将类的状态(即其字段的值)封装在一个安全的地方,以防止多个线程同时修改它。在Java中,这可以通过将类的状态定义为private,并使用public的getter和setter方法(也称为访问器方法)来访问和修改状态。这样,就可以限制其他类对类的状态的访问,防止多线程并发修改导致的状态不一致问题。

例如:

public class MyClass {  
    private int myState;  
      
    public int getMyState() {  
        return myState;  
    }  
      
    public void setMyState(int state) {  
        myState = state;  
    }  
}

在实际编程中,互斥和状态保护常常一起使用,以实现更安全的多线程程序。例如,一个简单的银行账户类可能包含一个balance字段,这个字段的状态可能需要通过getter和setter方法进行访问和修改,同时这些操作可能需要在synchronized方法中进行,以确保在任何给定的时间点只有一个线程可以修改账户的状态。

volatile关键字- 6.2.4 -

volatile是Java提供的一种轻量级的同步机制。当一个变量被声明为volatile时,可以保证并发编程中的可见性和有序性,但无法保证原子性。

volatile关键字的作用主要体现在两个方面:

  • 保证可见性:当一个线程修改了一个volatile变量的值后,其他线程将会立即感知到这个修改,也就是说,volatile关键字能够保证多个线程之间对该变量的值是一致的。

  • 保证有序性:volatile关键字可以保证volatile之前的代码不能调整到后面,volatile之后的代码不能调整到前面。这是因为在写入或读取volatile变量时,Java线程内存模型会强制刷新或无效化本地工作内存中的共享变量,以确保按照主内存中的最新值进行操作。

注意:虽然volatile关键字可以提供一定的线程同步机制,但它不能替代锁或其他更强大的同步工具。同时,使用volatile关键字也需要考虑到它的适用范围和限制,比如无法保证原子性等。

Thread类及其方法- 6.2.5 -

Java的Thread类是用于创建和启动新线程的类,新线程将运行指定的run()方法中的代码。以下是Thread类的一些主要方法和功能:

  • 构造方法:Thread类有多个构造方法,可以用于创建线程。例如,最常用的构造方法有两个参数:一个是Runnable对象,表示要在线程中运行的代码;另一个是线程名称,表示线程的名称。

  • start():此方法用于启动新线程。当调用start()方法时,新线程将开始运行,执行run()方法中的代码。

  • run():这是所有Thread对象必须实现的方法。当调用start()方法时,新线程将开始运行,执行此方法中的代码。通常,我们会在自定义Thread类的子类中重写此方法。

  • sleep():此方法使当前线程暂停执行指定的时间。它有两种形式:一种是不带参数的sleep(),它使线程暂停1秒钟;另一种是带一个long型参数的sleep(),它使线程暂停指定的时间(以毫秒为单位)。

  • interrupt():此方法用于中断线程。如果线程处于阻塞状态(例如,在Object的wait()或Thread的sleep()方法中),那么调用interrupt()方法将使线程抛出一个InterruptedException。要检查线程是否已被中断,可以使用Thread的isInterrupted()方法。

  • join():此方法使当前线程等待指定的Thread对象完成执行。它有两种形式:一种是不带参数的join(),它使当前线程等待直到对应的Thread对象完成执行;另一种是带一个long型参数的join(),它使当前线程等待指定的时间(以毫秒为单位),然后返回一个boolean值表示指定的Thread对象是否完成执行。

  • isAlive():此方法返回一个boolean值表示指定的Thread对象是否还活着(即是否已经开始并且尚未完成)。如果线程还在运行,或者在等待join()返回,那么它就是活着的。

  • currentThread():这是静态方法,用来返回执行这一行代码的线程,返回类型为Thread。

  • getName():获取当前线程的名字。

  • setName():设置当前线程的名字,记得要在start方法之前进行。

Thread类弃用的方法- 6.2.6 -

Thread类中一些已经被弃用的方法包括:

  • stop():该方法会立即终止线程,不给予线程任何机会将对象恢复为合法状态。这违反了并发安全等原则,因此绝对不应该使用此方法。

  • destroy():这个方法一直没有实现,如果实现了,会遇到与suspend()方法一样的条件竞争。

  • suspend()和resume():当一个线程被挂起时,它不会释放它所拥有的任何监视器,因此,如果其他线程试图访问这些监视器,这些监视器会变成死锁。这种机制会导致死锁之间的条件竞争,而且resume()会导致这几个方法不能使用。

  • countStackFrames():该方法也被弃用,虽然没有明确说明不推荐使用,但是在实际应用中,我们应避免使用此方法。

使用线程- 6.2.7 -

Java中可以使用线程来执行并行任务,提高程序的效率和响应能力。下面是一些使用线程的步骤:

1、创建线程类:要创建线程,必须实现Java的Runnable接口或继承Java的Thread类。通常情况下,实现Runnable接口是更常见的做法,因为它可以避免Java的单继承限制。

public class MyRunnable implements Runnable {  
    public void run() {  
        // 在这里编写线程的代码  
    }  
}

2、创建线程实例:在主程序中创建线程实例,将线程类作为参数传递给Thread类的构造函数。

MyRunnable myRunnable = new MyRunnable();  
Thread thread = new Thread(myRunnable);

3、启动线程:调用Thread类的start()方法启动线程。

thread.start();

4、线程状态:线程启动后,它将处于运行状态,当线程执行完run()方法或遇到阻塞时,线程将变为结束状态。可以使用Thread类的getState()方法获取线程的状态。

Thread.State state = thread.getState();

5、线程同步:当多个线程访问共享资源时,可能会出现竞争条件和死锁等问题,因此需要对共享资源进行同步访问。可以使用synchronized关键字或Lock接口来实现线程同步。

synchronized(this) {  
    // 在这里编写同步代码  
}

或者使用Lock接口:

Lock lock = new ReentrantLock();  
lock.lock();  
try {  
    // 在这里编写同步代码  
} finally {  
    lock.unlock();  
}

6、线程等待和通知:Thread类提供了一些等待和通知机制,可以让线程等待某个条件成立或通知其他线程某个条件已经成立。可以使用wait()、notify()和notifyAll()方法来实现等待和通知。

synchronized(this) {  
    while(!condition) {  
        this.wait(); // 等待条件成立  
    }  
    // 条件成立后的代码  
}

或者使用notify()或notifyAll()方法通知等待在该对象监视器上的线程:

synchronized(this) {  
    // 改变条件  
    this.notify(); // 通知一个等待线程  
}