文章目录


Java多线程

在之前学习 的过程中,我们使用的都是单线程,程序都是从main方法入口开始执行到程序结束,整个过程只能顺序执行,如果程序在某个过程出现问题,那么整个程序就会崩溃,所以单线程在一定程度上是具有脆弱性和局限性。单线程就好比是我们中午去吃饭的时候,只提供了一个打饭窗口给你,然后全校学生排队排到宿舍门口,绝对让你怀疑人生。那你可以感受到效率是极其低下的。但是多给你几个打饭窗口,分摊一下刚刚主道上的学生,这样一来同样都是实现打饭,但是效率会快很多,这就是多线程的概念。所谓多线程,就是指一个应用程序中有很多条并发执行的线索,每一个线索就成为是一个线程,他们会交替执行,彼此之间也可以进行通信。

一、线程概述


1.1 进程

了解线程之前,我们首先要理清进程的概念。搜了一下,网上给出了很专业的描述,but~ 看不懂!!!

【Java】多线程总结_多进程

简单来说在一个操作系统中,每个独立执行的程序都可以被称为一个进程,也就是指“正在运行的程序”,例如同时运行QQ、网易云、360安全卫士等。
在多任务操作系统中(能同时执行多个应用程序),可以查看当前系统中所有的进程,例如本人使用的是Win10,可以在资源管理器中来查看:

【Java】多线程总结_等待状态_02


在这里补充一下,在我们的多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边收听网易云音乐,一边可以打开4399小游戏畅游,但是实际上这些进程并不是在同一时刻运行的。在计算机上,所有的应用程序都是由CPU执行的,对于一个CPU来说,在某一个时间点只能够运行一个程序,也就是说只能够执行一个进程。出现上述现象的原因是,我们的操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间里可以执行某一个进程,然后会在下一段时间里切换到另一个进程中执行。由于CPU运行的速度很快,所以在很短的时间内能够切换到很多不同的进程运行,于是就出现了,我们一边…一边…

返回顶部


1.2 线程

  • 在多任务操作系统中,每个程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。操作系统中的每一个进程中都至少存在一个线程。当一个java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
  • 线程的分类
  • 单线程 — 运行代码过程中自上而下,没有出现多段程序代码交替运行
  • 多线程 — 运行的时候存在多个并发的程序代码

【Java】多线程总结_等待状态_03

返回顶部


二、线程的创建

java中实现多线程的三种方式:
        继承Thread类,重写run()方法
        实现Runable接口,重写run()方法
        实现Callable接口,重写call()方法


2.1 Thread类实现多线程

Thread类是java.lang包下的一个线程类,用来实现java多线程。通过继承Thread类的方式来实现多线程的主要步骤:
        1.创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法
        2.创建该子类的实例对象,并通过调用start()方法启动线程

package createThread;

// 1.定义一个继承Thread线程的子类
class newThread extends Thread{

//创建子线程类有参构造方法
public newThread(String name){
super(name);
}

// 重写Thread类的run()方法
public void run(){
int i = 0;
while(i++<5){
System.out.println(Thread.currentThread().getName()
+"的run()方法在运行");
}
}
}
public class myThread {
public static void main(String[] args) {

// 2.创建子类的实例对象
newThread thread1 = new newThread("thread1");
// 3.调用start()方法启动线程
thread1.start();

// 创建并启动另一个线程
newThread thread2 = new newThread("thread2");
thread2.start();
}
}

【Java】多线程总结_多线程_04


首先定义了一个继承Thread线程的子类,并且重写了Thread类的run()方法,其中currentThread()方法用于获取当前进程的对象,getName()用于获取进程的名称。然后通过main方法入口创建了两个进程实例,并制定进程名称thread1、thread2,最后调用对应实例的start()方法启动线程。通过运行可以看出两个线程都交替执行了run()方法,并不像单线程一样按照程序顺序依次执行。

返回顶部


2.2 Runnable接口实现多线程

在使用继承Thread类来实现多线程可以执行,但是由于java继承的特性—一个类只能继承一个父类(单继承),所以当一个类在已经继承了某个父类的情况下,就不能够在通过继承Theard类来实现多线程,此时可以考虑通过实现Runnable接口的方式来实现多线程。

