JavaSE-进阶-学习笔记-多线程

本文将为您介绍Java多线程编程的一些基本概念和重要特性。我们将探讨多线程的定义和作用,以及如何实现线程安全。此外,我们还将介绍线程状态和线程池的概念,并讨论如何避免死锁等常见问题。最后,我们将详细介绍并发三特性:原子性、可见性和有序性,以及如何在编写多线程程序时考虑这些特性。无论您是初学者还是有经验的开发者,这篇博客都将为您提供有用的信息和指导,帮助您更好地理解和应用Java多线程编程。

目录

第1章 多线程

1.1 并发与并行

1.2 线程与进程

1.3 Thread类

1.4 创建线程方式一__继承方式

1.5 创建线程的方式二__实现方式

1.6 匿名内部类方式

第2章 线程安全

2.1 线程安全

2.2 线程同步

2.3 同步代码块

2.4 同步方法

2.5 Lock锁

第3章 线程状态

3.1 线程状态概述

3.2 睡眠sleep方法

3.3 等待和唤醒

3.4 等待唤醒案例(包子铺卖包子)

第4章 线程池方式

4.1 线程池的思想

4.2 线程池概念

4.3 线程池的使用

4.4 线程池的练习

第5章 死锁

5.1 什么是死锁

5.2 产生死锁的条件

5.3 死锁代码

第6章 可见性

6.1 看程序说结果

6.2 JMM

关于JMM的一些同步的约定:

6.3 问题分析

不可见性原因:

6.4 问题处理

方式一:加锁synchronized

方式二:volatile关键字

解决线程间变量的不可见性的方案有两种常见方式:

6.5 volatile与synchronized的区别

第7章 原子性

7.1 看程序说结果

7.2 问题原理说明

7.3 volatile原子性测试

代码测试

volatile的使用场景

7.4 问题解决

方式一:使用锁机制synchronized

方式二:原子类

AtomicInteger

案例改造

7.5 原子类CAS机制实现线程安全

7.5.1 概述

7.5.2 CAS总结

7.6 CAS与Synchronized:乐观锁与悲观锁

共同点:

不同点:

第8章 指令重排

8.1 什么是重排序?

8.2 代码案例

8.3 内存屏障

8.3.1 概述

8.3.2 内存屏障的主要类型

8.3.3 示例

8.4 volatile总结

volatile是Java虚拟机提供的轻量级的同步机制

结束语:


第1章 多线程

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

1.1 并发与并行

  • 并行:指两个或多个事件在同一时刻发生(同时执行)。

  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

1.2 线程与进程

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

  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

进程

线程

进程与线程的区别

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

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

注意:下面内容为了解知识点

1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

2:Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

1.3 Thread类

线程开启我们需要用到了java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:

构造方法:

  • public Thread():分配一个新的线程对象。

  • public Thread(String name):分配一个指定名字的新的线程对象。

  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。

  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

常用方法:

  • public String getName():获取当前线程名称。

  • public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。

  • public void run():此线程要执行的任务在此处定义代码。

  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  • public static Thread currentThread():返回对当前正在执行的线程对象的引用。

翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。

1.4 创建线程方式一__继承方式

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。

  2. 创建Thread子类的实例,即创建了线程对象

  3. 调用线程对象的start()方法来启动该线程

代码如下:

测试类:

 public class Demo01 {
     public static void main(String[] args) {
         //创建自定义线程对象
         MyThread mt = new MyThread("新的线程!");
         //开启新线程
         mt.start();
         //在主方法中执行for循环
         for (int i = 0; i < 200; i++) {
             System.out.println("main线程!"+i);
         }
     }
 }

自定义线程类:

 public class MyThread extends Thread {
     //定义指定线程名称的构造方法
     public MyThread(String name) {
         //调用父类的String参数的构造方法,指定线程的名称
         super(name);
     }
     public MyThread() {
         //不指定线程的名字,线程有默认的名字Thread-0
     }
     /**
      * 重写run方法,完成该线程执行的逻辑
      */
     @Override
     public void run() {
         for (int i = 0; i < 200; i++) {
             System.out.println(getName()+":正在执行!"+i);
         }
     }
 }

1.5 创建线程的方式二__实现方式

