Java多线程基础(包括:线程的创建、状态生命周期、控制线程、线程同步、死锁、线程通信、线程池的基础知识)

一、引入与线程概述

为什么要搞多线程?单线程的程序只有一个顺序执行流,多线程的程序则可以包括多个程序执行流,多个顺序流之间互不干扰。

几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。当一个程序进入内存运行时,即变成一个进程。

一般而言,进程包含如下三个特征:
独立性:进程是系统中独立存在的实体,它可以拥有自己的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。进程具有自己的声明周期和各种不同的状态。
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
(注:并发性和并行性是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。)

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,因此编程更加方便;但是要注意确保线程不会妨碍同一进程里的其他线程。一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。

多线程编程具有如下几个优点:
1.进程之间不能共享内存,但线程之间共享内存非常容易。
2.系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
3.Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

二、线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

1.继承Thread类来创建并启动多线程的步骤如下:

1.定义Thread类的子类,并重写该类的run()方法,该run()方法体就代表了线程需要完成的任务。因此把run()方法称为线程执行体。
2.创建Thread子类的实例,即创建了线程对象。
3.调用线程对象的start()方法来启动该线程。
下面用一个简单的例子来创建并启动多线程(同时启动三个Thread-0,Thread-1,main):

public class FirstThread extends Thread{
    private int i;
    //重写了run方法
    public void run(){
        for(;i<100;i++){
            System.out.println(getName()+" "+i);//getName()返回当前线程的名字
        }
    }
    public static void main(String[] args){
        for(int i=0;i<100;i++){
            //Thread.currentThread() 该方法总是返回当前正在执行的线程对象
            //getName()总是返回调用该方法的线程名字
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20){
                new FirstThread().start();//创建并启动第一个线程
                new FirstThread().start();//创建并启动第二个线程
            }
        }
    }
}

可以看出,输出结果为Thread-0,Thread-1,main交替输出。
main()方法的方法体就是主线程的线程执行体。

2.实现Runnable接口创建线程类:

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

SecondThread st=new SecondThread();
new Thread(st);

也可以在创建Thread对象时为该Thread对象指定一个名字:

new Thread(st,"线程1")

3.调用线程对象的start()方法来启动该线程。
如下代码为一个简单示例:

public class SecondThread implements Runnable {
    private int i;
    public void run(){
        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
    public static void main(String[] args){
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20){
                SecondThread st=new SecondThread();
                new Thread(st,"新线程1").start();//起名字新线程1
                new Thread(st,"新线程2").start();//起名字新线程2
            }
        }
    }
}

该代码输出结果与上述代码差不多,采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类的实例变量。

3.使用Callable和Future创建线程:

Callable接口提供了一个call()方法可以作为线程执行体,call()方法比run()方法要强大:call()方法可以有返回值,并且可以声明抛出异常。
在Future接口里定义了如下几个公共方法来控制它关联的Callable任务:

boolean cancel(boolean mayInterruptIfRunning) 试图取消该Future里关联的Callable任务。
V get() 返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
boolean isCancelled() 如果在Callable任务正常完成前被取消,则返回true。
boolean isDone() 如果Callable任务已完成,则返回true。

创建并启动有返回值的线程的步骤如下:
1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,在创建Callable实现类的实例。从Java8开始可以直接使用Lambda表达式创建Callable对象。
2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3.使用FutureTask对象作为Thread对象的target创建并启动新线程。
4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return 1;
    }
}

public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 20) {
                new Thread(futureTask).start();
            }
        }
        System.out.println("子线程的返回值" + futureTask.get());
    }
}

创建线程的三种方式对比:
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
线程类只是实现了Runnable或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
劣势是,编程比较复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程的优缺点:
劣势是,因为线程类以及继承了Thread类,所以不能再继承其他父类。
优势是,编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