使用Runable接口实现多线程的步骤:
        1.创建一个Runnable接口的实现类,并且重写接口的run()方法
        2.创建Runnable接口实现类的对象
        3.使用Thread有参构造方法来创建线程实例,并将Runnable实现类的实例作为参数传入
        4.调用线程实例的start()方法启动线程

package createThread;

class runnableTest implements Runnable{

@Override
public void run() {
int i = 0;
while (i++<5){
System.out.println(Thread.currentThread().getName()
+"的run()方法正在执行!");
}
}
}

public class runnableThread {

public static void main(String[] args) {

// 创建runnableTest的实例对象
runnableTest runnable = new runnableTest();
// 使用Thread的构造方法创建线程对象
Thread thread1 = new Thread(runnable,"thread1");
// 调用start方法
thread1.start();

// 创建另一个线程
Thread thread2 = new Thread(runnable,"thread2");
thread2.start();
}
}

【Java】多线程总结_接口实现_05


注意:Runnable接口中只有一个抽象的run()方法,属于函数式接口,可以直接使用Lamba表达式的方式显示,后面的callable接口也是一样。

返回顶部


2.3 Callable接口实现多线程

通过Thread类和Runnable接口实现多线程时都需要重写run()方法,但是由于该方法并没有返回值,因此无法从多个线程中获取返回结果。为了解决这个问题,从JDK1.5开始新添加了一个Callable接口,来满足既能创建多线程又可以有返回指的需求。

通过Callable接口实现多线程的方式与Ruannable接口相似,不过这里传入到Thread构造方法中的参数是Runnable接口的子类FutureTask对象,FutureTask对象中封装有带有返回值的Callable接口的实现类。基本步骤:
        1.创建一个Callable接口的实现类,并且重写Callable接口中的抽象方法call()
        2.创建Callable接口的实现类对象
        3.通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象
        4.使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例
        5.调用线程实例的start()方法启动线程

package createThread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class callableTest implements Callable{

@Override
public Object call() throws Exception {
int i = 0;
while (i++<5){
System.out.println(Thread.currentThread().getName()
+"的call()方法被执行了!");
}
return i;
}
}


public class callableThread {

public static void main(String[] args) throws InterruptedException, ExecutionException {

// 创建Callable接口实现类的实例对象
callableTest callable = new callableTest();
// 使用FutureTask封装接口实现类的实例对象
FutureTask<Object> ft1 = new FutureTask<>(callable);
// 使用Thread有参构造方法创建线程对象
Thread thread1 = new Thread(ft1,"thread1");
// 启动线程
thread1.start();

// 创建另一个线程对象
// 使用FutureTask封装接口实现类的实例对象
FutureTask<Object> ft2 = new FutureTask<>(callable);
// 使用Thread有参构造方法创建线程对象
Thread thread2 = new Thread(ft2,"thread2");
// 启动线程
thread2.start();

}
}

【Java】多线程总结_接口实现_06

关于FutureTask参见大佬的博文~

返回顶部


2.4 实现多线程的三种方式的对比

在多线程的创建时有继承Thread类和实现Runnable、Callable接口三种方法。实现Callable接口方式ed区别就是它可以进行值的返回,bignqie可以声明抛出异常。接下来主要针对Thread类和实现接口两大种方式区别。

假设售票厅有4个售票窗口,同时发售某日某系列车的100张票,,其中4个窗口可以看做是4个线程,打印输出每个窗口的售票信息。

package Distinction;

class window1 extends Thread{
// 设定票数
private int tickets = 100;

public void run(){
while(true){
if(tickets>0){
System.out.println(Thread.currentThread().getName()
+"窗口出售了第"+tickets--+"张票");
}
}
}
}

public class TicketWindow1 {
public static void main(String[] args) {

// 创建4个线程对象并启动,代表4个销售窗口
new window1().start();
new window1().start();
new window1().start();
new window1().start();

}
}

【Java】多线程总结_多进程_07

注意

1.通过继承Thread类来创建多线程时,根据以上的案例可以看出,它不能够共享同一个资源—tickets。通过结果不难发现,每创建一个窗口(线程),就相当于创建了一个程序。在每个线程运行的时候,每一个窗口都发售了100张票,并没有共享100张票,各自处理自己的线程。

2.当在创建线程的时候没有指定线程名,在运行的时候系统会默认给定线程名。

返回顶部


