Java多线程(吐血超详细整理)

这篇具有很好参考价值的文章主要介绍了Java多线程(吐血超详细整理)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. Java内存的堆(heap)、栈(stack)和方法区(method)

栈:调用方法将在栈中开辟内存,称为入栈(压栈)。

​ 栈内存存放基本类型值和引用数据类型的地址。

​ 栈内存中的数据,没有默认初始化值,需要手动设置。

​ 方法调用完成,栈内存立即释放,称为出栈(弹栈)

堆:用于存放使用new创建的对象或数组。

​ 所有的对象都有内存地址值。

​ 数据都有默认初始化值。

​ 堆内存中的对象不再被指向时,JVM启动垃圾回收机制,自动清除。

方法区:与Java堆一样,是各个线程共享的内存区域。

​ 存储已被Java虚拟机加载的类信息、常量、静态变量、以及编译器编译后的代码等。

public class User {
    int m ;
    public void demo() {
        int i;
        System.out.println(i);	            //编译时异常,局部变量没有初始化,及没有默认值
        System.out.println(m);	            //运行时,可以输出0
    }
}
public class TestDemo {
    public static void main(String[] args) {
        User user = new User();
        user.demo();
    }
}

java多线程,Java,java,jvm,开发语言

2. 多线程基本

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

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

2.1 并发与并行

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

java多线程,Java,java,jvm,开发语言

  • 单CPU并发操作。

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

      java多线程,Java,java,jvm,开发语言

  • 多CPU并行操作。

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

      java多线程,Java,java,jvm,开发语言

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

2.2 线程与进程

2.2.1 概述

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

    java多线程,Java,java,jvm,开发语言

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

    java多线程,Java,java,jvm,开发语言

2.2.2 进程

  • 一个应用程序,至少需要启动一个进程,也可以启动多个进程。

java多线程,Java,java,jvm,开发语言

java多线程,Java,java,jvm,开发语言

2.2.3 线程

  • 一个进程至少执行一个线程,也可以执行多个线程。

    java多线程,Java,java,jvm,开发语言

java多线程,Java,java,jvm,开发语言

2.2.4 进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

java多线程,Java,java,jvm,开发语言

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

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

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

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

2.2.5 线程调度:

  • 分时调度

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

  • 抢占式调度

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

    java多线程,Java,java,jvm,开发语言

2.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接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。

2.4 方案1:继承Thread

2.4.1 步骤

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

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

2.4.2 实现

代码如下:

测试类:

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

2.5 方案2:实现Runnable

2.5.1 步骤

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

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

2.5.2 实现

代码如下:

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 Demo02 {
    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()方法。

2.5.3 Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

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

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

2.6 方案3:实现Callable

  • 实现 java.util.concurrent.Callable接口的call方法,然后通过java.util.concurrent.FutureTask适配成Runable,再然后通过Thread进行执行。

    java多线程,Java,java,jvm,开发语言

  • 步骤:

    1. 编写实现类,实现java.util.concurrent.Callable接口
    2. 创建适配类,包装Callable实现类
    3. 创建Thread类,并调用start()启动线程
  • 实现类

    package com.czxy.thread.callable;
    
    import java.util.concurrent.Callable;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("call:" + System.currentTimeMillis());
            return 100;
        }
    }
    
  • 测试类

    package com.czxy.thread.callable;
    
    import java.util.concurrent.FutureTask;
    
    /**
     * @author txt
     * @email tanxintong99@163.com
     */
    public class TestCallable {
        public static void main(String[] args) throws Exception {
            //1 任务回调实现类
            MyCallable myCallable = new MyCallable();
            //2 适配器
            FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
            //3 启动线程
            new Thread(futureTask).start();
            //4 获得返回结果
            Integer result = futureTask.get();
            System.out.println(result);
    
        }
    }
    

2.7 匿名内部类方式

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

使用匿名内部类的方式实现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);
        }
   	}
}

3. 线程安全

3.1 理论基础

3.1.1 概述:高并发及线程安全

  • 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
  • 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。