三、线程的状态和生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它是在这几个状态进行切换的:新建(NEW)、就绪(READY)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)超时等待(TIME_WAITING)和终止(TERMINATED)。

  1. 当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
  2. 当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序技术区,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。
  3. 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么任何时刻只有一个线程处于运行状态。在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
  4. 当线程执行了wait()方法后,线程进入了等待状态,进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。而超时等待状态相当于在等待状态的基础上增加了超时限制,比如seleep(long millis)方法或wait(long millis)方法,将线程至于TIMED_WAITING状态。当超时的时间到达后线程将会返回到RUNNABLE状态。
  5. 当发生如下情况时,线程将会进入阻塞状态:线程调用了一个阻塞式IO的方法,在该方法返回之前,该线程被阻塞;线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;线程在等待某个通知;程序调用了线程的suspend()方法将该线程挂起(不推荐,会导致死锁)。
  6. 线程会以如下三种方式结束,结束后就处于**终止*状态:
    run()或call()方法执行完成,线程正常结束;线程爆出一个未捕获的Exception或Error;直接调用该线程的stop()方法结束该线程(不推荐,会导致死锁)。

如下是线程状态转换图:
在这里插入图片描述

ps:

1.可以针对进入阻塞状态来让线程跳出阻塞状态。
2.线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。
3.就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。
4.为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态,返回true;当线程处于新建、死亡两种状态时返回false。
5.不要试图对一个已经死亡的线程调用start()方法使它重新启动,对新建状态的线程两次调用start()方法也是错误的。这都会引起IllegalThreadStateException异常。

四、控制线程

1.join() 方法

Thread提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。简单理解就是调join()的先执行。例如:如果一个线程实例threadA执行了threadB.join(),则表明当前线程A会等待threadB线程终止后threadA才会继续执行。
关于join有三个方法:

  1. public final void join() throws InterruptedException 这个是一直等待这个线程终止。
  2. public final void join(long millis) throws InterruptedException 等待这个线程死亡的时间最多为millis毫秒,如果为0表示一直等待。如果是负数则直接抛出IllegalArgumentException异常
  3. public final void join(long millis, int nanos) throws InterruptedException 等待最多millis毫秒加上这个线程死亡的nanos纳秒,如果millis为负数或者nanos不在0-999999范围则直接抛出IllegalArgumentException异常
public class JoinThread extends Thread{

    public JoinThread(String name) {
        super(name);
    }
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args) throws Exception {
    	//首先是跑main线程
        for (int i = 0; i < 20; i++) {
            if (i == 10) {
                JoinThread jt=new JoinThread("被Join的线程");
                jt.start();
                ////在main里面调用join(),main线程必须等join被执行完才继续执行
                jt.join();
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}
从输出结果可以看出,当main线程到9的时候,main线程停止,等待jt执行结束才开始执行。

2.守护线程

有一种线程的任务是为其他的线程提供服务,这种线程被称为后台线程、守护线程或精灵线程。JVM的垃圾回收线程就是典型的后台线程。后台线程有个特征:如果所有的前台线程都死亡,后台进程会自动死亡。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机就退出了。调用Thread对象的setDaemon(true)方法可将制定的线程设置成后台线程,Thread类还提供了一个isDaemon()方法,用于判断指定线程是否为后台线程。

public class DaemonThread extends Thread {

    private DaemonThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName() + " " + i + " " +  System.currentTimeMillis());
        }
    }

    public static void main(String[] args) {
        DaemonThread daemonThread = new DaemonThread("守护线程");
        //将这个daemonThread设置为守护线程
        daemonThread.setDaemon(true);
        //启动后台线程
        daemonThread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i + " " + System.currentTimeMillis());
        }
        //程序执行到此,main线程结束,守护线程也随即结束,不管前面的run设置为多少,只要main一结束它就结束
    }
}

一些注意事项:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException。不过抛出归抛出,该线程依然会执行,不过它不是被作为守护线程,而是作为正常的用户线程
  2. 在守护线程中产生的线程也是守护线程
  3. 并不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
  4. 守护线程中不能依靠finally块的内容来确保执行关闭或者清理资源逻辑,也就是说跑一个守护线程,里面的finally不一定被执行。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,那么finally自然就不会被执行了