采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

  3. 调用线程对象的start()方法来启动线程。

代码如下:

 public class MyRunnable implements Runnable{
     @Override
     public void run() {
         for (int i = 0; i < 20; i++) {
             System.out.println(Thread.currentThread().getName()+" "+i);
         }
     }
 }

 public class Demo {
     public static void main(String[] args) {
         //创建自定义类对象  线程任务对象
         MyRunnable mr = new MyRunnable();
         //创建线程对象
         Thread t = new Thread(mr, "小强");
         t.start();
         for (int i = 0; i < 20; i++) {
             System.out.println("旺财 " + i);
         }
     }
 }

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

Thread和Runnable的区别

总结:

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

  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

  4. 线程池只能放入实现Runable或Callable类线程。

1.6 匿名内部类方式

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:

 public class NoNameInnerClassThread {
     public static void main(String[] args) {        
 //      new Runnable(){
 //          public void run(){
 //              for (int i = 0; i < 20; i++) {
 //                  System.out.println("张宇:"+i);
 //              }
 //          }  
 //      }; //---这个整体  相当于new MyRunnable()
         
         Runnable r = new Runnable(){
             public void run(){
                 for (int i = 0; i < 20; i++) {
                     System.out.println("张宇:"+i);
                 }
             }  
         };
         new Thread(r).start();
 ​
         for (int i = 0; i < 20; i++) {
             System.out.println("费玉清:"+i);
         }
     }
 }

第2章 线程安全

2.1 线程安全

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

我们通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票),需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟。

模拟票:

 public class Ticket implements Runnable {
     private int ticket = 100;
     /*
      * 执行卖票操作
      */
     @Override
     public void run() {
         //每个窗口卖票的操作 
         //窗口 永远开启 
         while (true) {
             if (ticket > 0) {//有票 可以卖
                 //出票操作
                 //使用sleep模拟一下出票时间 
                 try {
                     Thread.sleep(100);
                 } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                 }
                 //获取当前线程对象的名字 
                 String name = Thread.currentThread().getName();
                 System.out.println(name + "正在卖:" + ticket--);
             }
         }
     }
 }

测试类:

 public class Demo {
     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();
     }
 }

结果中有一部分这样现象:

发现程序出现了两个问题:

  1. 相同的票数,比如5这张票被卖了两回。

  2. 不存在的票,比如0票与-1票,是不存在的。

这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

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

2.2 线程同步

线程同步是为了解决线程安全问题。

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:

 窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

那么怎么去使用呢?

有三种方式完成同步操作:

  1. 同步代码块。

  2. 同步方法。

  3. 锁机制。

2.3 同步代码块

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

格式:

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

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。

  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块解决代码:

 public class Ticket implements Runnable{
     private int ticket = 100;
     
     Object lock = new Object();
     /*
      * 执行卖票操作
      */
     @Override
     public void run() {
         //每个窗口卖票的操作 
         //窗口 永远开启 
         while(true){
             synchronized (lock) {
                 if(ticket>0){//有票 可以卖
                     //出票操作
                     //使用sleep模拟一下出票时间 
                     try {
                         Thread.sleep(50);
                     } catch (InterruptedException e) {
                         // TODO Auto-generated catch block
                         e.printStackTrace();
                     }
                     //获取当前线程对象的名字 
                     String name = Thread.currentThread().getName();
                     System.out.println(name+"正在卖:"+ticket--);
                 }
             }
         }
     }
 }

当使用了同步代码块后,上述的线程的安全问题,解决了。

2.4 同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

 public synchronized void method(){
     可能会产生线程安全问题的代码
 }

同步锁是谁?

对于非static方法,同步锁就是this。

对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

使用同步方法代码如下:

 public class Ticket implements Runnable{
     private int ticket = 100;
     /*
      * 执行卖票操作
      */
     @Override
     public void run() {
         //每个窗口卖票的操作 
         //窗口 永远开启 
         while(true){
             sellTicket();
         }
     }
     
     /*
      * 锁对象 是 谁调用这个方法 就是谁 
      *   隐含 锁对象 就是  this
      *    
      */
     public synchronized void sellTicket(){
         if(ticket>0){//有票 可以卖   
             //出票操作
             //使用sleep模拟一下出票时间 
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
             }
             //获取当前线程对象的名字 
             String name = Thread.currentThread().getName();
             System.out.println(name+"正在卖:"+ticket--);
         }
     }
 }