package Distinction;

class window2 implements Runnable{
// 设定票数
private int tickets = 100;

public void run(){
while(true){
if(tickets>0){
System.out.println(Thread.currentThread().getName()
+"窗口出售了第"+tickets--+"张票");
}
}
}
}

public class TicketWindow2 {
public static void main(String[] args) {
// 创建Runnable实现类的实例对象
window2 tw = new window2();

// 创建4个线程对象并启动,代表4个销售窗口
new Thread(tw,"窗口1").start();
new Thread(tw,"窗口2").start();
new Thread(tw,"窗口3").start();
new Thread(tw,"窗口4").start();

}
}

【Java】多线程总结_等待状态_08


如图所示,当实现Runnable、Callable接口的时候,只创建了一个接口实现类对象,然后创建了4个线程,在每个线程中都调用同一个对象的run()方法,这样就确保了每个线程访问的都是同一个tickets变量,共享100张票。

使用接口实现类相对于继承类的优点:

  • 适合多个线程去处理同一个共享资源的情况,把线程同程序代码、数据有效的分离很好的体现了面向对象的设计思想。
  • 可以避免java单继承带来的局限性。由于一个类只能继承一个父类,所以在当一个类继承了其他父类的情况下,只能够采取实现接口的方式来创建多线程

返回顶部


三、后台线程

通过程序的运行结果可以看出来,虽然创建的4个新线程的代码执行完毕后主线程跟着结束,但是整个java程序却并没有结束。对于java程序来说,只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中只有后台线程运行,这个进程就会结束。前台进程可以理解为新创建的线程,如果某个线程在启动之前调用了setDaemon(true)语句,那么这个线程就会变成一个后台线程。

package DamonThread;

class damon implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程正在运行!");
}
}

public class damonThread {
public static void main(String[] args) {

// 判断是否为后台进程
System.out.println("main线程是后台线程吗"+Thread.currentThread().isDaemon());

// 创建接口实现类的实例化对象
damon d = new damon();
// 调用Thread的含参构造方法创建线程
Thread thread = new Thread(d,"后台线程");
System.out.println("thread线程吗,默认是后台线程吗?"+thread.isDaemon());
// 将thread线程对象转换为后台线程
thread.setDaemon(true);
thread.start();

// 模拟主线程main的执行
for(int i=0;i<5;i++){
System.out.println(i);
}

}
}

【Java】多线程总结_等待状态_09

返回顶部


四、线程的生命周期及其状态转变

类似于创建对象,当再也没有新的索引指向该对象时,那么这个对象就会变成垃圾最终被回收站回收,也就是该对象的生命周期结束。线程也是如此,当new了一个新的线程时,他的生命周期就开始了,当线程中的代码正常执行完毕或抛出某个未捕获的异常或error时,该线程的生命周期就可谓是结束了。官方将线程的生命周期分为6个状态:NEW(新建状态)、RUNNABLE(可运行状态)、BKLOCKED(阻塞状态)、WAITING(等待状态)、TIMED_WAITING(定时等待状态)、TERMINATED(终止状态)。


【Java】多线程总结_多进程_10

NEW(新建状态)

创建一个新的线程后,该线程的生命周期就开始了,此时它还不能够运行。像创建其他的对象一样,仅仅有JVM虚拟机为期分配了存储空间,没有表现出任何线程的动态特征。


RUNNABLE(可运行状态)

当新建状态下的线程对象调用了start()方法后,就会从新建状态进入可运行状态。在可运行状态内部还可以分为两种状态:就绪状态(ready)、运行状态(running)。

就绪状态:线程对象使用start方法后,等待jvm的调度,此时线程并没有开始运行
运行状态:线程对象在获得jvm的调度后开始运行,如果此时CPU够多,将会允许多个线程并行运行。

BKLOCKED(阻塞状态)

处于运行状态的线程可能会因为某些原因而失去CPU的执行权,暂时停止运行进入阻塞状态。此时,jvm并不会给线程分配CPU,知道线程再次进入就绪状态,才有机会转换到运行状态。

线程进入阻塞状态的两种情况:
1.当线程A运行过程中,试图获取同步锁时,却被线程B获取,此时JVM把当前线程A存到对象的锁池中,线程A就会进入阻塞状态。
2.当线程运行过程中,发出I/O请求时,此刻该线程也会进入阻塞状态。


