Java多线程基础汇总(上)

简介: Java多线程基础汇总(上)

一. 概念


    Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    线程:一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码。通俗点说就是一个应用程序中执行着不同的功能,比如我们用微信聊天,微信运行起来相当于一个进程,而我们使用微信聊天,发朋友圈等等就相当于不同的线程。

    进程:是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,通俗点说相当于一个跑起来的程序。

    举个例子:就拿吃饭这件事来说,我们在吃饭的同时,可以有玩手机,看电视,聊天等等一系列的行为,我们把吃饭这件事就可以看做是一个进程,而玩手机,看电视,聊天这样的行为可以看做是一个独立的线程,当这些行为一起进行的时候,我们就可以看做是一个多线程任务



二.线程的创建


转载我之前的blog:Java线程的创建_Bc_小徐的博客-CSDN博客


三. Thread类的常见方法


1.启动一个线程

如果要启动一个线程,我们调用Thread类的start方法来解决

publicstaticvoidmain(String[] args) {
Threadthread=newThread(() -> {
System.out.println("线程1");
        });
//启动线程thread.start();
System.out.println("线程2");
    }

当我们调用start方法后,这个才真正的在操作系统的底层创建出了一个线程,这个线程就进入就绪状态了,等待cpu的调度;


2.终止一个线程

在多线程中,我们通常通过设置一个标志位来终结一个线程的运行

classMythreadimplementsRunnable{
//设置一个标志位,通过改变标志位的值,来终止线程的进行publicbooleanflag=true;
@Overridepublicvoidrun() {
while (flag){
for (inti=0; i<999; i++) {
System.out.println(i);
            }
        }
    }
}
publicclassDemo3 {
publicstaticvoidmain(String[] args) {
Mythreadmythread=newMythread();
Threadthread=newThread(mythread);
thread.start();
for (inti=0; i<999; i++) {
System.out.println(i);
if(i==666){
//改变标志位的值mythread.flag=false;
System.out.println("该线程该停止了");
            }
        }
    }
}


3.等待一个线程

在线程里面,有的时候我们需要等待一个线程先执行完,再进行下一个线程的进行,此时,Thread标准库里提供了一个join方法,让我们去实现:

publicclassDemo4 {
//等待一个线程publicstaticvoidmain(String[] args) throwsInterruptedException {
Threadthread=newThread(() -> {
System.out.println("线程1");
        });
//启动线程thread.start();
//在main函数里调用join,意思是让main线程等待thread线程执行完,main线程才执行thread.join();
System.out.println("主线程");
    }
}

ce62311852034068936b15c0db407837.png


四. 线程安全问题


1.导致线程安全的原因:

关于线程安全的问题我认为在Java中一直是个重要的问题,我们写程序一直要确保万无一失,关于线程安全可以举个例子来看:

classCounter {
publicintcount=0;
publicvoidadd() {
count++;
    }
publicintgetCount() {
returncount;
    }
}
publicclassDemo5 {
publicstaticvoidmain(String[] args) throwsInterruptedException {
Countercounter=newCounter();
Threadthread1=newThread(() -> {
for (inti=0; i<1000; i++) {
counter.add();
            }
        });
Threadthread2=newThread(() -> {
for (inti=0; i<1000; i++) {
counter.add();
            }
        });
//启动线程thread1.start();
thread2.start();
//等待线程thread1.join();
thread2.join();
System.out.println(counter.getCount());
    }

上述代码中,我们利用线程来对count 进行++的操作,并且让thread1和thread2线程各循环1000次,如果按照正常的逻辑执行的话,我们最后输出的一个结果是2000,但是真正输出的结果并不是这样的,如下图可以看到,我们分别打印了3次,而每次的结果都是不同的,所以

21731b00568d44f3a4c1068adf072c25.png

aa90c6ad14cf4fb8b26285dc75dbdb01.png

dd255c1dec2d42d3811bce6c62bda43d.png

这里就涉及到线程安全的问题了,出现这种Bug的原因实际上主要和线程调度的随机性有关,针对这个count++的操作,站在CPU的角度分析,是相当于三条指令来进行完成的:

第一步:Load操作:把内存中的数据读取到CPU寄存器中

第二步:Add:把寄存器的值进行+1的运算

第三步:Save:把寄存器的值写回到内存中

这三个步骤在两个线程中并发也是随机进行的,相当于线程的调度是随机的,它是一个大的范围,而对于++这个操作,这三条指令也是随机执行的,它在大的范围之下的小的范围,这就导致了为什么最终的结果和我们预期的不一样;

977ed7145de34a56b1b06448443169bf.png

上述的每一个系列号都是一种情况:

  拿第二个图来说,thread1 的 load,add,save先执行,当thread1的三条指令全部执行完,此时就进行了一个加1的操作,thread1执行完,再执行thread2 的三条指令;

  拿第三个图来说,thread1 的 load 指令先执行,再执行 thread2 的loaf add save 的操作,此时 thread1 的加1操作并没有执行完,所以不会加1,当执行完 thread2 的三条操作后,再执行thread1的剩下两条指令(add,save),当这两条指令执行完之后,才是一个完整的加1操作;

这也是导致线程安全的主要原因:它不是按顺序执行的,而是随机调度的;


2.如何解决线程安全问题

2.1  synchronized关键字

   上述的线程不安全问题,主要就是三条指令随机调度导致的,为了解决这一问题,我们可以对其加锁操作,将这三条指令的操作封装起来,意思就是要执行就一起执行,不能分散的执行,这样也是确保了操作的原子性,使得即使2个线程并发执行,但是要执行的操作是原子的,所谓原子,就是不可分割的;


Java里对于加锁的操作是使用synchronized关键字:

在被synchronzied修饰的代码块中,会触发加锁,出了synchronized代码块,就会解锁;

publicvoidadd() {
synchronized (this){
count++;
        }
    }

关于synchronized的写法:

如果对于普通方法:

第一种写法,锁住的对象就是当前的操作publicvoidadd() {
synchronized (this){
count++;
        }
    }
第二种写法,直接修饰这个方法synchronizedpublicvoidadd(){
count++;
    }

如果对于静态成员方法:

当修饰静态方法的时候第一种写法,synchronized后面锁住的对象就是当前类对象publicstaticvoidadd(){
synchronized (Counter.class){
        }
    }
第二种写法,直接修饰这个静态方法synchronizedpublicstaticvoidadd(){
    }

2.2  volatile关键字

导致线程安全的原因有很多,不止是原子性这一方面,例如下面一段代码:

publicclassDemo6 {
privatestaticintret=0;
publicstaticvoidmain(String[] args) {
Threadthread1=newThread(() -> {
while (ret==0) {
            }
System.out.println("线程结束");
        });
Threadthread2=newThread(() -> {
Scannerscan=newScanner(System.in);
ret=scan.nextInt();
        });
thread1.start();
thread2.start();
    }
}

上述代码中,我们通过输入ret的值来结束线程的进行,当我们输入一个非零的数,按照代码的逻辑,就可以结束线程1,打印线程结束了,但是预期的结果和我们实际输出的结果并不一样,当我们运行程序可以看到:

2cee8547277440e1960bef460772b6d6.png

线程始终没有结束,并且也没有打印线程结束,导致这一原因就是编译器优化做的决策,

a9068c3ac0b74948a96098e7d76521d4.png

所以这里相当于无论你怎么输入,它始终与第一次拿到的值进行比较,为了解决这一问题,我们就要使用volatile关键字来解除编译器优化,以确保拿到的值是你下一次输入的值,而不是和第一次拿到的值进行比较了;

5be5819561574a9885f21cc18b378f15.png

加上volatile关键字之后,输出的结果就和我们预期的一样了,被volatile修饰的变量就禁止编译器优化,保证每次都是从内存中重新读取数据;

注:但是volatile 不能保证原子性,它使用的场景是一个线程读,一个线程写;


3. wait 和 notify

    由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序,此时wait和notify的作用就来了,我们可以通过这两个关键字来控制线程的先后进行:

publicclassDemo7 {
publicstaticvoidmain(String[] args) throwsInterruptedException {
Objectlocker=newObject();
Threadthread1=newThread(() -> {
try {
System.out.println("线程1中的wait开始");
synchronized (locker) {
locker.wait();
                }
System.out.println("线程1中的wait结束");
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
        });
//启动线程thread1.start();
//线程休眠Thread.sleep(1000);
Threadthread2=newThread(() -> {
synchronized (locker) {
System.out.println("线程2中的notify开始");
locker.notify();
System.out.println("线程2中的notify结束");
            }
        });
//启动线程thread2.start();
    }
}

上述代码中我们就使用了wait和notify这两个关键字,让线程1先启动,再使用wait让线程1进入阻塞状态,再执行线程2,等线程2执行完,notify会起到一个唤醒的作用,唤醒线程1,让线程1执行完,这样就灵活的控制了线程执行的顺序;


wait主要做的事有:

1.解锁(所以在使用wait之前,要对其进行加锁,不然怎么解锁)

2.进入阻塞状态

3.等待被唤醒,重新拿到锁

notify的作用就是唤醒等待的方法


4.wait 和 sleep的区别(面试题)

1.wait需要搭配 synchronized 使用,而 sleep 不需要

2.wait 是 object 的方法,sleep 是 Thread的方法

目录
相关文章
|
5天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
44 17
|
16天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
1天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
18天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
18天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
18天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
42 3
|
18天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
104 2
|
26天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
48 6
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
55 3