2.5 Lock锁

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

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。

  • public void unlock():释放同步锁。

使用如下:

 public class Ticket implements Runnable{
     private int ticket = 100;
     
     Lock lock = new ReentrantLock();
     /*
      * 执行卖票操作
      */
     @Override
     public void run() {
         //每个窗口卖票的操作 
         //窗口 永远开启 
         while(true){
             lock.lock();
             if(ticket>0){//有票 可以卖
                 //出票操作 
                 //使用sleep模拟一下出票时间 
                 try {
                     Thread.sleep(50);
                 } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                 }
                 //获取当前线程对象的名字 
                 String name = Thread.currentThread().getName();
                 System.out.println(name+"正在卖:"+ticket--);
             }
             lock.unlock();
         }
     }
 }

第3章 线程状态

3.1 线程状态概述

线程由生到死的完整过程:

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典叫法)
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Terminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

3.2 睡眠sleep方法

我们看到状态中有一个状态叫做计时等待,可以通过Thread类的方法来进行演示.

public static void sleep(long time) 让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行

 public class Test{
   public static void main(String[] args){
     for(int i = 1;i<=5;i++){
         Thread.sleep(1000);
         System.out.println(i)   
     } 
   }
 }

这时我们发现主线程执行到sleep方法会休眠1秒后再继续执行。

3.3 等待和唤醒

Object类的方法

public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用.

 public class Demo1_wait {
     public static void main(String[] args) throws InterruptedException {
        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
         new Thread(() -> {
             try {
                 System.out.println("begin wait ....");
                 synchronized ("") {
                     "".wait();
                 }
                 System.out.println("over");
             } catch (Exception e) {
             }
         }).start();
     }

public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.

 
public class Demo2_notify {
     public static void main(String[] args) throws InterruptedException {
        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
         new Thread(() -> {
             try {
 ​
                 System.out.println("begin wait ....");
                 synchronized ("") {
                     "".wait();
                 }
                 System.out.println("over");
             } catch (Exception e) {
             }
         }).start();
 ​
         //步骤2:  加入如下代码后, 3秒后,会执行notify方法, 唤醒wait中线程.
         Thread.sleep(3000);
         new Thread(() -> {
             try {
                 synchronized ("") {
                     System.out.println("唤醒");
                     "".notify();
                 }
             } catch (Exception e) {
             }
         }).start();
     }
 }

3.4 等待唤醒案例(包子铺卖包子)

 定义一个集合,包子铺线程完成生产包子,包子添加到集合中;吃货线程完成购买包子,包子从集合中移除。
 1. 当包子没有时(包子状态为false),吃货线程等待.
 2. 包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态)

代码示例:

生成包子类:

 public class BaoZiPu extends Thread{
     private List<String> list ;
     
     public BaoZiPu(String name,ArrayList<String> list){
         super(name);
         this.list = list;
     }
     
     @Override
     public void run() {
         int i = 0; 
         while(true){
             //list作为锁对象
             synchronized (list){
                 if(list.size()>0){
                     //存元素的线程进入到等待状态
                     try {
                         list.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
 ​
                 //如果线程没进入到等待状态 说明集合中没有元素
                 //向集合中添加元素
                 list.add("包子"+i++);
                 System.out.println(list);
                 //集合中已经有元素了 唤醒获取元素的线程
                 list.notify();
             }
         }
     }
 }

消费包子类:

 
public class ChiHuo extends Thread {
 ​
     private List<String> list ;
     
     public ChiHuo(String name,ArrayList<String> list){
         super(name);
         this.list = list;
     }
 ​
     @Override
     public void run() {
         //为了能看到效果 写个死循环
         while(true){
             //由于使用的同一个集合 list作为锁对象
             synchronized (list){
                 //如果集合中没有元素,获取元素的线程进入到等待状态
                 if(list.size()==0){
                     try {
                         list.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
                 //如果集合中有元素 则获取元素的线程获取元素(删除)
                 list.remove(0);
                 //打印集合 集合中没有元素了
                 System.out.println(list);
                 //集合中已经没有元素 则唤醒添加元素的线程 向集合中添加元素
                 list.notify();
             }
         }
     }
 }

测试类:

 public class Demo {
     public static void main(String[] args) {
         //等待唤醒案例
         List<String> list = new ArrayList<>();
         // 创建线程对象        
         BaoZiPu bzp = new BaoZiPu("包子铺",list);
         ChiHuo ch = new ChiHuo("吃货",list);
         // 开启线程
         bzp.start();
         ch.start();
     }
 }

第4章 线程池方式

4.1 线程池的思想

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。

一 .简介 线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。

二.线程池 1.线程池的作用:线程池作用就是限制系统中执行线程的数量。 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果; 少了浪费了系统资源,多了造成系统拥挤效率不高。 用线程池控制线程数量,其他线程排队等候。

一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待任务,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

2.为什么要用线程池: 1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executors,严格意义上讲Executors并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口ExecutorService

3.比较重要的几个类: ExecutorService 真正的线程池接口。 ScheduledExecutorService 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 ThreadPoolExecutor ExecutorService的默认实现。 ScheduledThreadPoolExecutor 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

  1. newSingleThreadExecutor 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  2. newFixedThreadPool 创建固定大小的线程池。 每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束, 那么线程池会补充一个新线程。

  3. newCachedThreadPool 创建一个可缓存的线程池。 如果线程池的大小超过了处理任务所需要的线程, 那么会回收部分空闲(60秒不执行任务)的线程,当任务数增加时, 此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制, 线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

  4. newScheduledThreadPool 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

4.2 线程池概念

  • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

4.3 线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。

  2. 创建Runnable接口子类对象。(task)

  3. 提交Runnable接口子类对象。(take task)

  4. 关闭线程池(一般不做)。

Runnable实现类代码:

 public class MyRunnable implements Runnable {
     @Override
     public void run() {
         System.out.println("我要一个教练");
         try {
             Thread.sleep(2000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("教练来了: " + Thread.currentThread().getName());
         System.out.println("教我游泳,交完后,教练回到了游泳池");
     }
 }

线程池测试类:

 public class ThreadPoolDemo {
     public static void main(String[] args) {
         // 创建线程池对象
         ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
         // 创建Runnable实例对象
         MyRunnable r = new MyRunnable();
 ​
         //自己创建线程对象的方式
         // Thread t = new Thread(r);
         // t.start(); ---> 调用MyRunnable中的run()
 ​
         // 从线程池中获取线程对象,然后调用MyRunnable中的run()
         service.submit(r);
         // 再获取个线程对象,调用MyRunnable中的run()
         service.submit(r);
         service.submit(r);
         // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
         // 将使用完的线程又归还到了线程池中
         // 关闭线程池
         //service.shutdown();
     }
 }

Callable测试代码:

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

 public class ThreadPoolDemo2 {
     public static void main(String[] args) throws Exception {
         // 创建线程池对象
         ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
 ​
         // 创建Runnable实例对象
         Callable<Double> c = new Callable<Double>() {
             @Override
             public Double call() throws Exception {
                 return Math.random();
             }
         };
 ​
         // 从线程池中获取线程对象,然后调用Callable中的call()
         Future<Double> f1 = service.submit(c);
         // Futur 调用get() 获取运算结果
         System.out.println(f1.get());
 ​
         Future<Double> f2 = service.submit(c);
         System.out.println(f2.get());
 ​
         Future<Double> f3 = service.submit(c);
         System.out.println(f3.get());
     }
 }

4.4 线程池的练习

需求: 使用线程池方式执行任务,返回1-n的和

分析: 因为需要返回求和结果,所以使用Callable方式的任务

代码:

 public class Demo04 {
     public static void main(String[] args) throws ExecutionException, InterruptedException {
         ExecutorService pool = Executors.newFixedThreadPool(3);
 ​
         SumCallable sc = new SumCallable(100);
         Future<Integer> fu = pool.submit(sc);
         Integer integer = fu.get();
         System.out.println("结果: " + integer);
         
         SumCallable sc2 = new SumCallable(200);
         Future<Integer> fu2 = pool.submit(sc2);
         Integer integer2 = fu2.get();
         System.out.println("结果: " + integer2);
 ​
         pool.shutdown();
     }
 }

SumCallable.java

 public class SumCallable implements Callable<Integer> {
     private int n;
 ​
     public SumCallable(int n) {
         this.n = n;
     }
 ​
     @Override
     public Integer call() throws Exception {
         // 求1-n的和?
         int sum = 0;
         for (int i = 1; i <= n; i++) {
             sum += i;
         }
         return sum;
     }
 }

第5章 死锁

5.1 什么是死锁

在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了。

5.2 产生死锁的条件

1.有多把锁 2.有多个线程 3.有同步代码块嵌套

5.3 死锁代码

 public class Demo05 {
     public static void main(String[] args) {
         MyRunnable mr = new MyRunnable();
 ​
         new Thread(mr).start();
         new Thread(mr).start();
     }
 }
 ​
 class MyRunnable implements Runnable {
     Object objA = new Object();
     Object objB = new Object();
 ​
     /*
     嵌套1 objA
     嵌套1 objB
     嵌套2 objB
     嵌套1 objA
      */
     @Override
     public void run() {
         synchronized (objA) {
             System.out.println("嵌套1 objA");
             synchronized (objB) {// t2, objA, 拿不到B锁,等待
                 System.out.println("嵌套1 objB");
             }
         }
 ​
         synchronized (objB) {
             System.out.println("嵌套2 objB");
             synchronized (objA) {// t1 , objB, 拿不到A锁,等待
                 System.out.println("嵌套2 objA");
             }
         }
     }
 }

注意:我们应该尽量避免死锁

第6章 可见性

6.1 看程序说结果

 public class VolatileThread extends Thread {
 ​
     // 定义成员变量
     private boolean flag = false ;
     
     public boolean isFlag() { 
         return flag;
     }
 ​
     @Override
     public void run() {
         try {
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 ​
         // 将flag的值更改为true
         this.flag = true ;
         System.out.println("flag=" + flag);
     }
 }
 ​
 public class VolatileThreadDemo {// 测试类
     
     public static void main(String[] args) {
         // 创建VolatileThread线程对象
         VolatileThread volatileThread = new VolatileThread() ;
         volatileThread.start();
 ​
         // main方法
         while(true) {
             if(volatileThread.isFlag()) {
                 System.out.println("执行了======");
             }
         }
     }
 }

结果:

我们看到,VolatileThread线程中已经将flag设置为true,但main()方法中始终没有读到,从而没有打印。

6.2 JMM

概述:JMM(Java Memory Model) Java内存模型,是java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

关于JMM的一些同步的约定:

1.线程解锁前,必须把共享变量立刻刷回主存

2.线程加锁前,必须读取主存中的最新值到工作内存中

3.加锁和解锁是同一把锁

6.3 问题分析

  1. VolatileThread线程从主内存读取到数据放入其对应的工作内存

  2. 将flag的值更改为true,但是这个时候flag的值还没有写回主内存

  3. 此时main方法读取到了flag的值为false

  4. 当VolatileThread线程将flag的值写回去后,但是main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值,所以while(true)读取到的值一直是false。(如果有一个时刻main线程从主内存中读取到了主内存中flag的最新值,那么if语句就可以执行,main线程何时从主内存中读取最新的值,我们无法控制)

引发:

多个线程访问共享变量,会出现一个线程修改变量的值后,其他线程看不到变量最新值的情况。

总结:

并发编程下,多线程修改变量,会出现线程间变量的不可见性。

不可见性原因:

每个线程都有自己的工作内存,线程都是从主内存拷贝共享变量的副本值。

每个线程都是在自己的工作内存中操作共享变量的。

6.4 问题处理

方式一:加锁synchronized

使用synchronized关键字:

    // main方法
    while(true) {
        synchronized (volatileThread) {
            if(volatileThread.isFlag()) {
                System.out.println("执行了======");
            }
        }
    }

工作原理:

某一个线程进入synchronized代码块前后,执行过程入如下:

a.线程获得锁

b.清空工作内存

c.从主内存拷贝共享变量最新的值到工作内存成为副本

d.执行代码

e.将修改后的副本的值刷新回主内存中

f.线程释放锁

方式二:volatile关键字

使用volatile关键字:

    private volatile boolean flag ;

工作原理:

1.VolatileThread线程从主内存读取到数据放入其对应的工作内存中

2.将flag的值更改为true,但是这个时候flag的值还没有写回主内存

3.此时main方法main方法读取到了flag的值为false

4.当VolatileThread线程将flag的值写回去后,失效其他线程对此变量副本

5.再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

现象:

并发编程下,多线程修改变量,会出现线程间变量的不可见性。

解决线程间变量的不可见性的方案有两种常见方式:

1.加锁

解决原因:会清空工作内存,读取主内存中最新值到工作内存中来。

2.对共享的变量进行volatile关键字修饰

解决原因:一旦一个线程中的变量,添加了volatile修饰符,其它线程可以立即读取到最新值

6.5 volatile与synchronized的区别

1).volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。

2).volatile保证数据的可见性,但是不保证原子性(即:多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制,实现线程安全。

3).从性能上说,volatile更好点,仅仅是对实现线程间变量的可见性上。

第7章 原子性

概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

7.1 看程序说结果

 public class VolatileAtomicThread1 implements Runnable {
     // 定义一个int类型的遍历
     private int count = 0 ;
 ​
     @Override
     public void run() {
         // 对该变量进行++操作,100次
         for(int x = 1 ; x <= 100 ; x++) {
             count++ ;                   
             System.out.println("count =========>>>> " + count);
         }
     }
 }
 ​
 public class VolatileAtomicThreadDemo1 {
     
     public static void main(String[] args) {
         // 创建VolatileAtomicThread对象
         VolatileAtomicThread1 volatileAtomicThread = new VolatileAtomicThread1() ;
 ​
         // 开启100个线程对count进行++操作
         for(int x = 1 ; x <= 100 ; x++) {
             new Thread(volatileAtomicThread).start();
         }
     }
 }

执行结果:不保证一定是10000

7.2 问题原理说明

以上问题主要是发生在count++操作上:

count++操作包含3个步骤:

  • 从主内存中读取数据到工作内存

  • 对工作内存中的数据进行++操作

  • 将工作内存中的数据写回到主内存

count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。

1)假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态。

2)线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100。

3)线程B工作内存中x执行了+1操作,但是未刷新到主内存中。

4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作。

5)线程B将101写入到主内存。

6)线程A将101写入到主内存。