WAITING(等待状态)

当处于运行状态的线程调用了无时间参数限制的方法后,如wait\join等,就会将当前运行状态中的线程转化为等待状态。

处于等待状态的线程不能够立刻争夺CPU的使用权,必须等待其他线程执行特定的操作后,才有机会再次争夺CPU的使用权,将等待状态的线程转化为运行状态。

调用wait()方法 ---- 必须等待其他线程调用notify()或者notifyAll()方法唤醒当前正在等待状态中的线程。
调用join()方法 ---- 必须等待其他加入的线程终止


TIMED_WAITING(定时等待状态)

处于定时等待状态的线程同等待状态的线程处理方式相似,仅仅是运行线程调用了有时间参数限制的方法,结束等待状态除了上述情况就只有等待定时结束~


TERMINATED(终止状态)

线程的run()、call()方法正常执行完毕或者线程抛出了一个未捕获的异常、错误,线程就会进入终止状态。一旦进入终止状态,线程将不再拥有再次运行或转为其他状态的机会,该线程的生命周期就此终止。

返回顶部


五、线程的调度

程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU使用权,这种机制被称为线程的调度。

在计算机中,线程的调度有两种模型,分别是分时调度模型和抢占是调度模型。

  • 分时调度模型:让所有的线程轮流获取CPU使用权,并且平均分配每个线程占用的CPU时间片。
  • 抢占式调度模型:让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率较大。Java虚拟机默认采用抢占式调度模型。

5.1 线程的优先级

在程序中如果要对线程进行调度,最直接的方法就是设置线程的优先级。线程的优先级用1-10数字来表示,数越大优先级越高。同时还可以使用Thread类中的三个静态常量来表示现成的优先级:

常量

优先级

static int MAX_PRIORITY

10

static int MIN_PRIORITY

1

static int NORM_PRIORIY

5

在程序运行期间,每个处于就绪状态的线程都会具有自己的优先级。例如一般面方法就具有普通优先级。但是现成的优先级并不是一成不变的,可以通过Thread类的setPriority()来进行重新设定线程的优先级。该方法接收的是1-10之间的整数或者是三个静态常量。

package threadScheduling;

public class priority {

public static void main(String[] args) {

// 分别设置两个线程
Thread thread1 = new Thread(()->{
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"正在输出i:"+i);
}
});

Thread thread2 = new Thread(()->{
for(int j=0;j<10;j++){
System.out.println(Thread.currentThread().getName()+"正在输出j:"+j);
}
});

thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(8);

thread1.start();
thread2.start();
}
}

【Java】多线程总结_接口实现_11


在设置了两个线程之后,我们使用setPriority()方法修改线程的优先级,thread1设为1级,thread2设为8级,在运行程序时,线程2的优先级高于线程1,线程2会获得更多的机会优先执行。(上图只截取了部分,可以复制代码自行运行查看)

注意:
虽然java提供了1-10个优先级别,但是这些优先级实在系统支持的情况下所适用的。不同的操作系统对优先级的支持是不一样的,所以在设计多线程程序时,优先级只能够作为提高程序运行的手段,不能够能够更依赖于它。

返回顶部


5.2 线程休眠

如果在进行线程程序运行时,我们需要让线程优先级别低的先运行,是当前运行的线程暂停,可以使用sleep(long millis)方法,让当前运行的线程休眠,注意该方法在使用时会声明抛出InterruptedException异常,所以在书写时要捕获异常。

package threadScheduling;

