Java线程知识点总结

线程和进程

进程:程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

线程:一个比进程更小的执行单位,是操作系统能够进行运算调度的最小单位,包含在进程之中,是进程中的实际运作单位。

两者的关系(区别):线程是进程的子集,一个进程在其执行的过程中可以产生多个线程,每条线程并发执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。进程中的线程共享进程的堆和方法区的资源,每个线程有自己的程序计数器、虚拟机栈和本地方法栈。

并发编程三要素

  • 原子性:程序中的所有操作是不可中断的,要么全部执行成功要么全部执行失败。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
  • 可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。

【面试】Java中堆和栈有什么不同?(相对于线程来说)

栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。

堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。

线程安全、线程同步、线程互斥、线程通信

​ 线程安全:是指多线程执行同一份代码每次执行结果都和单线程一样。

​ 线程同步:对临界区的共享数据,A线程去操作数据,并且需要另一线程B的操作才能继续完成,这种线程之间协作的就是线程同步。

​ 线程互斥:对临界区的共享数据,两个线程都有修改情况,如果没有加锁或cas等的操作会造成数据混乱异常,这种就是线程互斥。

​ 线程通信:可以认为是线程同步的扩展,因为wait/notify必须获取了对象锁才能使用,通过wait/notify这种方式实现两个线程的等待唤醒。

线程的生命周期

新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态

  • 新建状态:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
  • 就绪状态:当线程对象调用了.start()方法之后,该线程处于就绪状态。JVM会为其创建方法调用栈和程序计数器,等待调度运行。
  • 运行状态:处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
  • 阻塞状态:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态。如果线程需要再次运行必须先转变成就绪状态。
  • 死亡状态:线程运行完成或出现异常,线程生命周期结束。

在这里插入图片描述

线程的实现方式

  • 实现Runnable接口
class MyThread implements Runnable{
   pulic void run(){
      System.out.println("Thread Body");
   }
}
 
public class Test{
  public static void main(String[] args){
     MyThread myThread = new MyThread;
     Thread thread = new Thread(myThread);
     thread.start();//启动线程
  }
}
  • 直接继承Thread类
class MyThread extends Thread{//创建线程类
    public void run(){
       System.out.println("Thread Body");//线程的函数体
    }
}
 
public class Test{
   public static void main(String[] args){
     MyThread thread = new Thread
     thread.run();//开启线程
   }
}
  • 实现Callable接口
public class CallableAndFuture{
   //创建线程类
   public static class CallableTest implements Callable{
     public String call() throws Exception{
        return "Hello World!";
     }
   }
   public static void main(String[] args){
     ExecutorService threadPool = Executors.newSingleThreadExecutor();
     Future<String> future = threadPool.submit(new CallableTest());
     try{
          System.out.println("waiting thread to finish");
          System.out.println(future.get());
        }catch{Exception e}{
          e.printStackTrace
        }
   }
}
  • 通过线程池创建线程

Java不支持类的多继承,但是允许实现多个接口,所以继承了Thread类就不能继承其他类,而且Thread类实际上也是实现了Runnable接口,所以最好使用Runnable接口实现线程。

Runnable和Callable

Runnable和Callable都是创建线程的方式。

Runnable:可以处理同一资源,实现资源的共享;线程不能返回结果;run()方法的异常只能在内部消化,不能继续上抛。

Callable:可以获取到线程执行的返回值、是否执行完成等信息,即能返回执行结果;call()方法允许向上抛出异常。

线程调度器(Thread Scheduler)和时间分片(Time Slicing)

线程调度器:一个操作系统服务,负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。

时间分片:将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(即最好不要让你的程序依赖于线程的优先级)。

Future和FutureTask

Future:是一个接口。是对runnable、callable的运行结果的操作,可以判断运行是否完成、是否取消以及获取运行的结果,获取运行结果调用get方法,这种方式获取结果是同步的。

FutureTask:是一个实现类,实现了Future和Runnable接口。所以既可以作为Runnable去启动一个线程,也可以作为Future去获取线程运行结果,只有当运行完成的时候结果才能取回结果,如果运行尚未完成get方法将会阻塞。Future是接口不能直接操作运行结果,FutureTask可以,也是Future唯一的实现类。由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。

线程安全

一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性。也就是说当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类就可以称为是线程安全的。

线程不安全

如果多个线程,存在着共享数据,那么就有可能出现线程的安全问题:当其中一个线程操作共享数据时,还未操作完成,另外的线程就参与进来,导致对共享数据的操作出现问题。