3.1.2 多线程的运行机制

  • 当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。

  • 看一下简单的线程的代码:

    1. 一个线程类:
    public class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                System.out.println("小强: " + i);
            }
        }
    }
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) {
            //1.创建线程对象
            MyThread mt = new MyThread();
    
            //2.启动线程
            mt.start();
            for (int i = 0; i < 20; i++) {
                System.out.println("旺财: " + i);
            }
        }
    }
    
  • 启动后,内存的运行机制:

    java多线程,Java,java,jvm,开发语言

    java多线程,Java,java,jvm,开发语言

  • 多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题

3.2 安全性问题

3.2.1 可见性 visibility

  • 例如下面的程序,先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值。(主线程读取变量有延迟)

    java多线程,Java,java,jvm,开发语言

    1. 线程类:
    public class MyThread extends Thread {
        public static int a = 0;
        @Override
        public void run() {
            System.out.println("线程启动,休息2秒...");
            try {
                Thread.sleep(1000 * 2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("将a的值改为1");
            a = 1;
            System.out.println("线程结束...");
        }
    }
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) {
            //1.启动线程
            MyThread t = new MyThread();
            t.start();
    
            //2.主线程继续
            while (true) {
                if (MyThread.a == 1) {
                    System.out.println("主线程读到了a = 1");
                }
            }
        }
    }
    
    1. 启动后,控制台打印:

java多线程,Java,java,jvm,开发语言

3.2.2 有序性

  • 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:

    ​ int a = 10; //1

    ​ int b = 20; //2

    ​ int c = a + b; //3

    第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。

  • 但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:

    java多线程,Java,java,jvm,开发语言

    多线程环境下,我们通常不希望对一些代码进行重排的!!

3.2.3 原子性 atomicity

请看以下示例:

1.制作线程类

public class MyThread extends  Thread {
    public static int a = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            a++;
        }
        System.out.println("修改完毕!");
    }
}

2.制作测试类

public class Demo {
  public static void main(String[] args) {
      /*
          概述:所谓的原子性是指在一次操作或者多次操作中,
              要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,
              要么所有的操作都不执行,多个操作是一个不可以分割的整体。
          演示高并发原子性问题:
              例如: 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
                  最终期望a的值为:200000
          出现高并发原子性问题的原因:虽然计算了2次,但是只对a进行了1次修改

       */
      // 创建并启动子线程
      MyThread mt = new MyThread();
      mt.start();

      // a变量自增3万次
      for (int i = 0; i < 100000; i++) {
          mt.a++;
      }

      // 暂定3秒,为了保证子线程执行完毕
      try {
          Thread.sleep(3000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }

      System.out.println("最终a的值为:"+mt.a);// 期望:200000
  }
}

原因:两个线程访问同一个变量a的代码不具有"原子性"

3.3 volatile关键字

3.3.1 什么是volatile关键字

  • volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。
  • volatile能解决变量的可见性、有序性;
  • volatile不能解决变量的原子性

3.3.2 volatile解决可见性

将3.2.1的线程类MyThread做如下修改:

  1. 线程类:
public class MyThread extends Thread {
    public static volatile int a = 0;//增加volatile关键字
    @Override
    public void run() {
        System.out.println("线程启动,休息2秒...");
        try {
            Thread.sleep(1000 * 2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("将a的值改为1");
        a = 1;
        System.out.println("线程结束...");
    }
}
  1. 测试类
public class Demo {
    public static void main(String[] args) {
        //1.启动线程
        MyThread t = new MyThread();
        t.start();

        //2.主线程继续
        while (true) {
            if (MyThread.a == 1) {
                System.out.println("主线程读到了a = 1");
            }
        }
    }
}

当变量被修饰为volatile时,会迫使线程每次使用此变量,都会去主内存获取,保证其可见性

3.3.3 volatile解决有序性

当变量被修饰为volatile时,会禁止代码重排

java多线程,Java,java,jvm,开发语言

3.3.4 volatile不能解决原子性

对于示例3.2.3,加入volatile关键字并不能解决原子性:

  1. 线程类:
public class MyThread extends  Thread {
    public static volatile int a = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            //线程1:取出a的值a=0(被暂停)
            a++;
            //写回
        }
        System.out.println("修改完毕!");
    }
}

  1. 测试类:
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //1.启动两个线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.start();
        t2.start();

