JUC多线程

JUC多线程一

1.多线程基础

一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充
分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使
得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。

1.1线程与进程

进程:

操作系统资源分配的基本单位

是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用
程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基
本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立
的,至少有一个线程。

线程:

处理机任务执行和调度的基本单位

进程内部的一个独立执行单元,一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单一的CPU操作系统,二线程便是这个操作系统中运行的多个任务

堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多

1.2进程与线程的区别总结

线程具有许多传统进程所具有的特征,故又称为轻型进程或进程元,而把传统的进程称为重型进程,它相当于只有一个线程的任务,而引入了线程的操作系统中,通常一个进程有若干个线程,至少包含一个线程

根本区别:进程是资源分配的基本单位,而线程是处理机任务调度和执行的基本单位

**资源开销:**每个进程都有独立的代码和数据空间,数据直接的切换会有比较大的开销,线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小

**包含关系:**如果一个进程内有多个线程,则执行过程不是一条线的,,而是多条线程共同完成的,线程是进程的一部分,所以线程也被称为轻量级进程

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

**影响关系:**一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

2.从JVM角度说进程与线程之间的关系

图解进程与线程的关系

下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。

JUC多线程_第1张图片

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。

程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

**本地方法栈:**和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

一句话简单了解堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

多进程和多线程区别

多进程:操作系统中同时运行的多个程序

多线程:在同一个进程中同时运行的多个任务

举个例子,多线程下载软件,可以同时运行多个线程,但是通过程序运行的结果发现,每一次结果都不一致。 因为多线程存在一个特性:随机性。造成的原因:CPU在瞬间不断切换去处理各个线程而导致的,可以理解成多个线程在抢CPU资源。

多线程提高CPU使用率

JUC多线程_第2张图片

多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高。但是如果多线程有安全问题或出现频繁的上下文切换时,运算速度可能反而更低。

Java中的多线程
Java程序的进程里有几个线程:主线程,垃圾回收线程(后台线程)等

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

Java支持多线程,当Java程序执行main方法的时候,就是在执行一个名字叫做main的线程,可以在main方法执行时,开启多个线程A,B,C,多个线程 main,A,B,C同时执行,相互抢夺CPU,Thread类是java.lang包下的一个常用类,每一个Thread类的对象,就代表一个处于某种状态的线程

3.Java中的多线程创建方式

3.1继承Thread类

具体步骤

1.定义一个类继承Thread类,并重写Thread类的run()方法,run()中的内容就是线程所要执行的内容,因此把run()方法称为线程的执行体
2.创建该类的实例对象,即创建了线程对象
3.调用线程的start()方法来启动线程

代码实例
package com.pjh.SimpleThread;

/**
 * @ClassName: Demo1
 * @Author: 86151
 * @Date: 2021/3/25 09:24
 * @Description: TODO
 */
public class Demo1 {
    public static void main(String[] args){
        System.out.println("createThread线程启动");
           /*创建一个实例对象并使用start方法启动线程*/
        CreateThread createThread = new CreateThread();
        createThread.start();
    }


    /*继承一个Thread类*/
    public static class CreateThread extends Thread{
        @Override
        public void run() {
            String threadName=Thread.currentThread().getName();
            for (int i = 0; i < 5; i++) {
                System.out.println(threadName+":"+i);
            }

        }
    }
}

小知识

1.上述的getName()返回的是当前线程的名字,也可以通过setName()方法来指定当前线程的名字
2.当java程序运行之后程序至少会创建一个主线程,主线程的线程执行方法不是由run()方法确定的而是由main()方法确定的
3.在默认情况下主线程的名字为main(),用户创建的线程名字依次为Thread

3.2通过实现Runable接口创建线程类

具体步骤

1.定义一个类实现Runable接口
2.创建该类的实例对象
3.将该接口的实例对象传入Thread类实例对象,这个对象才是真正的线程执行对象
4.调用线程对象的start()方法启动该线程

代码示例
package com.pjh.SimpleThread;

/**
 * @ClassName: Demo2
 * @Author: 86151
 * @Date: 2021/3/25 09:40
 * @Description: TODO
 */