解决线程不安全:

要求一个线程操作共享数据时,只有当其完成操作共享数据,其它线程才有机会执行共享数据。

下文多线程同步的知识即为了解决线程不安全。

竞态条件

在多线程应用中,多个线程需要共享对同一数据进行存取,会由于不正确的执行时序,而出现多个不正确的结果。这样计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。

最常见的竞态条件类型就是先检查后执行操作,即通过一个可能失效的观测结果来决定下一步的动作。

如下图,对于main线程,如果文件a不存在,则创建文件a,但是在判断文件a不存在之后,Task线程创建了文件a,这时候先前的判断结果已经失效,(main线程的执行依赖了一个错误的判断结果)此时文件a已经存在了,但是main线程还是会继续创建文件a,导致Task线程创建的文件a被覆盖、文件中的内容丢失等等问题。

在这里插入图片描述

因此引出线程中的锁。

多线程同步

为了解决上面的竞态条件,当多个线程需要访问同一资源的时候,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则程序的运行结果将不可预料。也就是说,当线程A需要使用某个资源,如果该资源正被线程B使用,同步机制就会让线程A一直等待下去,直到线程B结束对该资源的使用,线程A才能使用。

要想实现同步操作,必须获得每一个线程对象的锁(lock)。获得锁可以保证同一时刻只有一个线程进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其它线程都不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能先进入等待队列。当拥有该对象锁的线程退出临界区,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。

临界区:指访问那种一次只能有一个线程执行的资源的代码块。

实现同步的方式有以下三种方式。

synchronized关键字

每个Java对象都可以用作一个实现同步的对象锁,这些锁称为内置锁(synchronized)

其使用方式就是使用synchronized关键字,表明在任何时候只允许被一个线程所拥有。线程在进入同步代码块之前自动获得锁,然后执行相应的代码,在退出同步代码块时自动释放锁,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

  • synchronized方法:只要把多个线程对类需要被同步的资源的操作放到方法中,保证多线程访问的安全性。
public synchronize void test();
  • synchronized块:以把任意代码段声明为synchronized,也可以指定上锁的对象,很灵活。
synchronized(syncObject){
	//访问syncObject的代码块
}

wait()方法与notify()或notifyAll()方法

在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并且可以调用notify()或者notifyAll()方法通知正在等待的其它线程。

使用synchronized来修饰某个共享资源时,如果线程1在执行synchronized代码,线程2也要执行同一对象的统同一synchronize的代码,线程2将要等到线程1执行完后执行,这种情况可以使用wai()和notify()进行线程唤醒。

class NumberPrint implements Runnable{  
    private int number;  
    public byte res[];  
    public static int count = 5;
    public NumberPrint(int number, byte a[]){  
        this.number = number;  
        res = a;  
    }  
    public void run(){  
        synchronized (res){  
            while(count-- > 0){  
                try {  
                    res.notify();//唤醒等待res资源的线程,把锁交给线程(该同步锁执行完毕自动释放锁) 
                    System.out.println(" "+number);  
                    res.wait();//释放CPU控制权,释放res的锁,本线程阻塞,等待被唤醒。  
                    System.out.println("------线程"+Thread.currentThread().getName()+"获得锁,wait()后的代码继续运行:"+number);  
                } catch (InterruptedException e) {  
                    // TODO Auto-generated catch block  
                    e.printStackTrace();  
                }  
            }//end of while  
            return;  
        }//synchronized  

    }  
}  
public class WaitNotify {
    public static void main(String args[]){  
        final byte a[] = {0};//以该对象为共享资源  
        new Thread(new NumberPrint((1),a),"1").start();  
        new Thread(new NumberPrint((2),a),"2").start();  
    }  
}
1  
2  
------线程1获得锁,wait()后的代码继续运行:1  
1  
------线程2获得锁,wait()后的代码继续运行:2  
2  
------线程1获得锁,wait()后的代码继续运行:1  
1  
------线程2获得锁,wait()后的代码继续运行:2 

Lock

JDK5新增加Lock接口以及它的一个实现类显式锁(ReentrantLock),实现多线程的同步

  • lock():以阻塞的方式获取锁,如果获取到了锁,就会执行,其他线程需要等待,unlock()锁后别的线程才能执行;如果别的线程持有锁,当前线程等待,直到获取锁后返回。