        Thread.sleep(1000);
        System.out.println("获取a最终值:" + MyThread.a);//最终结果仍然不正确。

    }
}

所以,volatile关键字只能解决"变量"的可见性、有序性问题,并不能解决原子性问题

3.4 原子类

3.4.1 原子类概述

  • 在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:

    1. java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
    2. java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
    3. java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;

它们可以保证对“变量”操作的:原子性、有序性、可见性。

3.4.2 AtomicInteger类示例

我们可以通过AtomicInteger类,来看看它们是怎样工作的

  1. 线程类:
public class MyThread extends  Thread {
    //public static volatile int a = 0;//不直接使用基本类型变量

    //改用"原子类"
    public static AtomicInteger a = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
			// a++;
            a.getAndIncrement();//先获取,再自增1:a++
        }
        System.out.println("修改完毕!");
    }
}

  1. 测试类:
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //1.启动两个线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.start();
        t2.start();

        Thread.sleep(1000);
        System.out.println("获取a最终值:" + MyThread.a.get());
    }
}

我们能看到,无论程序运行多少次,其结果总是正确的!

3.4.3 AtomicInteger类的工作原理-CAS机制

  • 先来看一下调用过程:

java多线程,Java,java,jvm,开发语言

  • 在Unsafe类中,调用了一个:compareAndSwapInt()方法,此方法的几个参数:

    • var1:传入的AtomicInteger对象
    • var2:AtommicInteger内部变量的偏移地址
    • var5:之前取出的AtomicInteger中的值;
    • var5 + var4:预期结果

    此方法使用了一种"比较并交换(Compare And Swap)"的机制,它会用var1和var2先获取内存中AtomicInteger中的值,然后和传入的,之前获取的值var5做一下比较,也就是比较当前内存的值和预期的值是否一致,如果一致就修改为var5 + var4,否则就继续循环,再次获取AtomicInteger中的值,再进行比较并交换,直至成功交换为止。

  • compareAndSwapInt()方法是"线程安全"的。

  • 我们假设两个线程交替运行的情况,看看它是怎样工作的:

    • 初始AtomicInteger的值为0

    • 线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0

    • 线程A被暂停

    • 线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0

    • 线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)

    • 线程B成功将AtomicInteger中的值改为1

    • 线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)

      此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为0,比较失败,返回false,继续循环。

    • 线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1

    • 线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)

      此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为1,比较成功,将其修改为var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。

  • CAS机制也被称为:乐观锁。因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会导致CAS失败,而再次循环。

3.4.4 AtomicIntegerArray类示例

  • 常用的数组操作的原子类:
    1). java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类。

    2). java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。

    3). java.utio.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。

  • 数组的多线程并发访问的安全性问题:

    1. 线程类:
    public class MyThread extends Thread {
        public static int[] arr = new int[1000];
    
        @Override
        public void run() {
            for (int i = 0; i < intArray.length; i++) {
                intArray[i]++;
            }
        }
    }
    
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                new MyThread().start();//创建1000个线程,每个线程为数组的每个元素+1
            }
    
            Thread.sleep(1000 * 5);//让所有线程执行完毕
    
            System.out.println("主线程休息5秒醒来");
            for (int i = 0; i < MyThread.intArray.length; i++) {
                System.out.println(MyThread.intArray[i]);
            }
        }
    }
    
    

    正常情况,数组的每个元素最终结果应为:1000,而实际打印:

    1000
    1000
    1000
    1000
    999
    999
    999
    999
    999
    999
    999
    999
    1000
    1000
    1000
    1000
    

    可以发现,有些元素并不是1000.

  • 为保证数组的多线程安全,改用AtomicIntegerArray类,演示:

    1. 线程类:
    public class MyThread extends Thread {
        private static int[] intArray = new int[1000];//定义一个数组
        //改用原子类,使用数组构造
        public static AtomicIntegerArray arr = new AtomicIntegerArray(intArray);
        @Override
        public void run() {
            for (int i = 0; i < arr.length(); i++) {
                arr.addAndGet(i, 1);//将i位置上的元素 + 1
            }
        }
    }
    
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                new MyThread().start();
            }
            Thread.sleep(1000 * 5);//让所有线程执行完毕
    
            System.out.println("主线程休息5秒醒来");
            for (int i = 0; i < MyThread.arr.length(); i++) {
                System.out.println(MyThread.arr.get(i));
            }
        }
    }
    

    先在能看到,每次运行的结果都是正确的。