虽然计算了2次,但是只对A进行了1次修改。

7.3 volatile原子性测试

代码测试

 // 定义一个int类型的变量
 private volatile int count = 0 ;

小结:在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。

在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。

volatile的使用场景

  • 开关控制

    利用可见性特点,控制某一段代码执行或者关闭(比如今天课程的第一个案例)。

  • 多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读

7.4 问题解决

方式一:使用锁机制synchronized

我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。

 public class VolatileAtomicThread2 implements Runnable {
     // 定义一个int类型的变量
     private volatile int count = 0 ;
     private static final Object obj = new Object();
 ​
     @Override
     public void run() {
         // 对该变量进行++操作,100次
         for(int x = 1 ; x <= 100 ; x++) {
             synchronized (obj) {
                 count++ ;
                 System.out.println("count =========>>>> " + count);
             }
         }
     }
 }
 ​
 public class VolatileAtomicThreadDemo2 {
     
     public static void main(String[] args) {
         // 创建VolatileAtomicThread对象
         VolatileAtomicThread2 volatileAtomicThread = new VolatileAtomicThread2() ;
 ​
         // 开启100个线程对count进行++操作
         for(int x = 1 ; x <= 100 ; x++) {
             new Thread(volatileAtomicThread).start();
         }
     }
 }