public int consume(){
    int m = 0;
    try {
        lock.lock();
        while(ProdLine.size() == 0){
            System.out.println("队列是空的,请稍候");
            empty.await();
        }
        m = ProdLine.removeFirst();
        full.signal(); 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally{
        lock.unlock();
        return m;
    }
}
  • tryLock():以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回true,否则,返回false。
  • tryLock(long timeout,TimeUnit unit):在给定的时间单元内,获取到了锁返回true,超时则false。
  • lockInterruptibly().如果获取了锁,立即返回;如果没有获取锁,当前线程处于休眠状态,直到获取锁,或者当前线程被中断(会收到InterruptedException异常)。它与lock()方法最大的区别在于如果lockInterruptibly()方法获取不到锁,就会一直处于阻塞状态,且会忽略Interrupt()方法。

synchronized与Lock

Java语言中提供了两种锁机制的实现对某个共享资源的同步;synchronized和Lock。

其中synchronized使用Object类对象本身的notify()、wait()、notifyAll()调度机制。

Lock使用condition包进行线程之间的调度,完成synchronized实现的所有功能。

  • 用法不一样:synchronized既可以加在方法上,也可以加在特定的代码块中,括号中表示需要的锁对象。而Lock需要显式的指定起始位置和终止位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现,他有比synchronized更精确的线程语义。

  • 性能不一样:在JDK5中增加了一个Lock接口的实现类ReentrantLock。它不仅拥有和synchronized相同的并发性和内存语义、还多了锁投票、定时锁、等候锁和中断锁。它们的性能在不同的情况下会有所不同;在资源竞争不激烈的情况下,synchronized的性能要优于RenntrantLock,但是资源竞争激烈的情况下,synchronized性能会下降的非常快,而ReentrantLock的性能基本保持不变。

  • 锁机制不一样:synchronized获得锁和释放锁的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且自动解锁,而condition中的await()、signal()、signalAll()能够指定要释放的锁,不会因为异常而导致锁没有被释放从而引发死锁的问题;而Lock则需要开发人员手动释放,并且必须放在finally块中释放,否则会引起死锁问题,此外,Lock还提供了更强大的功能,他的tryLock()方法可以采用非阻塞的方式去获取锁。

虽然synchronized与Lock都可以实现多线程的同步,但是最好不要同时使用这两种同步机制给统一共享资源加锁(不起作用),因为ReentrantLock与synchronized所使用的机制不同,所以它们运行时独立的,相当于两个种类的锁,在使用的时候互不影响。

public class Test{
     public static void main(String[] args) throws InterruptedException{
         final Lock lock=new ReetrantLock();
         lock.lock();
         Thread t1=new Thread(new Runnable){
            public void run(){
               try{
                   lock.lockInterruptibly();
               } catch(InterruptedException e){
                   System.out.println(" interrupted.");
               }
            }
         });
         t1.start();
         t1.interrupt();
         Thread.sleep(1000);
    }
}
【面试】当一个线程进入一个对象的synchronized方法后,其他线程是否能够进入此对象的其他方法?

其他线程可进入此对象的非synchronized修饰的方法。如果其他方法有synchronized修饰,都用的是同一对象锁,就不能访问。

【面试】如果其他方法是静态方法,且被synchronized修饰,是否可以访问?

可以的,因为static修饰的方法,它用的锁是当前类的字节码,而非静态方法使用的是this,因此可以调用。

死锁(deadlock)

死锁是指多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们将一直互相等待而无法推进下去。也就是说,死锁会让你的程序挂起无法完成任务。

比如:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    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();
    }
}
Output

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程A通过synchronized(resource1)获得resource1的监视器锁,然后通过Thread.sleep(1000)让线程A休眠1s为的是让线程B得到执行然后获取到resource2的监视器锁。线程A和线程B休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

活锁(livelock):

活锁和死锁一样无法继续执行线程。

死锁不能自己改变线程状态,表现为等待外力作用。

活锁是由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。所谓的“活”就是处于活锁的实体是在不断的改变状态,有可能自行解开。

避免死锁

需要破坏产生死锁的四个条件中的其中一个即可。

  • 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件:一次性申请所有的资源。
  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
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();

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

volatile

一个类型修饰符(type specifier),被设计用来修饰被不同线程访问和修改的变量。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

变量定义volatile之后具备两种特性:

  • 保证此变量对所有的线程的可见性:保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新
  • 禁止指令重排序优化:编译器和cpu处理器的常常会对指令做重排,volatile变量通过内存屏障是一个CPU指令,指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序

ThreadLocal

ThreadLocal是线程局部变量,为解决多线程的并发问题提供了一种新的思路。ThreadLocal提供了set、get等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