3.5 synchronized关键字

3.5.1 多行代码的原子性问题

  • 之前的AtomicInteger类只能保证"变量"的原子性操作,而对多行代码进行"原子性"操作,使用AtomicInteger类就不能达到效果了。

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

    电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共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--);
            }
            if(ticket == 0) {
                System.out.println("售罄");
                break;
            }
        }
    }
}

测试类:

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

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

java多线程,Java,java,jvm,开发语言

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

  1. 相同的票数,比如5这张票被卖了两回。
  2. 不存在的票,比如0票与-1票,是不存在的。

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

线程安全问题都是由全局变量及静态变量引起的。而每个线程操作这个变量都需要很多步骤:获取变量的值、打印变量的值、更改变量的值,而一个线程在执行某一步骤时都可能被暂停,而另一个线程会执行,这同样会导致多个线程访问同一个变量,最终导致这个变量的值不准确。

3.5.2 synchronized关键字概述

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

  • synchronized有几种使用方式:
    a).同步代码块

    b).同步方法【常用】

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

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

根据案例简述:

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

3.5.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--);
				}
                if(ticket == 0) {
                	System.out.println("售罄");
                	break;
            	}
			}
		}
	}
}

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

3.5.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();
            if( ticket == 0 ) {
                System.out.println("售罄");
                break;
            }
		}
	}
	
	/*
	 * 锁对象 是 谁调用这个方法 就是谁 
	 *   隐含 锁对象 就是  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--);
        }
	}
}

3.5.5 Lock锁

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

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

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

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

  • 实现类:

    • ReentrantLock:重入锁也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁。
    • NonReentrantLock:不可重入锁
    • ReentrantReadWriteLock:读写锁,可以分别获取读锁或写锁。
      • 读锁使用共享模式;写锁使用独占模式;
      • 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
      • 当有读锁时,写锁就不能获得;
      • 而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。

使用如下:

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();
            if(ticket == 0) {
                System.out.println("售罄");
                break;
            }
		}
	}
}

4. JUC并发包

JUC就是java.util.concurrent工具包的简称。这是一个处理线程的工具包。

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

4.1 CopyOnWriteArrayList

  • ArrayList的线程不安全:

    1. 定义线程类:
    public class MyThread extends Thread {
        public static List<Integer> list = new ArrayList<>();//线程不安全的
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
            System.out.println("添加完毕!");
        }
    }
    
    
    1. 定义测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
    
            t1.start();
            t2.start();
    
            Thread.sleep(1000);
    
            System.out.println("最终集合的长度:" + MyThread.list.size());
        }
    }
    
    

    最终结果可能会抛异常,或者最终集合大小是不正确的。

  • CopyOnWriteArrayList是线程安全的:

    1. 定义线程类:
    public class MyThread extends Thread {
    //    public static List<Integer> list = new ArrayList<>();//线程不安全的
        //改用:线程安全的List集合:
        public static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
            System.out.println("添加完毕!");
        }
    }
    
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
    
            t1.start();
            t2.start();
    
            Thread.sleep(1000);
    
            System.out.println("最终集合的长度:" + MyThread.list.size());
        }
    }
    
    

    结果始终是正确的。

4.2 CopyOnWriteArraySet

  • HashSet仍然是线程不安全的:

    1. 线程类:
    public class MyThread extends Thread {
    	public static Set<Integer> set = new HashSet<>();//线程不安全的
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                set.add(i);
            }
            System.out.println("添加完毕!");
        }
    }
    
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            MyThread t1 = new MyThread();
            t1.start();
    
            //主线程也添加10000个
            for (int i = 10000; i < 20000; i++) {
                MyThread.set.add(i);
    
            }
            Thread.sleep(1000 * 3);
            System.out.println("最终集合的长度:" + MyThread.set.size());
        }
    }
    
    

    最终结果可能会抛异常,也可能最终的长度是错误的!!

  • CopyOnWriteArraySet是线程安全的:

    1. 线程类:
    public class MyThread extends Thread {
    //    public static Set<Integer> set = new HashSet<>();//线程不安全的
        //改用:线程安全的Set集合:
        public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
    
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                set.add(i);
            }
            System.out.println("添加完毕!");
        }
    }
    
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            MyThread t1 = new MyThread();
            t1.start();
    
            //主线程也添加10000个
            for (int i = 10000; i < 20000; i++) {
                MyThread.set.add(i);
    
            }
            Thread.sleep(1000 * 3);
            System.out.println("最终集合的长度:" + MyThread.set.size());
        }
    }
    
    

    可以看到结果总是正确的!!

4.3 ConcurrentHashMap

  • HashMap是线程不安全的。

    1. 线程类:
    public class MyThread extends Thread {
        public static Map<Integer, Integer> map = new HashMap<>();
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                map.put(i, i);
            }
        }
    }
    
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            MyThread t1 = new MyThread();
            t1.start();
    
            for (int i = 10000; i < 20000 ; i++) {
                MyThread.map.put(i, i);
    
            }
            Thread.sleep(1000 * 2);
    
            System.out.println("map最终大小:" + MyThread.map.size());
        }
    }
    
    

    运行结果可能会出现异常、或者结果不准确!!

  • Hashtable是线程安全的,但效率低:

    我们改用JDK提供的一个早期的线程安全的Hashtable类来改写此例,注意:我们加入了"计时"。

    1. 线程类:
    public class MyThread extends Thread {
        public static Map<Integer, Integer> map = new Hashtable<>();
        @Override
        public void run() {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                map.put(i, i);
            }
            long end = System.currentTimeMillis();
            System.out.println((end - start) + " 毫秒");
        }
    }
    
    
    1. 测试类:
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                new MyThread().start();//开启1000个线程
            }
    
            Thread.sleep(1000 * 20);//由于每个线程执行时间稍长,所以这里多停顿一会
    
            System.out.println("map的最终大小:" + MyThread.map.size());
           
        }
    }
    
    
    1. 最终打印结果:
    ...
    ...
    15505 毫秒
    15509 毫秒
    15496 毫秒
    15501 毫秒
    15539 毫秒
    15540 毫秒
    15542 毫秒
    15510 毫秒
    15541 毫秒
    15502 毫秒
    15533 毫秒
    15647 毫秒
    15544 毫秒
    15619 毫秒
    map的最终大小:100000
    

    能看到结果是正确的,但耗时较长。

  • 改用ConcurrentHashMap

    1. 线程类:
    public class MyThread extends Thread {
        public static Map<Integer, Integer> map = new ConcurrentHashMap<>();
    
        @Override
      public void run() {
            long start = System.currentTimeMillis();
          for (int i = 0; i < 100000; i++) {
                map.put(i, i);
            }
            long end = System.currentTimeMillis();
            System.out.println((end - start) + " 毫秒");
        }
    }
    
    1. 测试类:
  public class Demo {
    public static void main(String[] args) throws InterruptedException {
          for (int i = 0; i < 1000; i++) {
              new MyThread().start();
          }
  
          Thread.sleep(1000 * 20);
  
          System.out.println("map的最终大小:" + MyThread.map.size());
      }
  }
  1. 最终结果:
...
...
3995 毫秒
3997 毫秒
4007 毫秒
4007 毫秒
4008 毫秒
4010 毫秒
4019 毫秒
4022 毫秒
4026 毫秒
3985 毫秒
4152 毫秒
4292 毫秒
map的最终大小:100000

可以看到效率提高了很多!!!

  • HashTable效率低下原因:
public synchronized V put(K key, V value) 
public synchronized V get(Object key)

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

java多线程,Java,java,jvm,开发语言

ConcurrentHashMap高效的原因:CAS + 局部(synchronized)锁定

java多线程,Java,java,jvm,开发语言

4.4 CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造方法:

public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象

CountDownLatch重要方法:

public void await() throws InterruptedException// 让当前线程等待
public void countDown()	// 计数器进行减1
  • 示例
    1). 制作线程1:
public class ThreadA extends Thread {
    private CountDownLatch down ;
    public ThreadA(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("A");
        try {
            down.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C");
    }
}

2). 制作线程2:

public class ThreadB extends Thread {
    private CountDownLatch down ;
    public ThreadB(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("B");
        down.countDown();
    }
}

3).制作测试类:

public class Demo {
    public static void main(String[] args) {
        CountDownLatch down = new CountDownLatch(1);//创建1个计数器
        new ThreadA(down).start();
        new ThreadB(down).start();
    }
}

4). 执行结果:
会保证按:A B C的顺序打印。

说明:

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch。

await()方法的线程阻塞状态解除,继续执行。

4.5 CyclicBarrier

4.5.1 概述

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

例如:公司召集5名员工开会,等5名员工都到了,会议开始。

我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。

CyclicBarrier构造方法:

public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景

CyclicBarrier重要方法:

public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
  • 示例代码:
    1). 制作员工线程:
public class PersonThread extends Thread {
	private CyclicBarrier cbRef;
	public PersonThread(CyclicBarrier cbRef) {
		this.cbRef = cbRef;
	}
	@Override
	public void run() {
		try {
			Thread.sleep((int) (Math.random() * 1000));
			System.out.println(Thread.currentThread().getName() + " 到了! ");
			cbRef.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}

2). 制作开会线程:

public class MeetingThread extends Thread {
    @Override
    public void run() {
        System.out.println("好了,人都到了,开始开会......");
    }
}

3). 制作测试类:

public class Demo {
	public static void main(String[] args) {
		CyclicBarrier cbRef = new CyclicBarrier(5, new MeetingThread());//等待5个线程执行完毕,再执行MeetingThread
		PersonThread p1 = new PersonThread(cbRef);
		PersonThread p2 = new PersonThread(cbRef);
		PersonThread p3 = new PersonThread(cbRef);
		PersonThread p4 = new PersonThread(cbRef);
		PersonThread p5 = new PersonThread(cbRef);
		p1.start();
		p2.start();
		p3.start();
		p4.start();
		p5.start();
	}
}

4). 执行结果:

java多线程,Java,java,jvm,开发语言

4.5.2 使用场景

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

4.6 Semaphore

Semaphore的主要作用是控制线程的并发数量。

synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

Semaphore构造方法:

public Semaphore(int permits)						permits 表示许可线程的数量
public Semaphore(int permits, boolean fair)			fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

Semaphore重要方法:

public void acquire() throws InterruptedException	表示获取许可
public void release()								release() 表示释放许可
  • 示例一:同时允许1个线程执行

1). 制作一个Service类:

public class Service {
    private Semaphore semaphore = new Semaphore(1);
    //1表示许可的意思,表示最多允许1个线程执行acquire()和release()之间的内容
    
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()
                    + "   结束 时间=" + System.currentTimeMillis());
            semaphore.release();
			//acquire()和release()方法之间的代码为"同步代码"
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2). 制作线程类:

public class ThreadA extends Thread {
	private Service service;
	public ThreadA(Service service) {
		super();
		this.service = service;
	}
	@Override
	public void run() {
		service.testMethod();
	}
}

3). 测试类:

public class Demo {
	public static void main(String[] args) {
		Service service = new Service();
        //启动5个线程
		for (int i = 1; i <= 5; i++) {
			ThreadA a = new ThreadA(service);
			a.setName("线程 " + i);
			a.start();//5个线程会同时执行Service的testMethod方法,而某个时间段只能有1个线程执行
		}
	}
}

4). 结果:

java多线程,Java,java,jvm,开发语言

  • 示例二:同时允许2个线程同时执行
    1). 修改Service类,将new Semaphore(1)改为2即可:
public class Service {
    private Semaphore semaphore = new Semaphore(2);
    //2表示许可的意思,表示最多允许2个线程执行acquire()和release()之间的内容
    
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName()
                    + "   结束 时间=" + System.currentTimeMillis());
            semaphore.release();
			//acquire()和release()方法之间的代码为"同步代码"
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2). 再次执行结果:

java多线程,Java,java,jvm,开发语言

4.7 Exchanger

4.7.1 概述

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

Exchanger构造方法:

public Exchanger()

Exchanger重要方法:

public V exchange(V x)
  • 示例一:exchange方法的阻塞特性

1).制作线程A,并能够接收一个Exchanger对象:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
			System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));

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

}

2). 制作main()方法:

public class Demo {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

3).执行结果:

java多线程,Java,java,jvm,开发语言

  • 示例二:exchange方法执行交换

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
			System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

2).制作线程B:

public class ThreadB extends Thread {
	private Exchanger<String> exchanger;
	public ThreadB(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程B欲传递值'礼物B'给线程A,并等待线程A的值...");
			System.out.println("在线程B中得到线程A的值=" + exchanger.exchange("礼物B"));

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

3).制作测试类:

public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		ThreadB b = new ThreadB(exchanger);
		a.start();
		b.start();
	}
}

4).执行结果:

java多线程,Java,java,jvm,开发语言

  • 示例三:exchange方法的超时

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
    
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
    
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值,只等5秒...");
			System.out.println("在线程A中得到线程B的值 =" + exchanger.exchange("礼物A",5, TimeUnit.SECONDS));
			System.out.println("线程A结束!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.out.println("5秒钟没等到线程B的值,线程A结束!");
		}
	}
}

2).制作测试类:

public class Run {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

3).测试结果:

java多线程,Java,java,jvm,开发语言

4.7.2 使用场景

使用场景:可以做数据校对工作

需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,

并对两个文件数据进行校对,看看是否录入一致,

5. 线程池方式

5.1 线程池的思想

java多线程,Java,java,jvm,开发语言

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

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

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

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

5.2 线程池概念

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

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

java多线程,Java,java,jvm,开发语言

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

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

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

5.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;
    }
}

6. 死锁

6.1 什么是死锁

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

6.2 产生死锁的条件

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

6.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");
            }
        }
    }
}

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

7. 线程状态

7.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。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

java多线程,Java,java,jvm,开发语言

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

package demo25_state;

/**
 * @author txt
 * @email tanxintong99@163.com
 */