方式二:原子类

Java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

AtomicInteger

原子型Integer,可以实现原子更新操作

 public AtomicInteger():                 初始化一个默认值为0的原子型Integer
 public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
 ​
 int get():                               获取值
 int getAndIncrement():                   以原子方式将当前值加1,注意,这里返回的是自增前的值。
 int incrementAndGet():                   以原子方式将当前值加1,注意,这里返回的是自增后的值。
 int addAndGet(int data):                 以原子方式将输入的数值与实例中的值(AtomicInteger里的                                            value)相加,并返回结果。
 int getAndSet(int value):                以原子方式设置为newValue的值,并返回旧值。

案例改造

使用AtomicInteger对案例进行改造.

 public class VolatileAtomicThread3 implements Runnable {
     // 定义一个int类型的变量
     private AtomicInteger atomicInteger = new AtomicInteger() ;
 ​
     @Override
     public void run() {
         // 对该变量进行++操作,100次
         for(int x = 1; x <= 100 ; x++) {
             int i = atomicInteger.getAndIncrement();
             System.out.println("count =========>>>> " + i);
         }
     }
 }
 ​
 public class VolatileAtomicThreadDemo3 {
     public static void main(String[] args) {
         // 创建VolatileAtomicThread对象
         VolatileAtomicThread3 volatileAtomicThread = new VolatileAtomicThread3() ;
 ​
         // 开启100个线程对count进行++操作
         for(int x = 1 ; x <= 100 ; x++) {
             new Thread(volatileAtomicThread).start();
         }
     }
 }