在ThreadLocal类中有一个Map(ThreadLocalMap),用于存储每个线程的变量副本,Map中元素的key为线程对象,而value对应线程的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

实质上:它只是一个线程的局部变量(其实就是一个Map)(以空间换时间)。

一些锁的定义

  1. 可重入锁:可重入锁是指当持有锁的线程再次获取这个锁时能立即成功,锁的计数+1,当执行完代码块时计数-1,当计数为0时释放锁;不可重入锁是持有锁的线程再次获取时会造成死锁的情况。
  2. 可中断锁:可中断锁就是线程在等待获取锁的时候可以中断,去处理别的任务。Synchronized不是可中断锁,lock就是可中断锁。
  3. 公平锁与非公平锁:公平锁是指获取锁是有顺序的,例如ReentrantLock可以通过构造方法设置成公平锁,等待时间长的锁优先获取锁;非公平锁就是获取锁跟顺序无关,随机选择一个线程分配锁,例如synchronized就是典型的非公平锁,这种非公平锁有可能导致某个线程一直获取不到锁。
  4. 独享锁\共享锁(互斥锁\读写锁):独享锁是指这个锁同一时间只能被一个线程持有,例如synchronized、ReentrantLock;共享锁是这个锁可以被多个线程共同持有,例如ReadWriteLock和其子类ReentrantReadWriteLock,其读锁是共享锁,写锁是独享锁。而互斥锁和读写锁就分别是独享锁和共享锁的具体表现。
  5. 乐观锁与悲观锁:乐观锁是认为读多写少,所以在读的时候不会加锁,在写的时候会先照常执行,当发现执行结果不对时会舍弃本次操作再重试,例如CAS算法;悲观锁是认为读少写多,所以在每次读写都会进行加锁,例如独占锁。
  6. 偏向锁、轻量级锁、重量级锁:这三种其实是锁的三种状态,并且都是真的synchroinzed而言的。偏向锁是指同步代码一直被一个线程访问,那么这个线程会自动获取这个锁,这个锁就是偏向锁;轻量级锁是指之前的偏向锁对应的代码块被另一个线程访问了,那么这个偏向锁会升级为轻量级锁,这时线程获取锁通过自旋的方式获取,减少上下文切换的消耗;重量级锁是指线程去获取轻量级锁的自旋了一段时间还是不能获取到锁,为了降低CPU的消耗,会让该锁升级为重量级锁,线程获取重量级锁的时候时候进入阻塞状态。

CAS算法

CAS(Compareand Swap,比较并交换)是乐观锁的一种典型算法实现。其核心是对于修改操作,会有旧值、预期值和新值,当去修改内存中的旧值时,会先去判断是否和自己的预期值相等,如果相等说明没有被别的线程修改过,直接替换为新值;如果不相等说明被别的线程修改了,就舍弃本次操作。这种方式能优化锁,提高效率,但是也可能出现ABA(A值被改为B,有改为了A,CAS不能发现)的情况。

锁优化的6种方式

  1. 细化锁(减少锁的持有时间)。减少加锁的代码块,因为加锁部分的代码越长运行时间越长,别的线程等待时间越长。
  2. 分段锁(减小锁的粒度)。例如concurrentHashMap,内部实现是多个segment(reentrantLock子类),通过将这个表分段提升性能。
  3. 粗化锁。对于重量级锁会是等待线程进入阻塞状态,增加线程上下文切换的开销,因此频繁的使用锁还不如使用较粗粒度的锁,虽然单个锁的运行时间长了,但是减少了CPU开销。
  4. 自旋锁。针对等待时间短的锁。
  5. 锁分离。例如读写锁ReadWriteLock,或LinkedBlockingQueue维护头尾两个锁。
  6. 锁消除。JIT会在编译时对不会共享的对象进行锁消除。

start()与run()

  • start() :启动一个新线程,通过start()方法来启动的新线程,使该线程状态变为就绪(可运行)状态,并没有运行,该线程何时开始运行,取决于JVM里线程调度器的调度。运行之后就开始执行相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程随即终止。
  • run():就和普通的成员方法一样,可以被重复调用。如果直接调用run方法,并不会启动新线程,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行。

start()和run()区别

  • start()可以启动一个新线程,run()不能。
  • start()不能被重复调用,run()可以。
  • start()中的run()代码可以不执行完就继续执行下面的代码,即进行了线程切换。直接调用run方法必须等待其代码全部执行完才能继续执行下面的代码。
  • start() 实现了多线程,run()没有实现多线程。