public class priority {

public static void main(String[] args) {

// 分别设置两个线程
Thread thread1 = new Thread(()->{
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"正在输出i:"+i);
if(i==2){
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
});

Thread thread2 = new Thread(()->{
for(int j=0;j<10;j++){
System.out.println(Thread.currentThread().getName()+"正在输出j:"+j);
if(j==7){
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
});

thread1.start();
thread2.start();
}
}

【Java】多线程总结_等待状态_12


在创建了两个线程后我们都进行了判断,对线程1来说当执行到 ​​i==2​​​ 的时候进入休眠状态,此时线程2就会获得CPU的使用权,知道线程1的休眠期结束才会有机会获得CPU的使用权,线程2执行到 ​​j==7​​的时候进入休眠状态,接下来一样,但是线程休1眠期结束后,可以继续抢夺CPU资源。

注意:
Thread类提供了两种休眠的方法,这两种方法都带有休眠时间参数,当其他线程都终止后并不代表当前休眠状态的线程会立即执行,而是必须等休眠时间结束后,线程才会转换到就绪状态,才有机会争夺CPU的使用权。

返回顶部


5.3 线程让步

线程让步可以通过yield()方法来实现,该方法和sleep(long millis)相似,都可以让当前运行的线程暂停,区别在于yield()方法不会阻塞线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当线程使用了yield()方法后,当前运行的线程会进入就绪状态,优先级同级的或者比它高的线程会会获得执行的机会。

package threadScheduling;

import java.util.jar.JarOutputStream;

public class giveway extends Thread{

public giveway(String name){
super(name);
}

public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"------"+i);
if (i == 2) {
System.out.println("线程让步~~");
Thread.yield();
}
}
}

public static void main(String[] args) {

// 创建3个线程
Thread thread1 = new giveway("线程1");
Thread thread2 = new giveway("线程2");
Thread thread3 = new giveway("线程3");

thread1.setPriority(2);
thread2.setPriority(6);
thread3.setPriority(8);

thread2.start();
thread1.start();
thread3.start();
}
}

【Java】多线程总结_接口实现_13


从途中我们可以看出来,当线程1先到2让步后,线程2、3都是就绪状态,但是我设置的线程3的优先级高于线程2,所以线程3获取CPU使用权;线程3运行到2开始让步,线程2的优先级又高于线程1,所以线程2获取CPU使用权;当线程2运行到2让步时,线程3的优先级又高于线程1,所以会优先再次获取CPU的使用权。

返回顶部


5.4 线程插队

在现实生活中我们排队时不免会遇到插队的情况,线程中也是如此。在Thread类中提供了一个join()方法来实现这个功能。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞 ,直到被join()方法加入的线程执行完成后它才会继续运行。

package threadScheduling;

public class join implements Runnable{

@Override
public void run() {
for(int i =1;i<6;i++){
System.out.println(Thread.currentThread().getName()+"输入了"+i);
}
}

public static void main(String[] args) throws InterruptedException{
// 创建线程
Thread thread1 = new Thread(new join(),"thread1");
thread1.start();
for(int i=1;i<6;i++){
System.out.println(Thread.currentThread().getName()+"输入"+i);
if(i==2){
thread1.join();
}
}
}
}

【Java】多线程总结_接口实现_14

返回顶部


六、多线程同步

6.1 线程安全

package ThreadSecurity;

import org.omg.PortableServer.THREAD_POLICY_ID;

public class SaleThread implements Runnable{

private int tickets = 10;

@Override
public void run() {

while(true){ // 无限循环
if(tickets > 0){
try{
Thread.sleep(100); // 休眠模拟正常购票时的延时
}catch(Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "窗口正在出售第"+tickets--+"张票~");
}
}

}

public static void main(String[] args) {

SaleThread s = new SaleThread();
// 创建线程
Thread thread1 = new Thread(s,"窗口1");
Thread thread2 = new Thread(s,"窗口2");
Thread thread3 = new Thread(s,"窗口3");
Thread thread4 = new Thread(s,"窗口4");

thread1.start();
thread2.start();
thread3.start();
thread4.start();
}

}

【Java】多线程总结_等待状态_15


上述情况出现售票有负数的情况是因为线程出现了安全问题。在售票程序中添加了休眠sleep(100),模拟了正常情况下的延时。当售票号为1时,假设窗口1正在出售此票,而由于延时使得窗口1休眠;而此时其他窗口也进入了while循环判断,此时票号仍为1,也就是四个线程都进行了此次售票,当休眠结束后,每个线程(窗口)都会进行售票,于是票数递减,就出现了负数。

【Java】多线程总结_接口实现_16

返回顶部


6.2 同步代码块

由上面的案例我们可以了解到线程安全出现问题是由于多线程共享资源的时候,没有分清执行的状态而导致的,也就是说在共享资源时,多线程运行必须得保证共享资源的代码在同一时刻只能有一个执行访问共享的资源。为此,java中提供了线程同步机制,当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这段代码被称为同步代码。

synchronized(lock){
.....
}

这里的lock是一个锁对象,它是同步代码的关键。