3.sleep()方法

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。此外,Thread还提供了一个与sleep()方法有点相似的yield()静态方法,它也可以让当前正在执行的线程暂停,但不会阻塞该线程,只是将该线程转入就绪状态。实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会(这两个方法都不会释放锁)。
关于sleep()和yield()的方法的区别:
1.sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
2.sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程被yield()方法暂停之后,立即再次获得处理器资源被执行。
3.sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显示声明抛出该异常;而yield()方法则没有声明跑出任何异常。
4.sleep()方法比yield()方法有更好的一致性,通常不建议使用yield()。


4.设置优先级

每个线程都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围在1-10之间(1的优先级最低,10的最高),也可以使用MAX_PRIORITY值为10、MIN_PRIORITY值为1、NORM_PRIORITY值为5。不过一般不建议乱设置。

五、线程同步

synchronized关键字:该关键字常用于解决并发问题。在并发编程中存在线程安全问题,因为存在共享数据以及多线程共同操作共享数据。该关键字即可保证同一时刻只有一个线程可以执行某个方法或某个代码块。
Java的每一个对象都可以作为锁,这是synchronized同步的基础:1.普通同步方法,锁是当前实例对象,进入同步代码前要活的当前实例的锁;2.静态同步方法,锁是当前类的class对象,进入同步代码前要活的当前类对象的锁;3.同步方法快,锁是括号里面的对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。
synchronized关键字可以修饰方法、代码块,但不能修饰构造器,成员变量等等。
synchronize作用:1.确保线程互斥的访问同步代码;2.保证共享变量的修改能够及时看见;3.有效解决重排序问题。

public class SynchronizedTest implements Runnable{
	//如果不加synchronize,我们发现后面的t1 t2是交替输出的
    @Override
    public synchronized void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
    public static void main(String[] args) {
        SynchronizedTest st = new SynchronizedTest();
        //开两个线程
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        t1.start();
        t2.start();
    }
}
//从输出结果可以看出,t1全部运行完后,t2才开始。
//这说明给run方法加锁后,只有一把锁,多个线程访问时,只有一个线程能拿到锁

通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征:1.该类的对象可以被多个线程安全的访问;2.每个线程调用该对象的任意方法之后都将得到正确结果;3.每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

不可变类总是线程安全的,因为它的对象状态不可改变,可变对象则需要额外的方法来保证其线程安全。

可变线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的的负面影响:1.不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步;2.如果可变类有两种运行环境(单线程环境和多线程环境),则应该为该可变类提供两种版本,即线程安全版本和线程不安全版本。在单线程环境使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,线程会在如下几种情况释放对同步监视器的锁定(即释放锁):
1.当前线程的同步方法、同步代码快执行结束,当前线程即释放了同步监视器。
2.当前线程在同步代码快、同步方法中遇到break、return终止了该代码块、该方法继续执行,当前线程将会释放同步监视器。
3.当前线程在同步代码快、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
4.当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
ps:线程执行同步代码快或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。

从Java5开始,Java提供了一种通过显示定义同步锁对象来实现同步,即同步锁由Lock对象充当。加锁lock.lock();,最后解锁用finally块来保证释放锁finally{lock.unlock();}。Lock是控制多个线程对共享资源进行访问的工具

六、死锁详解

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。其中,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应当避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。死锁的规范定义:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。

在这里插入图片描述
如下为一个死锁例子:

public class DeadLockDemo {

    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + " get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + " waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + " get resource2");
                }
            }
        }, "线程1").start();
        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + " get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + " waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + " get resource1");
                }
            }
        }, "线程2").start();
    }
}
输出结果:
Thread[线程2,5,main] get resource2
Thread[线程1,5,main] get resource1
Thread[线程2,5,main] waiting get resource1
Thread[线程1,5,main] waiting get resource2

看代码不难看出,首先线程2获得了锁synchronized (resource2),然后由通过Thread.sleep(1000)休眠1s让线程1开始执行,然后线程1执行完后也进行了Thread.sleep(1000)休眠1s。这两个线程休眠结束后都企图去获得对方所持有的资源而陷入互相等待的状态,这就造成了死锁。

死锁产生的4个必要条件:

  • 互斥:一个资源一次只允许一个进程访问使用,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  • 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  • 不可抢占:进程所获得的的资源在未使用完毕之前,不能被其他进程强行夺走,只能由获得该再远的进程自己释放(只能是主动释放)。
  • 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