7.5 原子类CAS机制实现线程安全

7.5.1 概述

CAS的全称是: Compare And Swap(比较再交换);是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。

CAS可以将read-modify-check-write转换为原子操作,这个原子操作直接由处理器保证。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

举例:

  1. 在内存地址V当中,存储着值为10的变量。

  1. 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

  1. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

  1. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

  1. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

  1. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

  1. 线程1进行SWAP,把地址V的值替换为B,也就是12。

可以使用AtomicInteger类中的public final boolean compareAndSet(int expect , int update)方法进行校验。

7.5.2 CAS总结

比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作,如果不是就一直循环。

缺点:

1.循环会耗时

2.一次性只能保证一个共享变量的原子性

3.会出现ABA问题(此时建议引入原子引用,使用带版本号的原子操作,例如:AtomicStampedReference中的getStamp()方法)

7.6 CAS与Synchronized:乐观锁与悲观锁

共同点:

CAS和Synchronized都可以保证多线程环境下共享数据的安全性。

不同点:

Synchronized是从悲观的角度出发(悲观锁)

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock也是一种悲观锁。性能较差!

CAS是从乐观的角度出发(乐观锁)

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

第8章 指令重排

8.1 什么是重排序?

重排序就是编译器或者CPU的代码的的结构重排排序,已达到最佳的执行效果。重排大概分为编译器重排,处理器重排。

 
// 编译器重排
 //优化前
 int x = 1;
 int y = 2;
 int a1 = x * 1;
 int b1 = y * 1;
 int a2 = x * 2;
 int b2 = y * 2;
 ​
 //优化后
 int x = 1;
 int y = 2;
 int a1 = x * 1;
 int a2 = x * 2;
 int b1 = y * 1;
 int b2 = y * 2;
 ​
 //经过这样的优化,可能对于CPU来说只要读取一次的x和y值。而原来的可能需要反复读取寄存器来交替x和y的值。


 // 处理器重排
 //初始化:
 int a = 0;
 int b = 0;
 int x = 0;
 int y = 0;
 ​
 //处理器A执行
 a = 1; //A1  写到缓冲区
 x = b; //A2  变量b还没有来得及刷新到内存中,先读 读到可能是0
 ​
 //处理器B执行
 b = 2; //B1  写到缓冲区
 y = a; //B2  变量a还没有来得及刷新到内存中,先读 读到可能是0

由于处理器有读、写缓存区,写缓存区没有及时刷新到内存,造成其他处理器读到的值不是最新的,使得处理器执行的读写操作与内存上反应出的顺序不一致。

上面这个例子,可能造成处理器A读到的b=0,处理器B读到的a=0。A1写a=1先写到处理器A的写缓存区中,此时内存中a=0。如果这时处理器B从内存中读a,读到的将是0。