当线程执行到同步代码块的时候,首先会检查所锁象的标志位,默认情况下位1,此时线程会执行同步代码,并将锁对象的标志位置为0。当一个新的线程执行到这段同步代码的时候,由于锁对象标志位为0,就会使新线程发生线程堵塞。只有等上一个线程执行完毕同步代码块,对象锁标志位变为1的时候,新线程才可以获取锁对象,执行同步代码块

package ThreadSecurity;

import java.util.jar.JarOutputStream;

public class SaleThread2 implements Runnable{

private int tickets = 10;
Object lock = new Object();
@Override
public void run() {

while (true){
// 同步代码块
synchronized (lock){
if(tickets > 0){
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口正在出售第"+tickets--+"张票~");
}
}
}
}

public static void main(String[] args) {

SaleThread2 s = new SaleThread2();
new Thread(s,"窗口1").start();
new Thread(s,"窗口2").start();
new Thread(s,"窗口3").start();
new Thread(s,"窗口4").start();
}
}

将有关tickets变量的操作全部都放到同步代码块synchronized(lock){…}中,将不会出现售票为负数的结果,因为执行同步代码块得到时候,每一时刻只能够有一个线程访问该共享资源,保证了多线程的安全。

【Java】多线程总结_java_17


注意:

同步代码块中的锁对象可以是任意类型的对象,但是多个线程共享的锁对象必须是同一个。既然由锁来控制多线程安全 — 每次只有一个线程运行,那么锁对象必须是同一个。并且锁对象的创建不能够写在run()方法之外,保证锁的唯一;否则每个线程运行时就会新建一个不同的锁。

返回顶部


6.3 同步方法

与同步代码块机制类似还有同步方法,顾名思义,就是使用synchronized定义一个方法,在某一时刻只允许一个线程访问该方法,访问该方法的其他线程将会发生阻塞,直到当前线程访问完毕之后,其他线程才有机会执行。

[修饰符] synchronized 返回值类型 方法名([参数列表]){....}
package ThreadSecurity;

public class SaleThread3 implements Runnable{

private int tickets = 10;

private synchronized void saleTickets(){

if(tickets > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口正在出售第"+ tickets-- +"张票~");
}

}

@Override
public void run() {
while (true){
this.saleTickets();
}
}

public static void main(String[] args) {

SaleThread3 s = new SaleThread3();
new Thread(s,"1").start();
new Thread(s,"2").start();
new Thread(s,"3").start();
new Thread(s,"4").start();

}
}

【Java】多线程总结_多进程_18


使用同步方法同样可以达到同步线程的效果,但是我们可以发现它与同步代码块的不同之处:没有定义任意对象作为锁。那么同步方法就没有同步锁了吗?肯定不是,同步方法也有锁,它的锁就是当前调用该同步方法的对象,在上述代码中就是this指向的对象。这样做的好处是同步方法被所有的线程所共享,方法所在的对象相对于所有的线程来说是唯一的。

返回顶部


6.4 同步锁

synchronized同步代码块和同步方法使用一种封闭式额锁机制,使用起来简单,但是也有一些限制,就是在运行时无法中断一个处于等候状态获取锁的线程,也无法通过轮询得到锁。

在JDK5之后,Java增加了一个Lock锁。lock锁与synchronized隐式锁在功能上基本相同,其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回,不在继续等待。

package Lock;

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

public class equalLock implements Runnable{

private int tickets =10;
private final Lock lock = new ReentrantLock(); // 定义一个Lock锁对象

@Override
public void run() {

while (true){
lock.lock(); // 对代码块进行加锁
if (tickets>0){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"窗口正在出售第"+tickets--+"张票~");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
}

public static void main(String[] args) {

equalLock e = new equalLock();
new Thread(e,"1号").start();
new Thread(e,"2号").start();
new Thread(e,"3号").start();
new Thread(e,"4号").start();

}

}

通过Lock接口的实现类ReentrantLock来创建一个锁对象,并通过Lock锁对象的lock()、unlock()方法对核心代码进行上锁和解锁。

【Java】多线程总结_等待状态_19

返回顶部


6.5 死锁

多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。 死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。

下面我们通过一些实例来说明死锁现象:
先看生活中的一个实例,2个人一起吃饭但是只有一双筷子,2人轮流吃(同时拥有2只筷子才能吃)。某一个时候,一个拿了左筷子,一人拿了右筷子,2个人都同时占用一个资源,等待另一个资源,这个时候甲在等待乙吃完并释放它占有的筷子,同理,乙也在等待甲吃完并释放它占有的筷子,这样就陷入了一个死循环,谁也无法继续吃饭。。。
在计算机系统中也存在类似的情况。例如,某计算机系统中只有一台打印机和一台输入设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2
所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

package Lock;

public class DeadLock implements Runnable{

static Object a = new Object();
static Object b = new Object();
Boolean flag;

public DeadLock(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
synchronized (a) {
System.out.println("执行a锁");
synchronized (b) {
System.out.println("执行b锁");
}
}
}
} else {
while (true) {
synchronized (b) {
System.out.println("执行b锁");
synchronized (a) {
System.out.println("执行a锁");
}
}
}
}
}