注意:启动线程使用start()方法,而不是run()方法。调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直按调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。需要指出的是,调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用start()方法,否则将引发IllegaIThreadStateExccption异常。

【面试】为什么调用start()方法时会执行run()方法,且为什么不能直接调用run()方法

new 一个 Thread,线程进入了新建状态;调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到CPU后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。而直接执行 run() 方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

wait()与sleep()

sleep()和wait()都是使线程暂停执行一段时间的方法。

**区别一:**原理不同(面试时可不答,偏重2,3)

sleep()方法是Thread类的静态方法,是线程用来控制自身流程的。

wait()方法是Object类的方法,用于线程间的通信。

**区别二:**对锁的处理机制不同

调用wait()的时候方法会释放当前持有的锁

调用sleep()的时候不会释放锁

**区别三:**使用地方不同

sleep()方法可以放在任何地方使用,通常被用于暂停执行,且必须捕获异常

wait()方法必须放在同步方法或者同步代码块中使用,通常被用于线程间交互/通信,不需要捕获异常

**区别四:**对锁的处理机制不同

sleep()方法执行完成后,线程会自动苏醒。

wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。

推荐:由于sleep()不会释放锁标志,容易导致死锁问题的发生,一般情况下,不推荐使用sleep()方法,而推荐使用wait()方法。

notify()与notifyAll()

调用notify(),JVM会从这个条件队列上等待的多个线程中选择一个来唤醒,选择哪个取决于线程调度器。

调用notifyAll(),则会唤醒所有在这个锁上等待的线程,并允许他们争夺锁确保至少有一个线程能继续运行。

Wait()一般和notify/notifyAll一起使用,这三个方法都是Object的方法,一般用于多个线程对共享数据的获取,并且只能在synchrnoized中使用,因为wait和notify方法使用的前提是必须先获取一个锁。

由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,如果使用notify,容易导致类似于信号丢失的问题,因此大多数情况下,应该优先选择notifyAll而不是单个的notify。

sleep()与yield()

yield()方法会使当前线程从运行状态变为就绪状态,把运行机会让给其它相同优先级的线程。它是一个静态的原生(native)方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能会被再次继续执行的。

**区别一:**sleep()给其他线程运行机会时,不考虑线程的优先级;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。

**区别二:**sleep()方法执行后,线程会转入阻塞状态,在指定的时间内该线程不会被执行;yield()方法是使当前线程从运行状态回到可执行状态,执行yield()方法的线程很可能在进入到可执行状态后马上又被执行。

join()

join()的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码。简单点说就是将两个线程合并,并实现同步功能。

比如:为了确保三个线程的顺序为T1,T2,T3,应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

线程终止

  • stop():会释放已经锁定的所有监视资源,如果当前任何一个受监视资源保护的对象处于一个不一致的状态(执行了一部分),其他线程将会获取到修改了的部分值,这个时候就可能导致程序执行结果的不确定性,并且这种问题很难被定位。

  • suspend():调用suspend()方法不会释放锁,会导致此线程挂起,容易发生死锁。

鉴于以上两种方法的不安全性,不建议使用以上两种方法来终止线程。

建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,既执行完run()方法,也就是说提供一种能够自动让run()方法结束的方式,在实际中,我们可以通过flag标志来控制循环是否执行,从而使线程离开run方法终止线程。

public class MyThread implements Runnable{
 private volatile Boolean flag;
 public void stop(){
     flag=false;
 }
 public void run(){
  while(flag);//do something
 }
}

阻塞状态下的线程结束

当线程处于阻塞状态时(sleep()被调用或wait()方法被调用或当被I/O阻塞时),上面的方法就不可用了。此时使用interrupt()方法来打破阻塞的情况,当interrupt()方法被调用时,会抛出interruptedException异常,可以通过在run()方法中捕获这个异常来让线程安全退出。

package com.TryFirst;

import java.util.Scanner;

public class TestMain implements Runnable{
	
	  public static void main(String[] args){
		  Thread thread = new Thread(new TestMain());
		  thread.start();
		  thread.interrupt();
		}

        @Override
        public void run() {
            System.out.println("thread go to sleep");
              try{
                  //用休眠来模拟线程被阻塞
                  Thread.sleep(5000);
                  System.out.println("thread finish");
              } catch (InterruptedException e){
                  System.out.println("thread is interrupted!");
            }
        }
}