8.2 代码案例

 public class Barrier {
 ​
     int a = 0;
     int b = 0;
     int x = 0;
     int y = 0;
 ​
     //创建单个线程的线程池
     private static ExecutorService executorService1= Executors.newSingleThreadExecutor();
     private static ExecutorService executorService2= Executors.newSingleThreadExecutor();
     private static ExecutorService executorService3= Executors.newSingleThreadExecutor();
 ​
     public static void main(String ... args){
         for (int i=0;i< 1000000;i++){
             //初始化
             Barrier barrier=new Barrier();
 ​
             //处理器A执行
             executorService1.submit(()->{
                 barrier.a = 1; //A1
                 barrier.x = barrier.b; //A2
                 print(barrier);
             });
 ​
             //处理器B执行
             executorService2.submit(()->{
                 barrier.b = 2; //B1
                 barrier.y = barrier.a; //B2
                 print(barrier);
             });
         }
     }
 ​
     public static void print(Barrier barrier){
         executorService3.submit(()->{
             if(barrier.x==0 && barrier.y==0){
 System.out.println("=======>"+barrier.a+" , "+barrier.b+" , "+barrier.x+" , "+barrier.y);
             }else {
 System.out.println(barrier.a+" , "+barrier.b+" , "+barrier.x+" , "+barrier.y);
             }
         });
     }
 ​
 }

此时运行结果, 可以看到,确实有可能都是0。那么我们有没有办法解决这个问题呢?这时候我们就要提到内存屏障了。

8.3 内存屏障

8.3.1 概述

为了解决上述问题,处理器还是使用提供了个武器——内存屏障指令(Memory Barrier):

  1. 写内存屏障(Store Memory Barrier):处理器将当前存储缓存的值写回主存,以阻塞的方式。

  2. 读内存屏障(Load Memory Barrier):处理器处理失效队列,以阻塞的方式。

通过加入内存屏障,保证了两个操作之间数据的可见性。 volatile关键字通过“内存屏障”来防止指令被重排序 , volatile会在读取数据前插入一个读屏障,写数据之后加入一个写屏障,所以,它可以避免CPU重排导致的问题,实现多线程之间数据的可见性。

8.3.2 内存屏障的主要类型

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。

下面是基于保守策略的JMM内存屏障插入策略: 在每个volatile写操作的前面插入一个StoreStore屏障。 在每个volatile写操作的后面插入一个StoreLoad屏障。 在每个volatile读操作的后面插入一个LoadLoad屏障。 在每个volatile读操作的前面插入一个LoadStore屏障。

内存屏障的主要类型(以下了解即可) 不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。 Java内存屏障主要有Load和Store两类。 对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据 对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存

对于Load和Store,在实际使用中,又分为以下四种:

LoadLoad 屏障 序列:Load1,Loadload,Load2 确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。

StoreStore 屏障 序列:Store1,StoreStore,Store2 确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。

LoadStore 屏障 序列: Load1; LoadStore; Store2 确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。

StoreLoad 屏障 序列: Store1; StoreLoad; Load2 确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。

8.3.3 示例

下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

下面我们通过具体的示例代码来说明:

 class VolatileBarrierExample {
     int a;
     volatile int v1 = 1;
     volatile int v2 = 2;
 ​
     void readAndWrite() {
         int i = v1;           // 第一个volatile读
         int j = v2;           // 第二个volatile读
         a = i + j;            //普通写
         v1 = i + 1;           // 第一个volatile写
         v2 = j * 2;           // 第二个 volatile写
     }
 ​
     …                    //其他方法
 }

针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

8.4 volatile总结

volatile是Java虚拟机提供的轻量级的同步机制

1.保证可见性

2.不保证原子性

3.禁止指令重排

volatile是可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生。

结束语:

通过本篇博客的介绍,我们可以看到Java多线程编程的重要性和复杂性。在编写多线程程序时,我们需要考虑许多因素,如线程安全、线程状态、线程池、死锁和并发三特性等。只有深入理解这些概念和特性,才能编写出高效、安全、可靠的多线程程序。

同时,我们还需要注意Java多线程编程的一些陷阱和常见问题。例如,死锁可能会导致程序停滞不前,而线程不安全可能会导致数据损坏或不一致。因此,我们需要谨慎地编写多线程程序,并遵循最佳实践和标准的设计模式。

最后,我希望这篇博客能够为您提供有用的信息和指导,帮助您更好地理解和应用Java多线程编程。无论您是初学者还是有经验的开发者,都可以从中获得启发和帮助。谢谢阅读!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值