java 基础 线程

216 阅读21分钟

线程:

多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每个线程也是互相独立的。

线程和进程

进程特征
  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但必须更加小心,我们必须确保线程不会妨碍同一进程里的其他线程。

线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。

线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行,但操作系统无须将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程本身负责完成。

简而言之,一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。

线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性——多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现相互之间的通信。

当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程则简单得多,因此使用多线程来实现并发比使用多进程实现并发的性能要高得多。

总结起来,使用多线程编程具有如下几个优点。

  • 进程之间不能共享内存,但线程之间共享内存非常容易。

  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。

  • Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead) 5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

当发生如下情况,线程会进入就绪状态:

  • 当程序new的时候线程处于新建状态。该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体

  • 当调用start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

当发生如下情况时,线程将会进入阻塞状态。

  • 线程调用sleep()方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将有更深入的介绍。
  • 线程在等待某个通知(notify)。
  • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
  • 当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。

当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态。

  • 调用sleep()方法的线程经过了指定时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待某个通知时,其他线程发出了一个通知。
  • 处于挂起状态的线程被调用了resume()恢复方法。

线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。关于yield()方法后面有更详细的介绍。

Thread类

提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类的如下3个静态常量。

MAX_PRIORITY:其值是10。MIN_PRIORITY:其值是1 NORM_PRIORITY:其值是5。

下面程序使用了setPriority()方法来改变主线程的优先级,并使用该方法改变了两个线程的优先级,从而可以看到高优先级的线程将会获得更多的执行机会。

public class PriorityTest extends Thread {

// 定义一个有参数的构造器,用于创建线程时指定name

    public PriorityTest(String name) {

        super(name);

    }

    public void run() {

        for (int i = 0; i < 50; i++) {

            System.out.println(getName() + ",其优先级是:"

                    + getPriority() + ",循环变量的值为:" + i);

        }

    }

    public static void main(String[] args) {

// 改变主线程的优先级Thread.currentThread().setPriority(6); for (int i=0 ; i < 30 ; i++ )

        {

            if (i == 10) {

                PriorityTest low = new PriorityTest("低级");

                low.start();

                System.out.println("创建之初的优先级:"

                        + low.getPriority());

// 设置该线程为最低优先级

                low.setPriority(Thread.MIN_PRIORITY);

            }

            if (i == 20) {

                PriorityTest high = new PriorityTest("高级");

                high.start();

                System.out.println("创建之初的优先级:"

                        + high.getPriority());

// 设置该线程为最高优先级

                high.setPriority(Thread.MAX_PRIORITY);

            }

        }

    }

}

拖曳以移動

上面程序中的第一行粗体字代码改变了主线程的优先级为6,这样由main线程所创建的子线程的优先级默认都是6,所以程序直接输出low、high两个线程的优先级时应该看到6。接着程序将low线程的优先级设为Priority.MIN_PRIORITY,将high线程的优先级设置为Priority.MAX_PRIORITY。

ThreadLocal类:

ThreadLocal不是解决对象的共享访问问题,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

public void set(T value) {
    	// 获取当前线程
        Thread t = Thread.currentThread();
    	// 实际存储的数据结构类型
        ThreadLocalMap map = getMap(t);
    	// 如果存在map就直接set没有则直接创建并set
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
ThreadLocalMap getMap(Thread t) {
    // thread中维护了一个ThreadLocalMap
        return t.threadLocals;
    }
void createMap(Thread t, T firstValue) {
    //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

// ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
			// 
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
通过上面的代码不难看出在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。        

对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。

对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

当使用 ThreadLocal 维护变量时, ThreadLocal 为每个使用该变量的线程提供独立的变 量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

线程池:

  1. 线程池的概念:

​ 线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

  1. 线程池的工作机制

​ 2.1 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

​ 2.1 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

  1. 使用线程池的原因:

​ 多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

package com.blueearth.bewemp.doc.config;

import java.util.concurrent.*;

/**
 * @user:
 * @date:2021/1/11
 * @Description:
 */
public class ExecutorsDemo {
    /**
     * 创建一个线程池,该线程池可重用固定数量的线程*在共享的无界队列上操作。在任何时候,最多* {@code nThreads}个线程将是活动的处理任务。
     * *如果在所有线程都处于活动状态时提交了其他任务,则*它们将在队列中等待,直到某个线程可用为止。
     * *如果任何线程由于执行过程中的失败而终止
     * *在关闭之前*如果需要执行一个新任务,将替换一个新线程。池中的线程将存在*,
     * 直到明确地{@link ExecutorService#shutdown shutdown}为止
     */
    /**
     * 阿里巴巴规范
     * 强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
     * 说明:Executors返回的线程池对象的弊端如下:
     * 1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
     * 2)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
     */
    private static ExecutorService executorService = Executors.newFixedThreadPool(15);

    /**
     * Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
     *
     * ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
     *
     * LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
     *
     * 这里的问题就出在:**不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。**也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。
     *
     * 而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
     *
     * 上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM
     *
     *  public static ExecutorService newFixedThreadPool(int nThreads) {
     *         return new ThreadPoolExecutor(nThreads, nThreads,
     *                                       0L, TimeUnit.MILLISECONDS,
     *                                       new LinkedBlockingQueue<Runnable>());
     *     }
     * @param args
     */
    // 推荐使用以下形式:
    // 这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,
    // 这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好
    private static  ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));

    public static void main(String[] args){
        for(int i=0;i<Integer.MAX_VALUE;i++){
            executorService.execute(new SubThread());
            System.out.println("主线程main:"+Thread.currentThread().getName()+":::"+i++);
        }
    }
}
class SubThread implements Runnable{
    @Override
    public void run() {
        try {
            // 睡100ms然后进行执行子线程
            Thread.sleep(100);
        }catch (InterruptedException e){

        }
        System.out.println("子线程:"+Thread.currentThread().getName());
    }
}