public class Demo2 {
    public static void main(String[] args) {
           /*创建ImpThread接口的实现类*/
        ImpThread impThread = new ImpThread();
        Thread thread1 = new Thread(impThread, "线程1");
        Thread thread2 = new Thread(impThread, "线程2");
        Thread thread3 = new Thread(impThread, "线程3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
    public static class ImpThread implements Runnable{
        private int i=0;
        public void run() {
            String name=Thread.currentThread().getName();
            for (; i < 3; i++) {
                System.out.println(name+" 正在打印:"+i);
            }
        }
    }
}
代码相关

1.实现Runnable接口的类的实例对象仅仅作为Thread对象的Target,Runable实现类里包含run()方法仅仅作为线程执行体,而实际订单线程对象依然是Thread实例,这里的Thread实例负责执行其target方法
2.通过实现Runnable接口来实现多线程时,要获取当前线程对象只可以通过Thread.currentThread()方法,而不能通过this关键字获取
3、从JAVA8开始,Runnable接口使用了@FunctionlInterface修饰,也就是说Runnable接口是函数式接口,可使用lambda表达式创建对象,使用lambda表达式就可以不像上述代码一样还要创建一个实现Runnable接口的类,然后再创建类的实例。

JUC多线程_第3张图片

1、线程1和线程2,线程3输出的成员变量i是连续的,也就是说通过这种方式创建线程,可以使多线程共享线程类的实例变量,因为这里的多个线程都使用了同一个target实例变量。但是,当你使用我上述的代码运行的时候,你会发现,其实结果有些并不连续,这是因为多个线程访问同一资源时,如果资源没有加锁,那么会出现线程安全问题(这是线程同步的知识,这里不展开);

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

优势

1.适合多个相同的程序代码的线程去共享同一个资源
2.可以避免java中单继承的局限性
3.增加程序的健壮性,实现解耦操作,代码可以被多个线程所共享,代码与数据彼此独立
4.线程池只能放入Runable或callabel类线程,不能直接放入继承Thread 的类

区别

继承Thread类的,我们相当于拿出三件事即三个卖票10张的任务分别分给三个窗口,他们各做各的事各卖各的票各完成各的任务,因为MyThread继承Thread类,所以在new MyThread的时候在创建三个对象的同时创建了三个线程;
实现Runnable的, 相当于是拿出一个卖票10张得任务给三个人去共同完成,new MyThread相当于创建一个任务,然后实例化三个Thread,创建三个线程即安排三个窗口去执行。

4.线程的种类

1.概念介绍

java中有两种线程一种是主程一种是子线程

主线程即mian方法
子线程:不是主线程的都是子线程

子线程可以分为以下两种:
**
守护线程
非守护线程,即用户线程

守护线程:
**
主要指在进程中,为主线程提供一种通用服务的线程
比如gc线程
因此,主线程一旦结束或销毁
守护线程没有了守护对象
也将同步进行结束或销毁。
就像天鹅,
姿态优雅的天鹅总是出双入对。当一只天鹅去世后,它们就会变得郁郁寡欢。有的绝食殉情,有的撞墙自尽,有的甚至飞以高处,突然快速冲向湖水之中,跳水而死。天鹅远比人这种高级动物纯粹许多…(忽然抒情)
JUC多线程_第4张图片

用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止
守护线程当进程不存在或主线程停止,守护线程也会被停止

非守护线程/用户线程
**
通常异步处理一些业务或逻辑

守护线程与用户线程的关系
**
用户线程在start之前可以通过setDaemo(true)来转别为守护线程
如果start之后调用setDaemo(true)
将会throw IllegalThreadStateException。

2.图解主线程与子线程的关系

程序启动运行main的时候,java虚拟机启动一个进程,主线程main在main()被调用的时候被创建使用myThread.start(的时候,另外一个线程叶启动了,整个线程就在多线程的下运行

JUC多线程_第5张图片

5.线程安全

5.1.什么是线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这些代码,程序每次运行的结果和单线程运行的结果是一样的,而其他变量的值也和预期的值是一样的,那么这是线程安全的,反之就是线程不安全的

5.2经典卖票案例

三个线程就好比正在卖票的三个窗口,定义的ticketsum就好比卖票的数量

代码实现
package com.pjh.SimpleThread;

/**
 * @ClassName: Main
 * @Author: 86151
 * @Date: 2021/3/25 19:39
 * @Description: TODO
 */
public class Main {
    public static void main(String[] args) {
        //使用同一个对象
        ThreadSafe threadSafe = new ThreadSafe();
        Thread  one = new Thread(threadSafe, "一号");
        Thread two = new Thread(threadSafe, "二号");
        Thread three = new Thread(threadSafe, "三号");
        one.start();
        two.start();
        three.start();
    }
    public static class ThreadSafe implements Runnable {
        private  int ticketsum=100;
        public  void  run(){
            while(true){
                if (ticketsum>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String name = Thread.currentThread().getName();
                    System.out.println(name+"正在卖:"+ticketsum--);
                }
            }

        }
    }


}

运行结果

我们可以看到出现了重复卖票的情况这种在我们的日常开发中肯定是不被允许的
JUC多线程_第6张图片

1.出现卖重复票的原因

JUC多线程_第7张图片

2.出现有的票没卖出去的原因

但是很快的,我们又发现了新问题,因为线程在竞争的过程中,CPU的切换是非常快的,可能线程1正好执行完–maipiao的时候,线程已经切换到了线程2,此时–maipiao又再执行了一次,导致跳过了一张票没有卖出,或者,当线程1恰好正好将要执行–maipiao但还没执行的时候,线程已经切换到了线程2,此时因为线程1并没有进行–maipiao操作,线程2卖出了重复的同一张票以后,才执行了–maipiao,导致出现了同一张票重复销售的情况。

3.出现负数票的原因

JUC多线程_第8张图片

4.小结

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

5.3解决线程安全问题的方案1

1.同步机制

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容
易出现线程安全问题。要解决上述多线程并发访问一个资源的安全问题,Java中提供了同步机制(synchronized)来解决。

2.同步锁介绍

同步代码块:synchronized关键字可以用于某个区块中,表示对这个区块的资源实行互斥访问

synchronized(同步锁){
需要同步操作的代码
}

同步锁:
对象的同步锁只是一个概念,可以想象为在改对象上上了一把锁
1.锁可以是任意的类型
2.多个线程对象要使用同一把锁
任何时候都最多允许一个对象拥有同步锁谁拿到锁就谁进入同步代码块
使用以下代码块来演示

3.同步代码块
Object lock = new Object();
//创建锁 synchronized(lock)
{ 
//可能会产生线程安全问题的代码
}

**
**
示例代码
**

package com.pjh.SimpleThread;

/**
 * @ClassName: Main
 * @Author: 86151
 * @Date: 2021/3/25 19:39
 * @Description: TODO
 */
public class Main {
    public static void main(String[] args) {
        //使用同一个对象
        ThreadSafe threadSafe = new ThreadSafe();
        Thread  one = new Thread(threadSafe, "一号");
        Thread two = new Thread(threadSafe, "二号");
        Thread three = new Thread(threadSafe, "三号");
        one.start();
        two.start();
        three.start();
    }
    public static class ThreadSafe implements Runnable {
        private  static int ticketsum=100;
        java.lang.Object object=new java.lang.Object();
        public  void  run(){
            while(true){
                synchronized (object){
                    if (ticketsum>0){
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        String name = Thread.currentThread().getName();
                        System.out.println(name+"正在卖:"+ticketsum--);
                    }
                }

            }

        }
    }


}

运行结果
JUC多线程_第9张图片

4.同步代码块图解

JUC多线程_第10张图片

5.同步方法

什么是同步方法?
使用synchronized修饰的方法叫做同步方法,保证线程安全,当a线程执行该方法的时候,其他线程只可以在方法外等待

public synchornized void method(){
可能产生线程安全的代码块
}

那么锁对象在哪呢?
锁对象是隐藏的,谁调用这个方法谁就是隐藏的锁对象,
对于非static方法锁对象就是this
对于static方法锁对象是类名.class
上代码

//同步方法 
public synchronized void method(){

//可能会产生线程安全问题的代码 
}
package ThreadSafe;
public class ThreadSafe implements Runnable {
    //定义一个多线程共享的 票源
    private  int ticketsum=100;
    java.lang.Object object=new java.lang.Object();
    //设置买票的线程任务
    public  void  run(){
      while(true) {
          shuchu();
      }
    }
    public  synchronized void shuchu(){
            //判断还有没有票
            if (ticketsum > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖:" + ticketsum);
                ticketsum--;
            }

    }
}

5.4解决线程安全问题的方案Lock锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁的功能
Lock lock = new ReentrantLock(); 
lock.lock(); 
//需要同步操作的代码 
lock.unlock();
public void lock()加同步锁
public void unlock() 释放同步锁
代码演示
package com.pjh.SimpleThread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName: Main
 * @Author: 86151
 * @Date: 2021/3/25 19:39
 * @Description: TODO
 */
public class Main {
    public static void main(String[] args) {
        //使用同一个对象
        ThreadSafe threadSafe = new ThreadSafe();
        Thread  one = new Thread(threadSafe, "一号");
        Thread two = new Thread(threadSafe, "二号");
        Thread three = new Thread(threadSafe, "三号");
        one.start();
        two.start();
        three.start();
    }
    public static class ThreadSafe implements Runnable {
        private  static int ticketsum=100;
       Lock lock= new ReentrantLock();
        public  void  run(){
            while(true){
                /*锁上*/
                lock.lock();
                    if (ticketsum>0){
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        String name = Thread.currentThread().getName();
                        System.out.println(name+"正在卖:"+ticketsum--);
                    }
              lock.unlock();
            }
        }
    }


}

5.5synchronized和lock的区别

JUC多线程_第11张图片

区别如下:
**

  1. 来源:
    lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  2. 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  3. 是否响应中断

    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断

  4. 是否知道获取锁
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

  5. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

  6. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  7. synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

5.6死锁问题

多线程死锁:同步中嵌套同步,导致锁无法释放。
死锁解决办法:不要在同步中嵌套同步

示例代码
public class Demo6DeadLock {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建三个窗口对象
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
       Thread t3 = new Thread(ticket, "窗口3");

        //卖票
        t1.start();
        t2.start();
        t3.start();
    }

    static class Ticket implements Runnable {

        Object lock = new Object();
        private int ticket = 100;

        public void run() {
            String name = Thread.currentThread().getName();
            while (true) {
                if ("窗口1".equals(name)) {
                    synchronized (lock) {
                        sell(name);
                    }
                } else {
                    sell(name);
                }
                if (ticket <= 0) {
                    break;
                }
            }
        }

        private synchronized void sell(String name) {
            synchronized (lock) {
                if (ticket > 0) {
                    System.out.println(name + "卖票:" + ticket);
                    ticket--;
                }
            }
        }
    }
1、synchronized和lock的用法区别

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

2、synchronized和lock性能区别

synchronized是托管给JVM执行的,
而lock是java写的控制锁的代码。
在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

2种机制的具体区别:

**synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。**独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

3、synchronized和lock用途区别

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。

1.某个线程在等待一个锁的控制权的这段时间需要中断

2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程

3.具有公平锁功能,每个到来的线程都将排队等候

下面细细道来……

先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断
**
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);

第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。

言情反式叙述

Synchronized:线程一旦进入 该对象的锁池,就不能反悔了,一直要等到获得该对象的锁才行。独占锁 对应的应该就是重量级锁,现在的高版本应该好很多了;线程切换是 指的 现在持有该锁的 线程,发生阻塞或执行完 释放 该锁,然后下一个线程切换上来; Lock:和之相比最突出就是,线程能自己从 该对象的 锁池 中出来(那意思就是“老子不等你了!”),而利用Synchronized的话,就要一直等下去(“真爱!”)

6.线程状态

1.基础概述

查看Thread源码,能够看到java的线程有六种状态:

public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

**NEW(新建) **线程刚被创建,但是并未启动。

RUNNABLE(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。

BLOCKED(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

WAITING(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

TIMED_WAITING(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。

TERMINATED(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

JUC多线程_第12张图片

2.wait(),notify()

基础介绍

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。

wait 方法会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。 notify 方法会通知某个正在等待这个对象的控制权的线程继续运行。 notifyAll 方法会通知所有正在等待这个对象的控制权的线程继续运行。

notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。这些方法可以使用于“生产者-消费者”问题,消费者是在队列中等待对象的线程,生产者是在队列中释放对象并通知其他线程的线程。

注意:一定要在线程同步中使用,并且是同一个锁的资源

代码示例

wait和notify方法例子,一个人进站出站:

public class Demo7WaitAndNotify {
    public static void main(String[] args) {
        State state = new State();
        InThread inThread = new InThread(state);
        OutThread outThread = new OutThread(state);
        Thread in = new Thread(inThread);
        Thread out = new Thread(outThread);
        in.start();
        out.start();
   }
    // 控制状态
    static class State {
        //状态标识
        public String flag = "车站外";
    }
    static class InThread implements Runnable {
        private State state;
        public InThread(State state) {
            this.state = state;
        }
        public void run() {
            while (true) {
                synchronized (state) {
                    if ("车站内".equals(state.flag)) {
                        try {
                            // 如果在车站内,就不用进站,等待,释放锁
                            state.wait();
                        } catch (Exception e) {
                        }
                    }
                    System.out.println("进站");
                    state.flag = "车站内";
                    // 唤醒state等待的线程
                    state.notify();
                }
            }
        }
    }
    static class OutThread implements Runnable {
        private State state;
        public OutThread(State state) {
            this.state = state;
        }
        public void run() {
            while (true) {
                synchronized (state) {
                    if ("车站外".equals(state.flag)) {
                        try {
                            // 如果在车站外,就不用出站了,等待,释放锁
                            state.wait();
                        } catch (Exception e) {
                        }
                    }
                    System.out.println("出站");
                    state.flag = "车站外";
                    // 唤醒state等待的线程
                    state.notify();
                }
            }
        }
    }
}

3.wait()和sleep区别

来自不同的类

sleep()是属于Thread类中的,而wait()方法则是object类中的。

这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

有没有释放锁资源

每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

使用范围

sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用

代码演示
public class MultiThread {
 
	private static class Thread1 implements Runnable{		
		@Override
		public void run() {
			//由于 Thread1和下面Thread2内部run方法要用同一对象作为监视器,如果用this则Thread1   和Threa2的this不是同一对象
			//所以用MultiThread.class这个字节码对象,当前虚拟机里引用这个变量时指向的都是同一个对象
			synchronized(MultiThread.class){
				System.out.println("enter thread1 ...");
				System.out.println("thread1 is waiting");
				
				try{
					//释放锁有两种方式:(1)程序自然离开监视器的范围,即离开synchronized关键字管辖的代码范围
					//(2)在synchronized关键字管辖的代码内部调用监视器对象的wait()方法。这里使用wait方法
					MultiThread.class.wait();
				}catch(InterruptedException e){
					e.printStackTrace();
				}
				
				System.out.println("thread1 is going on ...");
				System.out.println("thread1 is being over!");
			}
		}
		
	}
	
	private static class Thread2 implements Runnable{
		@Override
		public void run() {	
			//notify方法并不释放锁,即使thread2调用了下面的sleep方法休息10ms,但thread1仍然不会执行
			//因为thread2没有释放锁,所以Thread1得不到锁而无法执行
			synchronized(MultiThread.class){
				System.out.println("enter thread2 ...");
				System.out.println("thread2 notify other thread can release wait status ...");
				MultiThread.class.notify();
				System.out.println("thread2 is sleeping ten millisecond ...");
				
				try{
					Thread.sleep(10);
				}catch(InterruptedException e){
					e.printStackTrace();
				}
				
				System.out.println("thread2 is going on ...");
				System.out.println("thread2 is being over!");
			}
		}		
	}
	
	public static void main(String[] args) {
		new Thread(new Thread1()).start();
		try{
			Thread.sleep(10);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
 
		new Thread(new Thread2()).start();
	}
 
}

运行结果

JUC多线程_第13张图片

7.终止线程的方式

结束线程有以下三种方法:

(1)设置退出标志,使线程正常退出。

(2)使用interrupt()方法中断线程。

(3)使用stop方法强行终止线程(不推荐使用Thread.stop, 这种终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

1.使用退出标志

一般run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出,代码示例:

public class Demo8Exit {
    public static boolean exit = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            public void run() {
                while (exit) {
                    try {
                        System.out.println("线程执行!");
                        Thread.sleep(100l);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
        Thread.sleep(1000l);
        exit = false;
        System.out.println("退出标识位设置成功");
    }
}

2 使用interrupt()方法

使用interrupt()方法来中断线程有两种情况:

1)线程处于阻塞状态

如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。

2)线程未处于阻塞状态

使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。

public class Demo9Interrupt {
    public static boolean exit = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            public void run() {
                while (exit) {
                    try {
                        System.out.println("线程执行!");
                        //判断线程的中断标志来退出循环
                        if (Thread.currentThread().isInterrupted()) {
                            break;
                        }
                        Thread.sleep(100l);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //线程处于阻塞状态,当调用线程的interrupt()方法时,
                        //会抛出InterruptException异常,跳出循环
                        break;
                    }
                }
            }
        });
        t.start();
        Thread.sleep(1000l);
        //中断线程
        t.interrupt();
        System.out.println("线程中断了");
    }
}

8.多线程并发的三个特性

基础概述

1.原子性
2.可见性
3.有序性

1.原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

2.可见性

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
举个简单的例子,看下面这段代码:

//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;

当线程1执行int i = 0这句时,i的初始值0加载到内存中,然后再执行i = 10,那么在内存中i的值变为10了。
如果当线程1执行到int i = 0这句时,此时线程2执行 j = i,它读取i的值并加载到内存中,注意此时内存当中i的值是0,那么就会使得j的值也为0,而不是10。

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3.可见性深度解析

案例代码

有如下代码,连个线程使用继承自Runable接口的类作为参数创建线程

package com.pjh.SimpleThread;

import java.util.Stack;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName: Demo2
 * @Author: 86151
 * @Date: 2021/3/25 09:40
 * @Description: TODO
 */
public class Demo2 {
    public static void main(String[] args) {
           /*创建ImpThread接口的实现类*/
        ImpThread impThread = new ImpThread();
        Thread thread1 = new Thread(impThread, "线程1");
        Thread thread2 = new Thread(impThread, "线程2");
        thread1.start();
        thread2.start();

    }
    public static class Status{

    }
    public static class ImpThread implements Runnable{
        private int i=0;

        Lock lock=new ReentrantLock();
        public  void run() {
            String name=Thread.currentThread().getName();
            for (; i < 10; i++) {

                   System.out.println(name+" 正在打印:"+i);
               }


            }
        }
    }


运行结果

疑问:无论如何线程一与线程二的打印顺序都应该是递增的,但是却出现了线程打印的数据不按规则递增的情况这就是不可见性导致的,由于解析过长点击链接跳转到附录观看
JUC多线程_第14张图片

4.有序性

有序性:程序执行的顺序按照代码的先后顺序执行

int count = 0;
boolean flag = false;
count = 1; //语句1
flag = true; //语句2

以上代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
什么是重排序?一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致。
as-if-serial:无论如何重排序,程序最终执行结果和代码顺序执行的结果是一致的。Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语意)

上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?

再看下面一个例子:

int a = 10; //语句1
int b = 2; //语句2
a = a + 3; //语句3
b = a*a; //语句4

这段代码有4个语句,那么可能的一个执行顺序是: 语句2 语句1 语句3 语句4

不可能是这个执行顺序: 语句2 语句1 语句4 语句3
因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。虽然重排序不会影响单个线程内程序执行的结果,但是多线程会有影响

下面看一个例子:

//线程1:
init = false
context = loadContext(); //语句1
init = true; //语句2
//线程2:
while(!init){//如果初始化未完成,等待
  sleep();
}
execute(context);//初始化完成,执行逻辑

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行execute(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

9.线程优先级

1.优先级priority

现今操作系统基本采用分时的形式调度运行的线程,线程分配得到时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。
在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。

public class Demo10Priorityt {
    public static void main(String[] args) {
        PrioritytThread prioritytThread = new PrioritytThread();
        // 如果8核CPU处理3线程,无论优先级高低,每个线程都是单独一个CPU执行,就无法体现优先级
        // 开启10个线程,让8个CPU处理,这里线程就需要竞争CPU资源,优先级高的能分配更多的CPU资源
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(prioritytThread, "线程" + i);
            if (i == 1) {
                t.setPriority(10);
            }
            if (i == 2) {
                t.setPriority(1);
            }
            t.setDaemon(true);
            t.start();
        }
        try {
            Thread.sleep(1000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1总计:" + PrioritytThread.count1);
        System.out.println("线程2总计:" + PrioritytThread.count2);
    }
    static class PrioritytThread implements Runnable {
        public static Integer count1 = 0;
        public static Integer count2 = 0;
        public void run() {
            while (true) {
                if ("线程1".equals(Thread.currentThread().getName())) {
                    count1++;
                }
                if ("线程2".equals(Thread.currentThread().getName())) {
                    count2++;
                }
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
            }
        }
    }
}

2.join()方法

join作用是让其他线程变为等待,thread.Join把指定的线程加入到当前线程,可以将交替执行的线程合并为顺序执行的线程,比如线程B调用了线程A的Join()方法,直到线程A执行完毕后才会继续执行线程B。

代码
package com.pjh.SimpleThread;

import java.util.Random;

/**
 * @ClassName: Demo3
 * @Author: 86151
 * @Date: 2021/3/27 13:57
 * @Description: TODO
 */
public class Demo3 {
    public static void main(String[] args)
    {
        JoinThread joinThread = new JoinThread();
        Thread thread1 = new Thread(joinThread, "线程1");
        Thread thread2 = new Thread(joinThread, "线程2");
        Thread thread3 = new Thread(joinThread, "线程3");
        thread1.start(); thread2.start(); thread3.start();
        try {
            thread1.join();
        } catch (Exception e) {

        }
        for (int i = 0; i < 5; i++)
        {
            System.out.println("main ---i:" + i);
        }
    }
    static class JoinThread implements Runnable
    {
        private Random random = new Random();
        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 5; i++)
            {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }System.out.println(name + "内容是:" + i); } } }
}

执行结果

可以看到主线程main()在线程1执行完毕之后才会执行

F:\java1.8\bin\java.exe "-javaagent:F:\IDEA2020.2\IntelliJ IDEA 2020.2\lib\idea_rt.jar=62473:F:\IDEA2020.2\IntelliJ IDEA 2020.2\bin" -Dfile.encoding=UTF-8 -classpath F:\java1.8\jre\lib\charsets.jar;F:\java1.8\jre\lib\deploy.jar;F:\java1.8\jre\lib\ext\access-bridge-64.jar;F:\java1.8\jre\lib\ext\cldrdata.jar;F:\java1.8\jre\lib\ext\dnsns.jar;F:\java1.8\jre\lib\ext\jaccess.jar;F:\java1.8\jre\lib\ext\jfxrt.jar;F:\java1.8\jre\lib\ext\localedata.jar;F:\java1.8\jre\lib\ext\nashorn.jar;F:\java1.8\jre\lib\ext\sunec.jar;F:\java1.8\jre\lib\ext\sunjce_provider.jar;F:\java1.8\jre\lib\ext\sunmscapi.jar;F:\java1.8\jre\lib\ext\sunpkcs11.jar;F:\java1.8\jre\lib\ext\zipfs.jar;F:\java1.8\jre\lib\javaws.jar;F:\java1.8\jre\lib\jce.jar;F:\java1.8\jre\lib\jfr.jar;F:\java1.8\jre\lib\jfxswt.jar;F:\java1.8\jre\lib\jsse.jar;F:\java1.8\jre\lib\management-agent.jar;F:\java1.8\jre\lib\plugin.jar;F:\java1.8\jre\lib\resources.jar;F:\java1.8\jre\lib\rt.jar;D:\IDEAWorkSpace\Thread\target\classes com.pjh.SimpleThread.Demo3
线程1内容是:0
线程2内容是:0
线程3内容是:0
线程2内容是:1
线程2内容是:2
线程1内容是:1
线程3内容是:1
线程1内容是:2
线程2内容是:3
线程3内容是:2
线程3内容是:3
线程2内容是:4
线程1内容是:3
线程1内容是:4
main ---i:0
main ---i:1
main ---i:2
main ---i:3
main ---i:4
线程3内容是:4

Process finished with exit code 0

3.yield()方法

Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果) yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

JUC多线程_第15张图片

结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

10.锁优化

基础概述

重了。jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。

注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

1.自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

2.适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

3. 锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

public void test(){
    Vector vector = new Vector();
    for(int i = 0 ; i < 10 ; i++){
        vector.add(i);
    }
    System.out.println(vector);
}

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

4. 锁粗化

在使用同步锁的时候,需要让同步块的作用范围尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

5. 偏向锁

轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的。而偏向锁只需要检查是否为偏向锁、锁标识为以及ThreadID即可,可以减少不必要的CAS操作。

6.轻量级锁

引入轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。轻量级锁主要使用CAS进行原子操作。
但是对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

7. 重量锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock(互斥锁)实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

附录

1.多线程可见性问题

一、Java 中共享变量的内存可见性问题

在多线程下处理共享变量时Java 的内存模型,如下图所示。

JUC多线程_第16张图片

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。Java 内存模型是一个抽象的概念,那么在实际实现中线程的工作内存是什么呢?请看图2-5 。
JUC多线程_第17张图片

图中所示是一个双核CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。那么Java 内存模型里面的工作内存,就对应这里的Ll或者L2缓存或者CPU的寄存器。
当一个线程操作共享变量时, 它首先从主内存复制共享变量到自己的工作内存, 然后对工作内存里的变量进行处理, 处理完后将变量值更新到主内存。
那么假如线程A 和线程B 同时处理一个共享变量, 会出现什么情况?我们使用图2 - 5所示CPU 架构, 假设线程A 和线程B 使用不同CPU 执行,并且当前两级Cache 都为空,那么这时候由于C ache 的存在,将会导致内存不可见问题, 具体看下面的分析。
• 线程A 首先获取共享变量X 的值,由于两级Cache 都没有命中,所以加载主内存中X 的值,假如为0。然后把X=O 的值缓存到两级缓存, 线程A修改X 的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A 所在的CPU 的两级Cache 内和主内存里面的X 的值都是1 。
• 线程B 获取X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X= 1 ; 到这里一切都是正常的, 因为这时候主内存中也是X= 1 。然后线程B 修改X 的值为2, 并将其存放到线程2 所在的一级Cache 和共享二级Cache 中,最后更新主内存中X 的值为2 到这里一切都是好的。
• 线程A 这次又需要修改X 的值, 获取时一级缓存命中,并且X= 1,到这里问题就出现了,明明线程B 已经把X 的值修改为了2 ,为何线程A 获取的还是1呢? 这就是共享变量的内存不可见问题, 也就是线程B 写入的值对线程A 不可见。
那么如何解决共享变量内存不可见问题? 使用Java 中的volatile关键字就可以解决这个问题, 下面会有讲解。

二、volatile

在并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
并非在所有情况下使用它们都是等价的, volatile虽然提供了可见性保证,但并不保证操作的原子性。那么一般在什么时候才使用volatile 关键字呢?
• 写入变量值不依赖、变量的当前值时。因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性的,而volatile 不保证原子性。
• 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile 的。
示例:

public class ThreadSafeinteger {
private volatile int value ;
    public int get() (
        return value;
    }
        
     public void set (int value) {
        this .value = value ;
    }
        
}

2.1 为什么volatile不保证原子性

原子性:不可分割的,也即某个线程正在做某个具体业务时,中间不可以被加塞,需要整体完整。要么同时成功,要么同时失败。
因为根据JMM Java内存模型我们可以知道,会先从主内存中获取,再计算,最后写入。看字节码文件也可以看出来,虽然加了volatile关键,还是将是获取一计算一写入三步操作,这三步操作不是原子性的,所以volatile 不保证原子性。
解决原子性的问题:使用原子操作类:原子操作类都使用CAS 非阻塞算法,性能更好。Atomiclnteger。

三、CAS

CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起(因为用户态切换到系统态会花费大量的资源),而是被告知这次竞争中失败,并可以再次尝试。CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。
它都会在CAS指令之前返回该位置的值,CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。这其实和乐观锁的冲突检查+数据更新的原理是一样的。

3.1 Unsafe

unsafe是CAS的核心类,因为Java方法无法直接访问底层系统,需要通过本地native方法来访问,unsafe相当于是一个后门,基于该类可以直接操作特定内存数据。

3.2 CAS的缺点

1 如果CAS失败,会一直进行重试,如果CAS长时间不成功,可能会给cpu带来很大的开销。
2 只能保证一个共享变量的原子操作。
3 ABA问题-原子操作类ABA的问题

3.2.1 ABA问题

CAS算法实现的前提是取出来内存中某个时刻的数据,进行比较,然后写入替换,那么在这个时间差可能会出现数据的变化。
比如说两个线程1、线程2、两个线程都把同一个数据从内存中取出来为A,线程2进行了一些操作把值改为了B,然后又将数据改回了A,这个时候线程1进行数据修改的时候,发现内存中仍然是A,然后就操作成功了。
虽然说是修改成功了,但是不代表这个过程是没有问题的。

3.2.2 ABA问题的解决

借助原子引用类AtomicStampedReference,比较值+版本号。在进行设置新的值的同时也会比较修改版本号。

2.synchronized原理解析

基础概述

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个线程执行synchronized声明的代码块。还可以保证共享变量的内存可见性。同一时刻只有一个线程执行,这部分代码块的重排序也不会影响其执行结果。也就是说使用了synchronized可以保证并发的原子性,可见性,有序性。

1. 解决可见性问题

JMM关于synchronized的两条规定:
线程解锁前(退出同步代码块时):必须把自己工作内存中共享变量的最新值刷新到主内存中
线程加锁时(进入同步代码块时):将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁)

做如下修改,在死循环中添加同步代码块

while (flag) {
            synchronized (this) {
            }
        }

2.synchronized实现可见性的过程

  1. 获得互斥锁(同步获取锁)
  2. 清空本地内存
  3. 从主内存拷贝变量的最新副本到本地内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放互斥锁

3. 同步原理

synchronized的同步可以解决原子性、可见性和有序性的问题,那是如何实现同步的呢?
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  1. 普通同步方法,锁是当前实例对象this
  2. 静态同步方法,锁是当前类的class对象
  3. 同步方法块,锁是括号里面的对象

当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁。

synchronized的同步操作主要是monitorenter和monitorexit这两个jvm指令实现的,先写一段简单的代码:

public class Demo2Synchronized {
    public void test2() {
        synchronized (this) {
        }
    }
}

在cmd命令行执行javac编译和javap -c Java 字节码的指令
javac Demo2Synchronized.java
javap -c Demo2Synchronized.class

从结果可以看出,同步代码块是使用monitorenter和monitorexit这两个jvm指令实现的:

JUC多线程_第18张图片

3.Java内存可见性

3.1 了解Java内存模型

JVM内存结构、Java对象模型和Java内存模型,这就是三个截然不同的概念,而这三个概念很容易混淆。这里详细区别一下

3.1.1 JVM内存结构

我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。其中有些区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。
在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域结构如下:

JUC多线程_第19张图片

JVM内存结构,由Java虚拟机规范定义。描述的是Java程序执行过程中,由JVM管理的不同数据区域。各个区域有其特定的功能。

3.1.2 Java对象模型

Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
HotSpot虚拟机中(Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机),设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass对象,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。
JUC多线程_第20张图片

这就是一个简单的Java对象的OOP-Klass模型,即Java对象模型。

3.1.3 内存模型

Java内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
有兴趣详细了解Java内存模型是什么,为什么要有Java内存模型,Java内存模型解决了什么问题的学员,参考:https://www.hollischuang.com/archives/2550。
Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。JSR-133: Java Memory Model and Thread Specification中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。
JUC多线程_第21张图片

JMM线程操作内存的基本的规则:
第一条关于线程与主内存:线程对共享变量的所有操作都必须在自己的工作内存(本地内存)中进行,不能直接从主内存中读写
第二条关于线程间本地内存:不同线程之间无法直接访问其他线程本地内存中的变量,线程间变量值的传递需要经过主内存来完成。

  • 主内存
    主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

  • 本地内存
    主要存储当前方法的所有本地变量信息(本地内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的本地内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

3.1.4 小结

JVM内存结构,和Java虚拟机的运行时区域有关。 Java对象模型,和Java对象在虚拟机中的表现形式有关。 Java内存模型,和Java的并发编程有关

3.2 内存可见性

3.2.1 内存可见性介绍

可见性:一个线程对共享变量值的修改,能够及时的被其他线程看到
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:

  1. 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
  2. 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。

JUC多线程_第22张图片

如上图所示,本地内存 A 和 B 有主内存中共享变量 x 的副本。假设初始时,这三个内存中的 x 值都为 0。线程 A 在执行时,把更新后的 x 值(假设值为 1)临时存放在自己的本地内存 A 中。当线程 A 和线程 B 需要通信时,线程 A 首先会把自己本地内存中修改后的 x 值刷新到主内存中,此时主内存中的 x 值变为了 1。随后,线程 B 到主内存中去读取线程 A 更新后的 x 值,此时线程 B 的本地内存的 x 值也变为了 1。
从整体来看,这两个步骤实质上是线程 A 在向线程 B 发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证。

3.3.2 可见性问题

前面讲过多线程的内存可见性,现在我们写一个内存不可见的问题。
案例如下:

public class Demo1Jmm {
    public static void main(String[] args) throws InterruptedException {
        JmmDemo demo = new JmmDemo();
        Thread t = new Thread(demo);
        t.start();
        Thread.sleep(100);
        demo.flag = false;
        System.out.println("已经修改为false");
        System.out.println(demo.flag);
    }
    static class JmmDemo implements Runnable {
        public boolean flag = true;
        public void run() {
            System.out.println("子线程执行。。。");
            while (flag) {
            }
            System.out.println("子线程结束。。。");
        }
    }
}

执行结果

JUC多线程_第23张图片

按照main方法的逻辑,我们已经把flag设置为false,那么从逻辑上讲,子线程就应该跳出while死循环,因为这个时候条件不成立,但是我们可以看到,程序仍旧执行中,并没有停止。
原因:线程之间的变量是不可见的,因为读取的是副本,没有及时读取到主内存结果。** 解决办法**:强制线程每次读取该值的时候都去“主内存”中取值

你可能感兴趣的:(多线程)