public class Demo25 {
    private static  Object obj = new Object();
    public static void main(String[] args) throws Exception {
        // 1 任务
        Thread thread25 = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    synchronized ("") {
                        "".wait();
                    }
                    System.out.println("我执行了,准备获得锁");
                    synchronized (obj) {
                        System.out.println("获得锁");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        //2 获得新创建状态
        System.out.println(thread25.getState());            //NEW
        //3 运行态
        thread25.start();
        System.out.println(thread25.getState());            //RUNNABLE
        Thread.sleep(1000);
        //4 计时等待态
        System.out.println(thread25.getState());            //TIMED_WAITING
        Thread.sleep(2000);
        //5 等待态
        System.out.println(thread25.getState());            //WAITING

        synchronized (obj) {
            //6 5秒唤醒
            Thread.sleep(5000);
            synchronized ("") {
                "".notify();
            }
            //7 阻塞态
            Thread.sleep(1000);
            System.out.println(thread25.getState());        //BLOCKED
        }
    }
}

7.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秒后再继续执行。

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

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

=========================================================

后记

java多线程,Java,java,jvm,开发语言

好啦,以上就是本期全部内容,能看到这里的人呀,都是能人

十年修得同船渡,大家一起点关注。

我是♚焕蓝·未来,感谢各位【能人】的:点赞收藏评论,我们下期见!

各位能人们的支持就是♚焕蓝·未来前进的巨大动力~

注:如果本篇Blog有任何错误和建议,欢迎能人们留言!文章来源地址https://www.toymoban.com/news/detail-841558.html

到了这里,关于Java多线程(吐血超详细整理)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 软件测试禅道是什么?项目管理工具详细解答(精细)吐血整理