参考1

参考2

什么是线程安全?

  • 多线程环境下,
  • 对对象的访问不需加入额外的同步控制,
  • 操作的数据结果依然是正确的。
保证线程安全:

1、synchronized

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

对于非静态代码块synchronized方法,锁的对象就是本身的this方法。

虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

2、Lock

先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。我们先来看下一般是如何使用的:

private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类

   private void method(Thread thread){
       lock.lock(); // 获取锁对象
       try {
           System.out.println("线程名:"+thread.getName() + "获得了锁");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
           System.out.println("线程名:"+thread.getName() + "释放了锁");
           lock.unlock(); // 释放锁对象
       }
   }

这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

线程的面试题:

Q&A 1.什么是线程?

线程是程序执行运算的最小的基本单位

Q&A2.线程与进程的区别?
  • 进程是系统中正在运行的一个应用程序,表示资源分配的基本单位。又是调度运行的基本单位。
  • 一个线程只能属于一个进程,而一个进程可以拥有多个线程。
  • 同一进程的所有线程共享该进程的所有资源。同一进程的多个线程共享代码段。
Q&A3.如何保证线程安全?

加锁是最简单的直接的方式。synchronized关键字

Q&A4.如何使用线程?线程是如何启动的?
  • 实现runnable接口,

  • 实现Callable接口,

  • 继承Thread类,

  • 线程启动,重写run方法然后调用start()即可开启一个线程。

Q&A5.线程的几种状态?

5种状态:创建,就绪,运行状态,阻塞,死亡

线程创建时new状态,调用start()进入runnable就绪状态,争夺cpu资源后进入running状态,由于某种原因进入(1)等待阻塞:运行的线程会释放占用的所有资源,jvm会把该线程放入“等待池”进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒, (2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。 (3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。 当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 当线程正常执行结束会进入dead状态(一个未捕获的异常也会使线程终止)

  • yield()只是使当前线程重新回到runnable状态
  • sleep()会让出cpu,不会释放锁
  • join()会让出cpu,释放锁
  • wait() 和 notify() 方法与suspend()和 resume()的区别在于wait会释放锁,suspend不会释放锁
  • wait() 和 notify()只能运行在Synchronized代码块中,因为wait()需要释放锁,如果不在同步代码块中,就无锁可以释放
  • 当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒
Q&A6.并发和并行的区别?

并发:同一时段,多个任务都在执行,

并行:单位时间内多个任务同时执行。

Q&A7.使用多线程带来什么问题可能?

并发是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁

Q&A8.什么是死锁?如何避免死锁?

死锁:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

破坏死锁的四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。 (无法破坏)
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(可以使用一次性申请所有资源)
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(占用部分资源线程去申请其他资源,如果不能申请到就主动释放它占有的资源)
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(按顺序申请资源。)
Q&A9.sleep()和wait()方法的区别?
  • sleep方法会让出cpu没有释放锁,wait方法释放了锁。

  • 两者都可以暂停线程的执行。

  • Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。

  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

Q&A 介绍一下Syncronized锁,

synchronized修饰静态方法以及同步代码块的Synchronized用法锁的是类,线程想要执行对应的同步代码就需要或的类锁。

synchronized修饰成员方法,线程获取的是调用当前对象实例的对象锁。

Q&A 介绍一下Synchronized和lock,

synchronized是java的关键字,当用来修饰一个方法或者代码块的时候,能够保证在同一时刻最多只有一个线程执行该代码。jdk1.5后引入自旋锁,锁粗化,轻量级锁,偏向锁来优化关键字的性能。

Lock是一个接口,Synchronized发生异常时自动释放线程占有的锁,因此不会导致死锁的现象。Lock发生异常时需要通过unLock()去释放锁,则需要在使用finally块中释放锁,Lock可以让等待锁的线程响应中断,而synchronized却不行,synchronized时等待的线程会一直等待。Lock可以知道是否成功获取锁,而synchronized却无法办到。

Q&A 介绍一下volatile

volatile修饰的是保障有序性和可见性,比如我们写的代码不一定会按照我们书写的顺序来执行。

volatile是Java提供的轻量级的同步机制,比sync的开销要小

被volatile定义的变量,系统每次用到它时都是直接从主存中读取,而不是各个线程的工作内存

volatile可以像sync一样保持变量在多线程环境中是实时可见的

可见性:

每个线程都有自己的工作内存,每次线程执行时,会从主存获得变量的拷贝,对变量的操作是在线程的工作内存中进行,不同的线程之间不共享工作内存;对于volatile(sync,final)来说,打破了上述的规则,当线程修改了变量的值,其他线程可以立即知道该变量的改变。而对于普通变量,当一个线程修改了变量,需要将变量写回主存,其他线程从主存中读取变量后才对该线程可见

volatile具有sync的可见性,但是不具备原子性(解决java多线程的执行有序性)。volatile适用于多个变量之间或者某个变量当前值和修改之后值之间没有约束。因此,单独使用volatile还不足以实现计数器,互斥锁等。

在并发编程中谈及到的无非是可见性、有序性及原子性。而这里的Volatile只能够保证前两个性质,对于原子性还是不能保证的,只能通过锁的形式帮助他去解决原子性操作