避免死锁(了解,因为下面方法的开销非常大。我们只要破坏其一即可):

  • 全部可用:即所有资源都可以被进程自由的访问(破坏互斥
  • 资源一次性分配:一次性分配所有资源,这样就不会再由请求了。只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏占有且等待
  • 可抢占资源:即当某个进程获得了部分资源,但得不到其他资源,则释放已占有的资源(破坏不可抢占
  • 资源有序分配:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏循环等待

我们对上面的程序的线程2进行修改:

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + " get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + " waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + " get resource2");
                }
            }
        }, "线程2").start();
输出:
Thread[线程1,5,main] get resource1
Thread[线程1,5,main] waiting get resource2
Thread[线程1,5,main] get resource2
Thread[线程2,5,main] get resource1
Thread[线程2,5,main] waiting get resource2
Thread[线程2,5,main] get resource2

我们发现,这么修改就避免了死锁,这是因为破坏了“占有且等待”这个条件。我们让线程1取得resource1,然后让其顺序的去取得resource2。取得完毕后,线程1就会释放资源,这样线程2就可以继续的去顺序取得resource1和resource2了。


发现存在死锁时,应立即解除:

  • 资源剥夺法:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
  • 撤销进程法:强制撤销部分、甚至全部死锁进程并且剥夺这些进程的资源。撤销的院子可以按进程优先级和撤销进程代价的高低进行。
  • 进程回退法:让一个或多个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

七、线程通信

线程同步有三种:传统的线程通信、使用Condition控制线程通信、使用阻塞队列(BlockingQueue)控制线程通信。

传统的线程通信是借助Object类的wait()、notify()、notifyAll()三种方法。这三个方法必须由同步监视器对象来调用:对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

wait() 导致当前线程等待,直到其他线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程。该wait()若不加参数,则一直等待直到其他线程通知,带参数可以是毫秒也可以是毫秒加微秒。
notify() 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
notifyAll() 唤醒再此同步监视器上等待的所有线程。

使用Condition控制线程通信:
如果程序不适用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,即不能用上述三种方法。
Condition类提供了await()、signal()、signalAll()三种方法,这三种方法与上述三个方法类似。
获取指定Lock对象对应的Condition:private final Condition cond=lock.newCondition()下面直接用cond调用方法即可。

使用阻塞队列(BlockingQueue)控制线程通信:
BlockingQueue有个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该进程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
BlockingQueue提供如下两个支持阻塞的方法:

put(E e) 尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
take() 尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

当然,它也可以使用Queue中的方法:

这里说一下在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回flase。

介绍下常见的BlockingQueue包含实现类:

ArrayBlockingQueue 基于数组实现的BlockingQueue队列。
LinkedBlockingQueue 基于链表实现的BlockingQueue队列。
PriorityBlockingQueue 它并不是标准的阻塞队列。该队列调用remove()、poll()、take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。它可以根据元素的大小来自然排序。

八、线程池

系统启动一个新线程的成本比较高,因为它涉及与操作系统交互。当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。线程池在系统启动时即创建大量空闲的线程,程序将一个对象传给线程池,线程池就会启动一个空闲的线程来执行它们的方法,当方法执行结束后,该线程并不会死亡,而是在其返回线程池中成为空闲状态,等待执行下一个方法。
使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数。

从Java5开始,新增了一个Executors工厂来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池:

newCachedThreadPool() 创建一个具有缓存功能的线程池,系统根据需要光剑线程,这些线程将会被缓存在线程池中。
newFixedThreadPool(int nThreads) 创建一个可重用的、具有固定线程数的线程池。
newSingleThreadExecutor() 创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入的参数为1。
newScheduledThreadPool(int corePoolSize) 创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。里面传入的参数指池中索堡村的线程数,即使线程是空闲的也被保存在线程池内。
newSingleThreadScheduledExecutor() 创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
ExecutorService newWorkStealingPool(int parallelism) 创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。
ExecutorService newWorkingStealingPool() 该方法是前一个方法的简化版本。如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数。

