java多线程相关知识讲解

一.多线程的基本概念。

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

线程和进程的生命周期是一样的分为5个阶段

创建、就绪、运行、阻塞、终止。

对于在java中实现多线程的方法。

有3种 分别是继承Thread类,实现Runable接口,实现Callable接口。

一般来说前两种使用的比较多。第三种方法使用的比较少一点。具体原因是:

实现Callable接口需要使用Future和ExecutorService等类来执行任务,并且需要额外的代码来处理返回值和异常。相比于继承Thread类或实现Runnable接口,实现Callable接口需要更多的代码量,相对也更加复杂。

但是这个方法也有它的使用场景,比如下面这些。

  1. 需要在多个线程中执行某个计算任务,最终将所有线程的计算结果汇总,可以使用Callable来返回每个线程的计算结果,最终进行汇总。

  2. Callable接口的call()方法可以抛出异常,而Runnable接口的run()方法不能抛出异常。在某些场景下,可能需要在多线程任务中抛出异常,这时候就需要使用Callable接口。

接下来就详细介绍一下继承Thread类和实现Runnable接口这两种方法的实现,以及它们的不同。

对于继承Thread类

代码实现

    public class MyThread extends Thread {
        private int count; // 执行次数

        public MyThread(int count) {
            this.count = count;
        }

        // 重写run方法
        @Override
        public void run() {
            for (int i = 0; i < count; i++) {
                System.out.println("当前线程:" + Thread.currentThread().getName() + ",执行次数:" + i);
                try {
                    Thread.sleep(1000); // 线程休眠1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        // 测试代码
        public static void main(String[] args) {
            MyThread t1 = new MyThread(3); // 创建线程1,执行3次
            MyThread t2 = new MyThread(5); // 创建线程2,执行5次
            t1.start(); // 启动线程1
            t2.start(); // 启动线程2
        }
    }

运行结果

 

以上代码中,MyThread类继承了Thread类,并且重写了run方法,用于执行多线程任务。在run方法中,使用for循环执行指定次数的任务,并且每隔1秒(使用sleep()方法实现,参数单位为ms)输出一次当前线程名和执行次数。

在main方法中,创建了两个MyThread对象t1和t2,并且分别执行了3次和5次任务。通过调用start()方法,启动了两个线程,执行run方法中的任务。由于继承Thread类实现多线程,因此可以直接调用start()方法启动线程。start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。

从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。

Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。

实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
但是同一个线程的start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。

这是因为线程的生命周期是有限的,一旦线程启动,它就进入了运行状态,如果再次调用start()方法就会破坏线程的生命周期,导致线程状态不一致,因此Java会抛出该异常。

如果需要重新启动一个线程,可以创建一个新的线程对象,并且调用start()方法启动线程。如果需要重新执行线程任务,可以在run()方法中实现对任务的重置操作。

总之,避免在一个已经启动的线程上重复调用start()方法,这样会导致线程状态不一致,可能会导致程序出现异常或者死锁等问题。对于死锁这个问题后面会详细介绍。

 实现java.lang.Runnable接口

实现代码

public class MyRunnable implements Runnable {
    private int count; // 执行次数

    public MyRunnable(int count) {
        this.count = count;
    }

    // 实现run方法
    @Override
    public void run() {
        for (int i = 0; i < count; i++) {
            System.out.println("当前线程:" + Thread.currentThread().getName() + ",执行次数:" + i);
            try {
                Thread.sleep(1000); // 线程休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 测试代码
    public static void main(String[] args) {
        MyRunnable r1 = new MyRunnable(3); // 创建任务1,执行3次
        MyRunnable r2 = new MyRunnable(5); // 创建任务2,执行5次
        Thread t1 = new Thread(r1); // 创建线程1,并传入任务1
        Thread t2 = new Thread(r2); // 创建线程2,并传入任务2
        t1.start(); // 启动线程1
        t2.start(); // 启动线程2
    }
}

输出结果

以上代码中,MyRunnable类实现了Runnable接口,并且实现了run方法,用于执行多线程任务。在run方法中,使用for循环执行指定次数的任务,并且每隔1秒输出一次当前线程名和执行次数。

在main方法中,创建了两个MyRunnable对象r1和r2,并且分别执行了3次和5次任务。使用Thread类创建了两个线程t1和t2,并且分别传入了对应的MyRunnable对象,启动了两个线程。在实现Runnable接口实现多线程时,需要使用Thread类创建线程对象,并且将实现Runnable接口的对象传入,作为线程的执行任务。调用start()方法启动线程。

由于实现Runnable接口,多个线程可以共用一个任务对象,因此可以避免多线程之间的资源竞争,提高了程序的执行效率和稳定性。

总之,实现Runnable接口实现多线程是Java中一种常见的实现方式,它可以避免线程状态不一致的问题,提高程序的执行效率和稳定性。

 这就是为什么使用实现Runnable接口的时候可以看到线程是有序执行的。不像继承thread类那样存在一个线程资源竞争的现象。

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。这是因为Java中的线程是基于操作系统的线程实现的。在操作系统中,每个线程都有自己的堆栈空间,用于存储线程的局部变量和方法调用栈。如果一个类继承Thread类,那么每个线程都会有自己的实例对象,这些对象之间是互相独立的,没有办法共享资源。而如果一个类实现了Runnable接口,那么多个线程可以共享同一个Runnable对象,这样就可以很容易地实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1.适合多个相同的程序代码的线程去处理同一个资源

2.可以避免java中的单继承的限制

3.增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4.线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

所以对于写多线程方面的代码还是尽量使用Runnable接口。

线程状态转换

1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
 对于这些状态下面这个图可以很形象的展示出来。

 这里补充介绍一下锁池这个概念

Java的锁池是指Java中用于实现同步的一种机制,用于管理线程之间的互斥关系,保证多个线程对共享资源的访问是有序的。锁池是由Java虚拟机维护的,它包含多个锁对象,每个锁对象都有一个等待队列,用于存放因为互斥关系而被阻塞的线程。

当一个线程访问某个共享资源时,如果该资源已经被其他线程占用了,那么该线程就会被加入到该资源的锁池等待队列中,等待其他线程释放该资源的锁对象。当其他线程释放了该资源的锁对象时,锁池中的线程就会被唤醒,继续竞争锁对象,以便访问共享资源。

Java中的锁池是基于synchronized关键字实现的。当一个线程执行synchronized代码块时,如果该代码块的锁对象已经被其他线程占用了,那么该线程就会被加入到该锁对象的等待队列中,等待其他线程释放锁对象。当其他线程执行完该代码块,释放了锁对象时,等待队列中的线程就会被唤醒,继续竞争锁对象。

总之,Java的锁池是用于管理线程之间的互斥关系,保证多个线程对共享资源的访问是有序的。锁池是由Java虚拟机维护的,它包含多个锁对象,每个锁对象都有一个等待队列,用于存放因为互斥关系而被阻塞的线程。

线程调度

1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:


static int MAX_PRIORITY
          线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
          线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
          分配给线程的默认优先级,取值为5。

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
 每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。(也就是5)
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
 
3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
 
4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
 
5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
 
6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
 常用函数说明

sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

Java中的sleep方法是Thread类的一个静态方法,用于使当前线程进入休眠状态,暂停执行一段时间。它的语法格式如下:

public static void sleep(long millis) throws InterruptedException

其中millis参数表示线程休眠的时间,以毫秒为单位。当调用sleep方法时,当前线程会被暂停执行,进入休眠状态,直到休眠时间结束或者线程被中断。

使用sleep方法可以实现线程的暂停和延迟执行,常用于以下场景:

  • 在执行任务之前等待一段时间;
  • 实现周期性的任务,如定时器等;
  • 模拟耗时操作,如文件读写、网络传输等;
  • 接收到外部事件后等待一段时间再处理。

需要注意的是,sleep方法会让当前线程暂停执行,但不会释放锁对象。如果一个线程在执行synchronized代码块时调用了sleep方法,那么其他线程无法访问该代码块,即使锁对象已经被释放,也需要等待该线程休眠时间结束后才能访问。

另外,sleep方法也可能会抛出InterruptedException异常,当线程正在休眠时被中断,就会抛出此异常。可以在catch块中处理该异常,例如在捕获InterruptedException异常后,可以使用Thread.currentThread().interrupt()方法重新标记线程的中断状态,并进行相应的处理。

总之,Java中的sleep方法可以使当前线程进入休眠状态,暂停执行一段时间,常用于实现线程的暂停和延迟执行。需要注意sleep方法不会释放锁对象,可能会抛出InterruptedException异常。

join():等待线程终止。

Java中的join()方法是Thread类的一个方法,用于等待线程执行结束。它的语法格式如下:

public final void join() throws InterruptedException

当一个线程A调用另一个线程B的join()方法时,线程A会进入阻塞状态,直到线程B执行结束才会继续执行。如果线程B已经执行结束,那么调用join()方法的线程A不会进入阻塞状态,直接执行下面的代码。

join()方法可以用于控制线程的执行顺序和结果。例如,在主线程中创建多个子线程,需要等待所有子线程执行结束后再进行下一步操作,可以使用join()方法。又例如,在一个线程中需要等待另一个线程执行完毕后再进行下一步操作,也可以使用join()方法。

需要注意的是,join()方法也可能会抛出InterruptedException异常,当线程正在等待另一个线程执行结束时,如果该线程被中断,则会抛出该异常。可以在catch块中处理该异常,例如在捕获InterruptedException异常后,可以使用Thread.currentThread().interrupt()方法重新标记线程的中断状态,并进行相应的处理。

总之,Java中的join()方法可以等待线程执行结束,常用于控制线程的执行顺序和结果。需要注意join()方法可能会抛出InterruptedException异常。

下面是一个简单的使用join函数的相关代码

public class JoinExample {

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(3)); // 创建线程1,并传入共享任务
        Thread t2 = new Thread(new MyRunnable(5)); // 创建线程2,并传入共享任务
        t1.start(); // 启动线程1
        t2.start(); // 启动线程2
        try {
            t1.join(); // 等待线程1执行结束
            t2.join(); // 等待线程2执行结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("所有线程执行完毕。");
    }

    // 定义共享任务
    static class MyRunnable implements Runnable {
        private int count; // 执行次数

        public MyRunnable(int count) {
            this.count = count;
        }

        // 实现run方法
        @Override
        public void run() {
            for (int i = 0; i < count; i++) {
                System.out.println("当前线程:" + Thread.currentThread().getName() + ",执行次数:" + (i + 1));
                try {
                    Thread.sleep(1000); // 线程休眠1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果

在这个示例中,我们首先创建了两个线程t1和t2,并且让它们执行相同的任务。在主线程(main方法)中,我们使用join()方法等待t1和t2执行结束,然后输出所有线程执行完毕的提示信息。

yield():用于让当前线程放弃CPU资源,让其他线程运行

它的语法格式如下:

public static native void yield()

当一个线程调用yield()方法时,它会让出CPU资源,让其他线程有机会运行。但是,由于CPU的调度机制是非确定性的,也就是说,不能保证yield()方法一定会让其他线程运行,也不能保证当前线程不会立即重新获得CPU资源。

yield()方法通常用于协助调试和测试,它可以使程序更容易地暴露出一些潜在的问题,例如线程之间的竞争、死锁等。另外,yield()方法也可以用于提高系统的响应速度,例如在需要及时响应用户输入或者其他事件时,可以使用yield()方法让其他线程有机会运行。

需要注意的是,yield()方法只是建议线程放弃CPU资源,不能保证一定会让其他线程运行。在实际应用中,我们需要根据具体的需求和场景选择合适的线程控制方法,以便实现更高效、更稳定的多线程应用。

代码例子


public class YieldExample {

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "线程1");
        Thread t2 = new Thread(new MyRunnable(), "线程2");
        t1.start();
        t2.start();
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "执行第" + (i + 1) + "次");
                Thread.yield(); // 让出CPU资源
            }
        }
    }
}

 在这个例子中,可以看到确实是有两种情况发送,在线程1让出CPU资源后,线程2获取到资源,和线程1让出CPU资源后,仍然是线程1获取的cpu资源。

sleep()和yield()是Java多线程中两个常用的线程控制方法,它们的主要区别如下:

  1. sleep()方法会让当前线程进入阻塞状态,放弃CPU资源,让其他线程有机会运行,而yield()方法不会阻塞线程,只是建议让出CPU资源,让其他线程有机会运行。
  2. sleep()方法指定了线程需要休眠的时间,一旦休眠时间到达,线程就会自动唤醒,而yield()方法不指定休眠时间,只是建议让出CPU资源,具体是否让出CPU资源还要看CPU的调度机制。
  3. sleep()方法可以抛出InterruptedException异常,需要进行异常处理,而yield()方法不会抛出异常。

需要注意的是,sleep()和yield()方法都只是建议线程执行的策略,不能保证一定会让其他线程运行或者阻塞指定的时间。对于yield()方法为什么不能保证一定会让其他线程运行原因上面已经说了,这里补充一下为什么sleep()不能保证阻塞指定的时间。

在Java中,sleep()方法会让当前线程进入阻塞状态,放弃CPU资源,让其他线程有机会运行。当调用sleep()方法时,线程会进入“休眠”状态,但实际的休眠时间可能会比指定的时间长或者短,这是由于以下几个原因:

  1. 系统调度延迟:在休眠期间,系统可能会因为调度延迟等原因而无法及时唤醒线程,导致实际休眠时间比指定的时间长。
  2. 线程调度机制:在多线程环境下,线程调度是非确定性的,也就是说,不能保证sleep()方法一定会让其他线程运行,也不能保证当前线程不会立即重新获得CPU资源,从而导致实际休眠时间比指定的时间短。
  3. 其他因素:例如操作系统的负载、硬件性能等因素也可能影响sleep()方法的实际休眠时间。

还有就是线程优先级对于这两个函数的使用影响。

sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。


 setPriority(): 更改线程的优先级

Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

 interrupt():中断一个线程的执行

Java中的interrupt()方法是Thread类的一个方法,用于中断一个线程的执行。它的语法格式如下:

public void interrupt()

当一个线程调用interrupt()方法时,它会将该线程的中断标志设置为true。如果线程正在阻塞状态(例如调用了sleep()、wait()、join()等方法),那么它会立即抛出InterruptedException异常。如果线程没有处于阻塞状态,那么它仅仅是将中断标志设置为true,具体的处理方式需要由程序员自行决定。

需要注意的是,interrupt()方法并不会立即停止线程的执行,它只是建议线程终止执行。如果线程不响应中断,那么它可能会一直执行下去。因此,在实际应用中,我们需要在程序中适时检查线程的中断标志,并通过程序逻辑来终止线程的执行。

wait():让线程进入等待

Java中的wait()方法是Object类的一个方法,用于让一个线程进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒它。它的语法格式如下:

public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException

当一个线程调用wait()方法时,它会释放所持有的对象锁,并进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒它。如果wait()方法没有传入参数,那么线程会一直等待下去,直到被唤醒。如果wait()方法传入了一个参数,那么线程会等待指定的时间,如果在等待时间内没有被唤醒,那么它会自动唤醒,继续执行。

需要注意的是,wait()方法只能在同步代码块或同步方法中使用,否则会抛出IllegalMonitorStateException异常。因为wait()方法需要持有对象锁才能进入等待状态,如果没有持有对象锁,那么就无法进入等待状态。

代码例子

    public static void main(String[] args) {
        Object lock = new Object(); // 创建一个共享对象
        Thread t1 = new Thread(new MyRunnable(lock), "线程1");
        Thread t2 = new Thread(new MyRunnable(lock), "线程2");
        t1.start();
        t2.start();
        try {
            Thread.sleep(5000); // 等待5秒钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lock) { // 获取对象锁
            lock.notify(); // 唤醒一个线程
            //lock.notifyAll(); // 唤醒所有线程
        }
    }

    static class MyRunnable implements Runnable {
        private final Object lock;

        public MyRunnable(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) { // 获取对象锁
                try {
                    System.out.println(Thread.currentThread().getName() + "开始等待...");
                    lock.wait(); // 进入等待状态
                    System.out.println(Thread.currentThread().getName() + "被唤醒了...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果

在这个示例中,我们创建了两个线程t1和t2,并且让它们共享一个对象lock。在MyRunnable中,我们使用synchronized关键字获得对象锁,然后调用lock.wait()方法进入等待状态,直到其他线程调用lock.notify()方法唤醒它。在主线程中,我们让t1和t2先执行wait()方法,然后等待1秒钟,最后调用lock.notify()方法唤醒它们。

下面讲一下notify()和notifyAll()方法

notify()是Object类的一个方法,用于唤醒一个正在等待当前对象锁的线程。它的语法格式如下:

public final void notify()

当一个线程调用notify()方法时,它会唤醒一个正在等待当前对象锁的线程,使其从wait()方法返回。如果有多个线程正在等待当前对象锁,那么只有其中一个线程会被唤醒,至于具体哪个线程被唤醒,取决于JVM的实现。

需要注意的是,notify()方法只能在同步代码块或同步方法中使用,否则会抛出IllegalMonitorStateException异常。因为notify()方法需要持有对象锁才能唤醒其他线程,如果没有持有对象锁,那么就无法唤醒其他线程。

除了notify()方法外,Object类还提供了一个notifyAll()方法,用于唤醒所有正在等待当前对象锁的线程。它的语法格式如下:

public final void notifyAll()

当一个线程调用notifyAll()方法时,它会唤醒所有正在等待当前对象锁的线程,使它们从wait()方法返回。如果没有线程正在等待当前对象锁,那么notifyAll()方法不会有任何影响。

需要注意的是,在使用notify()方法唤醒线程时,需要确保它们已经获取了对象锁。如果线程还没有获取对象锁,那么它就无法被唤醒,进入死锁状态。另外,在使用wait()方法时,我们需要遵循以下几个原则:

1. wait()方法只能在同步代码块或同步方法中使用,否则会抛出IllegalMonitorStateException异常。
2. wait()方法会释放所持有的对象锁,进入等待状态。如果其他线程在调用notify()或notifyAll()方法前获取了对象锁,那么当前线程就无法被唤醒,进入死锁状态。
3. wait()方法一般需要与notify()或notifyAll()方法一起使用,以便实现线程之间的协作。在使用notify()或notifyAll()方法唤醒线程时,需要确保它们已经获取了对象锁。 4. 在使用wait()方法时,需要捕获InterruptedException异常,以便保证程序的健壮性和稳定性。

总之,notify()和notifyAll()方法是Java多线程中常用的线程控制方法之一,可以实现线程之间的协作和同步。

线程类的一些常用方法: 

  sleep(): 强迫一个线程睡眠N毫秒。 
  isAlive(): 判断一个线程是否存活。 
  join(): 等待线程终止。 
  activeCount(): 程序中活跃的线程数。 
  enumerate(): 枚举程序中的线程。 
    currentThread(): 得到当前线程。 
  isDaemon(): 一个线程是否为守护线程。 
  setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
  setName(): 为线程设置一个名称。 
  wait(): 强迫一个线程等待。 
  notify(): 通知一个线程继续运行。 
  setPriority(): 设置一个线程的优先级。
 这些有一部分详细介绍过,有一部分不是图片重要的就没有详细介绍了。

线程同步

synchronized关键字

synchronized关键字的作用域有两种

在方法定义中,synchronized关键字用于限制对于某个对象的同步访问。如果一个方法被声明为synchronized,那么在执行这个方法期间,对象锁就会被获取,其他线程就无法访问该对象的其他synchronized方法,直到当前线程释放对象锁为止。

在代码块定义中,synchronized关键字用于控制对于某个对象的同步访问。如果一个代码块被声明为synchronized,那么在执行这个代码块期间,对象锁就会被获取,其他线程就无法访问该对象的其他synchronized代码块,直到当前线程释放对象锁为止。

我们还需要明确几点:

1.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

2.每个对象只有一个锁(lock)与之相关联。

3.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。


 同步方法

Public synchronized void method()
{
}

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。
 同步块

            public void method3(SomeObject so)
              {
                     synchronized(so)
{
       //被控制的代码
}
}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable
{
       private byte[] lock = new byte[0];  // 特殊的instance变量
    Public void methodA()
{
       synchronized(lock) { //… }
}
//被控制的代码
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

总结一下:

1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小 但是,一旦程序发生死锁,程序将只能强制停止运行。

线程数据传递

在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。

有三种方法可以用来传递数据。

通过构造方法传递数据

在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。
 

 
package mythread; 
public class MyThread1 extends Thread 
{ 
private String name; 
public MyThread1(String name) 
{ 
this.name = name; 
} 
public void run() 
{ 
System.out.println("hello " + name); 
} 
public static void main(String[] args) 
{ 
Thread thread = new MyThread1("world"); 
thread.start(); 
} 
} 

在上面的代码中就通过

public MyThread1(String name) 

this.name = name; 

这个构造方法传入了String name 的值。

由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。

通过变量和方法传递数据

使用set方法传输数据给变量。

这个和上面的代码差不多,只不过赋值方式,是在创建对象后面。

通过回调函数传递数据

上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。

 
package mythread; 
class Data 
{ 
public int value = 0; 
} 
class Work 
{ 
public void process(Data data, Integer numbers) 
{ 
for (int n : numbers) 
{ 
data.value += n; 
} 
} 
} 
public class MyThread3 extends Thread 
{ 
private Work work; 
public MyThread3(Work work) 
{ 
this.work = work; 
} 
public void run() 
{ 
java.util.Random random = new java.util.Random(); 
Data data = new Data(); 
int n1 = random.nextInt(1000); 
int n2 = random.nextInt(2000); 
int n3 = random.nextInt(3000); 
work.process(data, n1, n2, n3); // 使用回调函数 
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 
+ String.valueOf(n3) + "=" + data.value); 
} 
public static void main(String[] args) 
{ 
Thread thread = new MyThread3(new Work()); 
thread.start(); 
} 
} 

关于线程池

为什么要使用线程池:

总体来说,线程池有如下的优势:

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的组成:

线程池由线程池管理器、工作线程和任务队列三部分组成。其中,线程池管理器用于创建和管理线程池,包括线程池的大小、线程的超时时间等;工作线程是线程池中的实际工作者,用于执行任务;任务队列用于保存需要执行的任务,等待工作线程执行。

线程池的主要作用是控制线程的数量和复用线程。通过线程池,我们可以避免频繁地创建和销毁线程,从而减少了系统资源的消耗。此外,线程池还可以控制并发线程的数量,避免系统的负载过高,提高程序的响应速度和稳定性。

线程池的使用

线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

可以看到,其需要如下几个参数:

1.corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
2.keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
3.unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
4.workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
5.threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
6.handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

线程池的使用流程如下:

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

线程池的工作原理

 

详细的关于各种线程池的区别和相关的介绍可以看一下这个博客。

http://t.csdn.cn/NwZ3n

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值