线程与进程

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

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

并发与并行

并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

线程安全

指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:

多线程对比多进程优势

  1. 进程之间不能共享数据,线程可以。
  2. 系统创建进程需要为该进程重新分配系统资源,故创建线程代价较小。
  3. java内置多线程功能支持,简化了多线程编程。

线程创建和启动

  • 继承Thread类创建线程

    步骤:

             a. 定义一个继承Thread类的子类,并重写该类的run()方法;
    
    1. 创建Thread子类的实力,即创建了线程对象;
    2. 调用该线程对象的start()方法启动线程。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class IThread extends Thread {
    private String ThreadName;
    public IThread(String ThreadName){
    this.ThreadName=ThreadName;
    }
    public void run(){
    for(int i=0;i<10;i++)
    System.out.println(ThreadName+" run "+i);
    }

    public static void main(String[] args) {
    IThread thread_1=new IThread("A");
    IThread thread_2=new IThread("B");
    thread_1.start();
    thread_2.start();
    }
    }

    程序启动运行main时,java虚拟机启动一个进程,主线程main正在main()调用时被创建,随着调用线程的两个对象的start方法,另外两个线程也启动,这样整个应用就在多线程下运行。

    注:start()方法调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行由操作系统决定;多线程代码执行顺序都是不确定的,每次执行结果都是随机的。

  • 实现Runnable接口创建线程类

    步骤:

        a. 定义Runnable接口的实现类,并重写该接口的run()方法;
    
        b. 创建Runnable实现类的实力,并以此实力作为Thread的target对象,即Thread对象才是真正的线程对象
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class IRunnable implements Runnable {
    private String name;
    public IRunnable(String name){
    this.name=name;
    }
    public void run(){
    for(int i=0;i<10;i++){
    System.out.println(name+" run "+i);
    }
    }

    public static void main(String[] args) {
    IRunnable th1=new IRunnable("A");
    IRunnable th2=new IRunnable("B");
    // 将Runnable类对象作为Thread构造函数的参数,创建Thread对象,即为一个线程
    Thread myth1=new Thread(th1);
    Thread myth2=new Thread(th2);
    myth1.start();
    myth2.start();
    }
    }
        线程间可以共享相同的内存单元(包括代码与数据)利用共享单元来实现数据交换。对于Thread(Runnable target)构造方法创建线程,当使用相同的目标对象的线程,目标对象的成员变量自然就是这些线程共享的数据单元。
    

    **Thread与Runable的区别 **

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

    1. 适合多个相同的程序代码的线程去处理同一个资源
    2. 可以避免java重点单继承的限制
    3. 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
    4. 线程池只能放入事先Runnable或callable类线程,不能直接放入继承Thread的类。
  • 通过callable和Futrue创建线程

    步骤:

    1. 创建Callable接口的实现类,并重写call()方法,该call()方法将作为线程的执行体,并且有返回值。
    2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class ICallable implements Callable<Integer> {
    public Integer call() throws Exception{
    int i=0;
    for(;i<10;i++){
    System.out.println("当前线程为:"+Thread.currentThread()+"——"+i);

    }
    return i;
    }

    public static void main(String[] args) {
    ICallable ca = new ICallable();
    FutureTask<Integer> ft = new FutureTask<>(ca);
    Thread th = new Thread(ft, "线程");
    for (int i = 0; i < 3; i++) {
    //当前线程
    System.out.println(Thread.currentThread().getName() + "当前线程");
    if (i == 0) {
    th.start();
    }
    }
    try {
    System.out.println("返回值是:" + ft.get());

    } catch (Exception e) {
    e.printStackTrace();

    }
    }
    }

线程的生命周期

img

  1. 新建状态(New): 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。

    注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

    处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

    提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

  3. 运行状态(Running):处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

    处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

    注: 当发生如下情况是,线程会从运行状态变为阻塞状态:

    ​ ①、线程调用sleep方法主动放弃所占用的系统资源

    ​ ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

    ​ ③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有

    ​ ④、线程在等待某个通知(notify)

    ​ ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。

    当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。.

  4. 阻塞状态(Blocked):处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。

    在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。阻塞的情况分三种:

    1. 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    3. 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程控制

  1. sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),并进入阻塞状态

    sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。

    1
    2
    th.sleep(1000) //这里sleep的是main线程而非th线程
    Thread.sleep(1000)
  2. join()合并线程

    等待. 阻塞调用此方法时所在的线程并释放锁, 让调用该方法的线程完成才继续执行.

    线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
    从上面的方法的列表可以看到,它有3个重载的方法:

1
2
3
4
5
6
void join()      
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度

​ join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

  1. yield()线程让步:暂停当前正在执行的线程对象,并执行其他线程。

    yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

sleep()和yield()的区别

sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

sleep() 方法声明抛出InterruptedException异常,而 yield() 方法没有声明抛出任何异常

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

改变线程的优先级。每个线程在执行时都具有一定的优先级,优先级高的线程具有较多的执行机会。每个线程默认的优先级都与创建它的线程的优先级相同。main线程默认具有普通优先级。参数priorityLevel范围在1-10之间,常用的有如下三个静态常量值:MAX_PRIORITY:10;MIN_PRIORITY:1;NORM_PRIORITY:5。

1
2
3
4
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
  1. setDaemon(true):

    设置为后台线程。后台线程主要是为其他线程(相对可以称之为前台线程)提供服务,或“守护线程”。JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:

    ​ • 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。

    ​ • Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

    1
    2
    3
    4
    5
    6
    7
    public final void setDaemon(boolean on)        将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。    
    该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
    参数:
    on - 如果为 true,则将该线程标记为守护线程。
    抛出:
    IllegalThreadStateException - 如果该线程处于活动状态。
    SecurityException - 如果当前线程无法修改该线程。

    注:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。

  2. interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!

线程同步

  1. Synchronized

synchronized 可以保证方法或者代码块在运行时, 同一时刻只有一个方法可以进入到临界区, 同时它还可以保证共享变量的内存可见性. synchronized 主要有一下三个作用: 保证互斥性, 保证可见性, 保证顺序性.

由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

1、synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;

3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;

  1. volatile

    • volatile关键字为域变量的访问提供了一种免锁机制; • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;

    • 因此每次使用该域就要重新计算,而不是使用寄存器中的值;

    • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。