    禅道是一个测试管理工具,可以在里面进行项目管理以及bug用例管理,是一个非常好用的管理工具 当测试环境搭建完成后,测试人员将在自己搭建的环境上执行测试用例,开展测试工作 测试人员在执行测试用例的过程中,如发现实际结果与预期结果不一致, 则意味着出现

    2023年04月21日
    浏览(50)
  • Java/Python/Go不同开发语言在进程、线程和协程的设计差异

    在多线程项目开发时,最常用、最常遇到的问题是 1,线程、协程安全 2,线程、协程间的通信和控制 本文主要探讨不同开发语言go、java、python在进程、线程和协程上的设计和开发方式的异同。 进程 进程是 操作系统进行资源分配的基本单位,每个进程都有自己的独立内存空

    2024年01月23日
    浏览(47)
  • JVM(Java虚拟机)-史上最全、最详细JVM笔记

    一、JVM概述 1、1为什么要学习JVM 1、2虚拟机 1、3JVM作用 1、4JVM整体组成部分  二、JVM结构--类加载器 2、1类加载子系统 2、2类加载过程 2、2、1加载 2、2、2链接 2、2、3初始化 2、3类加载器分类 2.3.1 引导类加载器(启动类加载器 BootStrap ClassLoader) 2.3.2 扩展类加载器(Extension ClassL

    2024年02月03日
    浏览(63)
  • java语法(二)线程并发、Juit单元测试、反射机制、注解、动态代理、XML解析、JVM

    正则表达式验证网站 1、 ? :表示前边这个字符可以出现0次或者1次。例如下边 /used? 既可以匹配 use 也可以匹配 used 。 2、 * :匹配0个或者多个字符, * 号代表前边这个字符可以出现0次或者多次。例如 /ab*c 可以匹配 ac、abc、abbbbc 3、 + :与 * 号不同的是, + 需要前面这个字符

    2024年02月06日
    浏览(49)
  • 【JVM】详细解析java创建对象的具体流程

    目录 一、java创建对象的几种方式 1.1、使用new  1.2、反射创建对象 1.2.1、Class.newInstance创建对象 1.2.2、调用构造器再去创建对象Constructor.newInstance  1.3、clone实现 1.4、反序列化 二、创建对象的过程 2.1、分配空间的方式 1、指针碰撞 2、空闲列表 3、怎么选择分配方式 三、

    2024年02月15日
    浏览(54)
  • Java集合,超详细整理,适合新手入门

    指的是在集合里面放的是单个的对象 Collection 接口有两个重要的子接口 List、Set Collection提供了size()方法,List提供了get()方法 Collection接口的实现子类可以存放多个元素,每个元素可以是Object; Collection的实现类,有些可以存放重复的元素,有些不可以; Collection的实现类,有些

    2024年02月14日
    浏览(54)
  • JVM--- 垃圾收集器详细整理

    目录 一、垃圾收集需要考虑的三个事情: 二、垃圾回收针对的区域 三、如何判断对象已死         1.引用计数算法:         2.可达性分析算法 四、引用 五、生存还是死亡? 六、回收方法区 七、垃圾收集算法         1.分代收集理论 2.标记-清除算法         3.标记

    2024年02月19日
    浏览(69)
  • 【Java】JDK、JRE、JVM详细解读(区别和联系)

    大多数人刚接触Java的时候都会经常看到JDK、JRE、JVM,但可能一直迷迷糊糊,不知道它们之间真正的作用和含义,而了解JDK、JRE、JVM分别是什么及它们之间的关系有助于我们更加深刻的理解java语言的特性。 Java程序是运行在JVM(Java虚拟机)上的,在开发程序之前要配置Java开发环

    2023年04月13日
    浏览(81)
  • Java JVM分析利器JProfiler 结合IDEA使用详细教程

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 对于我们Java程序员而言,肯定需要对项目工程进行JVM监控分析,最终选择jprofiler,它可以远程链接,使用方便,功能也很强大! JProfiler是一个重量级的JVM监控工具,提供对JVM精确监控,其中堆遍历、

    2024年02月08日
    浏览(41)
  • Java Scanner 类,超详细整理,适合新手入门

    目录 一、什么是 Java Scanner 类? 二、引用数据类型 1、引用数据类型的定义 三、Scanner 类有哪些常用方法? hasNext()用法 四、next() 与 nextLine() 区别 next(): nextLine(): 五、使用 next 方法 五、使用 nextLine方法 Java Scanner 类是 Java 中一个用于读取用户输入的类,它可以从标准输入、

    2024年02月06日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包