如果I/0停滞,进入非运行状态,基本上要等到I/O完成才能离开这个状态;或通过出发异常,使用readLine()方法在等待网络上的发布信息”此时线程处于阻塞状态,让程序离开run()就出发close()方法来关闭流“,此时就会抛出IOException异常,通过捕获此异常离开run()。

interrupted()与isInterrupted()

interrupted()和isInterrupted()的主要区别是前者会将中断状态清除而后者不会。

Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有可能被其它线程调用中断来改变。

守护线程

Java提供了两种线程:守护线程和用户线程。

守护线程又被称为“服务进程”、“精灵线程”、“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分,通俗点讲,每一个守护线程都是JVM中非守护线程的“保姆”。典型例子就是“垃圾回收器”。只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。

用户线程和守护线程几乎一样,唯一的不同就在于如果用户线程已经全部退出运行,只剩下守护线程运行,JVM也就退出了。因为当所有非守护线程结束时,没有了守护者,守护线程就没有工作可做,也就没有继续运行程序的必要,程序也就终止了。同时会“杀死”所有的守护线程。也就是说,只要有任何非守护线程运行,程序就不会终止。

Java语言中,守护线程优先级都较低,它并非只有JVM内部提供,用户也可以自己设置守护线程,方法就是在调用线程的start()方法之前,设置setDaemon(true)方法,若将参数设置为false,则表示用户进程模式。需要注意的是,守护线程中产生的其它线程都是守护线程,用户线程也是如此。

需要在调用start()方法前调用setDaemon()这个方法,否则会抛出IllegalThreadStateException异常。

public class TestMain extends Thread{
	@Override
	  public void run() {
		  for(int i = 0 ; i<15;i++) {
			  System.out.print(getName()+":"+i + ";");
		}
	}
}
/*
 * public final void setDaemon(boolean on):是否设置为守护进程。true:是;false:否
 */
public class TestMain2 {
	public static void main(String[] args){
		TestMain t1 = new TestMain();
		TestMain t2 = new TestMain();
		t1.setName("A");
		t2.setName("B");

		//添加守护线程
		t1.setDaemon(true);
		t2.setDaemon(true);
		
		t1.start();
        t2.start();
			
		Thread.currentThread().setName("C");
			for(int i=0;i<2;i++) {
				System.out.print(Thread.currentThread().getName()+":"+i + ";");
		}
}
C:0;A:0;B:0;B:1;B:2;B:3;A:1;C:1;A:2;A:3;A:4;A:5;A:6;B:4;A:7;B:5;B:6;A:8;A:9;

A线程和B线程均设置为守护线程,C线程为用户进程。这三个线程均随机抢占CPU的使用权,当C抢占并且运行完毕之后,A和B这两个线程将在某一时间死亡,并不是立刻死亡而是继续执行一段时间,测试结果中会发现A和B在C结束后并没有马上停止并且也没有运行完全。

多线程的优势

  • 提高执行效率,减少程序的响应时间。因为单线程执行的过程只有一个有效的操作序列,如果某个操作很耗时(或等待网络响应),此时程序就不会响应鼠标和键盘等操作,如果使用多线程,就可以将耗时的线程分配到一个单独的线程上执行,从而使程序具备更好的交互性。
  • 与进程相比,线程的创建和切换开销更小。因开启一个新的进程需要分配独立的地址空间,建立许多数据结构来维护代码块等信息,而运行于同一个进程内的线程共享代码段、数据段、线程的启动和切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
  • 目前市场上服务器配置大多数都是多CPU或多核计算机等,它们本身而言就具有执行多线程的能力,如果使用单个线程,就无法重复利用计算机资源,造成资源浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
  • 利用多线程能简化程序程序的结构,是程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。

多线程中的上下文切换

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

线程池

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。

一个线程池包括以下四个基本组成部分:

  1. 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
  2. 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
  3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
  4. 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

场景:假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。

常见线程池

  • **newSingleThreadExecutor:**创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • **newFixedThreadPool(int nThreads):**创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,后面进入等待队列直到前面的任务完成才继续执行。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • **newCacheThreadPool(推荐使用):**可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • **newScheduleThreadPool(int corePoolSize):**创建一个大小无限的线程池,支持定时以及周期性执行任务的需求。

submit()与execute()

两个方法都可以向线程池提交任务。

execute()方法的返回类型是void,它定义在Executor接口中

submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。

参考博文

【1】:Java 并发基础常见面试题总结

【2】:JAVA多线程高并发面试题总结

【3】:Java多线程常用面试题

【4】:java线程知识点汇总

发布了20 篇原创文章 · 获赞 18 · 访问量 1328
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 书香水墨 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览