public static void main(String[] args) {

DeadLock thread1 = new DeadLock(true);
DeadLock thread2 = new DeadLock(false);

new Thread(thread1,"a").start();
new Thread(thread2,"b").start();

}

}

在这里,我们创建了两个线程,通过有参构造方法来区分不同的状态,并同时创建了两个锁对象。线程a状态为true,线程b状态为false。当线程启动时,a线程为true首先执行获取a锁,同时b线程也执行获取了b锁,此时a、b两所都处于锁死状态;但是a线程执行代码又需要去获取b锁,b线程也需要获取a锁,由于a、b线程的代码均未执行完毕,当前所获的锁不能够被释放,于是就出现a线程死咬a锁不放,b线程死咬b锁不放,出现僵持的状态,此时就发生了死锁。

关于死锁的详细解释参见大佬博客~~~

返回顶部


七、多线程通信

多线程的通信保证了线程任务的协调进行,就拿商品生产出售来说,肯定是当前库存中有什么商品才能够出售该商品,不可能出现出售未生产的商品。在java的Object类中,提供了如下几个方法用于解决线程间的通信问题,由于java中的所有类都是Object的子类,所以任何类实例对象都可以直接使用这些方法。

方法

说明

wait()

导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。

notify()

唤醒正在等待对象监视器的单个线程。

notifyAll()

唤醒正在等待对象监视器的所有线程。

package ThreadConstraction;

import java.util.ArrayList;
import java.util.List;

public class good {

public static void main(String[] args) {

// 定义一个集合类,模拟存储生产的商品
List<Object> good = new ArrayList<>();

// 记录线程执行前统一的开始时间
Long begin = System.currentTimeMillis();

// 创建一个生产者线程,用于生产并存入商品集合
Thread create = new Thread(()->{
int num = 0;
while (System.currentTimeMillis() - begin <= 100){
synchronized (good){
if (good.size()>0){
try{
good.wait(); //使当前线程放弃同步锁
}catch(Exception e){
e.printStackTrace();
}
} else {
good.add("商品"+num++);
System.out.println("生产商品"+num);
}
}
}
},"生产者");

// 创建一个消费者线程,用于消费并将商品从商品集合中删除
Thread customer = new Thread(()->{
int num = 0;
while (System.currentTimeMillis() - begin <=100){
synchronized (good){
if (good.size()<=0){
// 如果商品数不足就唤醒生产者进行生产
try{
good.notify();
}catch(Exception e){
e.printStackTrace();
}
}else {
good.remove("商品" + num++);
System.out.println("消费商品" + num);
}
}
}
},"消费者");

// 启动线程
create.start();
customer.start();

}
}

在生产者和消费者现成的两个执行任务中同时使用synchronized关键字同步商品生产和消费,之后每生产出商品,就调用wait()方法将当前线程置于等待状态,等待消费者线程进行消费,当消费者线程执行任务发现没有商品可以消费时,就会调用notify()方法唤醒对应同步锁上 等待的生产者线程,让生产者线程继续生产商品,这样一来生产者与消费者线程轮流有序执行。

【Java】多线程总结_等待状态_20

返回顶部


八、线程池

对于复杂的任务来说,频繁地手动式创建、管理线程是不可取的。因为线程对象使用了大量的内存,在大规模的应用程序中,创建、分配和释放多线程对象会产生大量内存管理开销,因此java提供的线程池来创建多线程会进一步优化线程管理。