其中,前三个方法返回一个ExecutorService对象,该对象即代表一个线程池,它可以执行Runnable对象或Callable对象所代表的的线程;中间两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务;后两个方法是Java8新增的,利用了CPU并行的能力,这两个方法生产的池都是后台线程池(如果前台的线程都死亡了,则后台线程池里的线程会自动死亡)。

ExecutorService代表尽快执行线程的线程池(只要线程池中有空闲线程,就立即执行线程任务),它提供了如下三个方法:

Future<?> submit(Runnable task) 将一个Runnable对象提交给指定线程池,线程池将在有空闲线程时执行Runnable对象代表的任务。其中Future对象代表Runnable任务的返回值(run()方法无返回值,所以Future对象将在run()方法执行结束后返回null)。
T>Future<T submit(Runnable task,T result) 将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务。其中result显示指定线程执行结束后的返回值,所以Future对象将在run()方法执行结束后返回result。
T>Future<T submit(Callable task) 将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行Callable对象代表的任务。其中Future代表Callable对象里call()方法的返回值。

ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池,它提供如下四个方法:

ScheduledFuture<V schedule(Callable<V callable,long dalay,TimeUnit unit) 指定callable任务将在delay延迟后执行。
ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unti) 指定command任务将在delay延迟后执行。
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) 指定command任务将在delay延迟后执行,而且以设定频率重复执行。即在initialDelay后开始执行,依次在initialDelay+period、initialDelay+
*period…处重复执行。
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,随后在每一次执行中止和下一次执行开始之间都存在给定的延迟。如果任务在人一次执行时遇到异常,都会取消后续执行;否则只能通过程序来显示取消或终止该任务。

用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程池不再接受新任务,但是会将以前所有已提交的任务执行完成。线程池中所有的任务都执行完成后,池中所有的线程都会死亡;另外也可以调用shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

使用线程池来执行线程任务的步骤如下:
1.调用Executor类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
3.调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例。
4.当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。

八、线程相关类

ThreadLocal(线程局部变量),即为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。它只提供了如下三个public方法:

T get() 返回此线程局部变量中当前线程副本中的值。
void remove() 删除此线程局部变量中当前线程的值。
void set(T value) 设置此线程局部变量中当前线程副本中的值。

ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对同一变量的访问冲突。ThreadLocal将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存。通常建议如果多个线程之间需要共享资源(变量),以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共相冲突,则可以使用ThreadLocal。

ArrayList、LinkedList、HashSwt、TreeSet、HashMap、TreeMap都是线程不安全的,当多个并发线程向这些集合中存、取元素时,就可能会破坏这些集合的数据完整性。我们可以使用Collections提供的类方法把这些集合包装成线程安全的集合:

<T Collection<T synchronizedCollection(Collection<T c) 返回指定collection对应的线程安全的collection。
static<T List <T synchronizedList(List<T list) 返回指定List对象对应的线程安全的List对象。
static<K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回指定Map对象对应的线程安全的Map对象。
static<T Set<T synchronizedSet(Set<T s) 返回指定Set对象对应的线程安全的Set对象。
static<K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) 返回指定SortedMap对象对应的线程安全的SortedMap对象。
static<T SortedSet<T synchronizedSortedSet(SortedSet<T s) 返回指定SortedSet对象对应的线程安全的Sorted对象。

代码例子:HashMap m=Collections.synchronizedMap(new HashMap());
如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装

线程安全的集合类:
以Concurrent开头的集合类,例如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque。
以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet。
以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
当多个线程共享访问一个公共元素时,ConcurrentLinkedQueue是一个恰当的选择,因为它不允许使用null元素,它实现了多线程的高效访问,多个线程访问该集合时无需等待。
在默认情况下,ConcurrentHashMap支持16个线程并发写入,当有超过16个线程并发向该Map中写入数据时,可能有一些线程需要等待。
对于CopyOnWriteArrayList集合,它采用复制底层数组的方式来实现写操作。当线程对该集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对该集合执行写入操作时,该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对CopyOnWriteArrayList集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。

猜你喜欢

转载自blog.csdn.net/laobanhuanghe/article/details/96600609