8.1 Executor接口实现线程池管理

从JDK5开始,在java.util.concurrent包下增加了Executor接口及其子类,允许使用线程池技术来管理线程并发问题。Executor接口提供了一个常用的ExecutorService子接口,通过该子接口可以方便地进行线程池管理。

主要步骤:

  • 创建一个实现Runnable或callable接口的实现类,同时重写run或call方法
  • 创建Runnable或callable接口的实现类对象
  • 使用Executors线程执行器类创建线程池
  • 使用ExecutorService执行器服务类的submit方法将Runnable或Callable接口的实现类对象提交到线程池进行管理
  • 线程执行任务完毕后,使用shutdown方法关闭线程池

本案例的线程池是通过Executor的newCachedThreadPool()方法创建的,对于JDK5中的Executor线程执行器工具类来说,一共提供了4中方法来创建用于不同需求的线程池。

方法

说明

newCachedThreadPool()

创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。(创建一个可扩展线程池的执行器,该线程执行器用于启动许多短期任务的应用程序)

newFixedThreadPool(int nThreads)

创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。

newSingleThreadExecutor()

创建一个使用从无界队列运行的单个工作线程的执行程序。

newScheduledThreadPool(int corePoolSize)

创建一个定长线程池,可以调度命令在给定的延迟之后运行,或定期执行。

package ThreadPool;

import javax.swing.plaf.synth.SynthOptionPaneUI;
import java.util.concurrent.*;

public class ExecutorTest implements Callable<Object>{

// 重写Callable接口的call方法
@Override
public Object call() {

int i = 0;
try{
while (i++<5){
System.out.println(Thread.currentThread().getName()+"的run()方法正在执行~");
}
}catch(Exception e){
e.printStackTrace();
}
return i;
}

public static void main(String[] args) throws InterruptedException, ExecutionException {

// 创建接口实现类的实例对象
ExecutorTest executor = new ExecutorTest();

// 使用executors线程执行器创建线程池
ExecutorService service = Executors.newCachedThreadPool();

// 将Callable接口实现类对象提交到线程池进行管理
Future<Object> result1 = service.submit(executor);
Future<Object> result2 = service.submit(executor);

// 关闭线程池
service.shutdown();

// 对于有返回值的线程任务,获取执行结果
System.out.println("thread-1:"+result1.get());
System.out.println("thread-2:"+result2.get());

}
}

在创建一个自定义的线程池时,系统会默认生成线程池名称为pool-1,在该线程池中管理有两个默认生成名称的线程thread-1和thread-2,同时还可以获取两个线程的执行结果。

【Java】多线程总结_java_21

返回顶部


8.2 CompletableFuture类实现线程池管理

在使用Callable接口实现多线程时,会使用到FutureTask类对线程执行结果进行管理和获取,由于该类在获取结果时是通过阻塞或者轮询的方式,违背多线程的初衷且耗费过多资源。为此,JDK8中对futureTask进行了改进,增加了一个函数式异步编程辅助类CompletableFuture,该类同时实现了Future接口和CompletionStage接口。

【Java】多线程总结_接口实现_22


在获取CompletableFuture对象的静态方法中,runAsync()和supplyAsync()方法的本质区别就是获取ComplrtablrFuture对象是否含有计算的结果(类似Runnable和Callable接口的区别)。

package ThreadPool;

import java.util.concurrent.CompletableFuture;

public class CompletableFutureTest {


public static void main(String[] args) throws Exception{

// 创建第一个线程,执行1-5相加
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(()->{

int sum=0,i=0;
while(i++<5){
sum+=i;
System.out.println(Thread.currentThread().getName()+"线程正在执行...i"+i);
}
return sum;
});

// 创建第二个线程,执行6-10相加
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{

int sum=0,j=5;
while (j++<10){
sum+=j;
System.out.println(Thread.currentThread().getName()+"线程正在执行...j"+j);
}
return sum;
});

// 将两个线程运算结果进行取整
CompletableFuture<Integer> completableFuture3 = completableFuture1.thenCombine(completableFuture2,(result1,result2)->result1+result2);
System.out.println("1到10相加的结果为:"+completableFuture3.get());

}

}

分享一位大佬的CompletableFuture类实现线程池管理博客~~~

返回顶部