【JavaSE】Java基础语法(三十五):多线程实战(2)

简介: 3. 线程死锁概述死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的.产生条件

3. 线程死锁

概述

死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的.

产生条件

  • 多个线程
  • 存在锁对象的循环依赖

4. 线程的状态

线程的状态

f7611850b6654f5cb6576c2c466b29ed.png

e95d3860e354461dbf6dfd4ee5ab3252.png

线程通信

线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法,如下:

等待方法

  • void wait() 让线程进入无限等待。
  • void wait(long timeout) 让线程进入计时等待
  • 以上两个方法调用会导致当前线程释放掉锁资源。

唤醒方法:

  • void notify() 唤醒在此对象监视器(锁对象)上等待的单个线程。
  • void notifyAll() 唤醒在此对象监视器上等待的所有线程。
  • 以上两个方法调用不会导致当前线程释放掉锁资源。

注意:

等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中调用)

等待和唤醒方法应该使用相同的锁对象调用

5. 线程池

5.1 线程使用存在问题

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

如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源。

5.2 线程池介绍

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

5.3 线程池使用的大致流程

创建线程池指定线程开启的数量

提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。

线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。

如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任。

5.4 线程池的好处

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

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

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

5.5 Java 提供的线程池

  • java.util.concurrent.ExecutorService 是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了

获取线程池我们使用工具类 java.util.concurrent.Executors的静态方

public static ExecutorService newFixedThreadPool (int num) : 指定线程池最大线程池数量获取线程池

线程池ExecutorService的相关方法

Future submit(Callable task)

Future<?> submit(Runnable task)

关闭线程池方法(一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭)

void shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。

5.6 线程池处理 Runable 任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
    1 需求 :
        使用线程池模拟游泳教练教学生游泳。
        游泳馆(线程池)内有3名教练(线程)
        游泳馆招收了5名学员学习游泳(任务)。
    2 实现步骤:
        创建线程池指定3个线程
        定义学员类实现Runnable,
        创建学员对象给线程池
 */
public class Test1 {
    public static void main(String[] args) {
        // 创建指定线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        // 提交任务
        threadPool.submit(new Student("小花"));
        threadPool.submit(new Student("小红"));
        threadPool.submit(new Student("小明"));
        //  threadPool.shutdown();// 关闭线程池
    }
}
class Student implements Runnable {
    private String name;
    public Student(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        String coach = Thread.currentThread().getName();
        System.out.println(coach + "正在教" + name + "游泳...");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(coach + "教" + name + "游泳完毕.");
    }
}

5.7 线程池处理 Callable 任务

import java.util.concurrent.*;
/*
    需求: Callable任务处理使用步骤
        1 创建线程池
        2 定义Callable任务
        3 创建Callable任务,提交任务给线程池
        4 获取执行结果
    <T> Future<T> submit(Callable<T> task) : 提交Callable任务方法    
    返回值类型Future的作用就是为了获取任务执行的结果。
    Future是一个接口,里面存在一个get方法用来获取值
    练一练:使用线程池计算 从0~n的和,并将结果返回
 */
public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建指定线程数量的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        Future<Integer> future = threadPool.submit(new CalculateTask(100));
        Integer sum = future.get();
        System.out.println(sum);
    }
}
// 使用线程池计算 从0~n的和,并将结果返回
class CalculateTask implements Callable<Integer> {
    private int num;
    public CalculateTask(int num) {
        this.num = num;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;// 求和变量
        for (int i = 0; i <= num; i++) {
            sum += i;
        }
        return sum;
    }
}

6. 自定义线程池

7ccaed97c074408094d6fec21e1a053b.png

91168d1839984e82a78387b33735090e.png

该拒绝策略 在 超出(最大线程+队列数)时报错如下:

6c7ae43b3c0e439ba05f11a95e0e4c23.png


29e4558e11c5dd9da09ef02cae686f84.png

7. volatile

如何保证变量的可见性?

在 Java 中,volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。


volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

如何禁止指令重排序?

在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:


public native void loadFence();

public native void storeFence();

public native void fullFence();


理论上来说,你通过这个三个方法也可以实现和volatile禁止重排序一样的效果,只是会麻烦一些。

volatile 可以保证原子性么?

volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。

很多人会误认为自增操作 inc++ 是原子性的,实际上,inc++ 其实是一个复合操作,包括三步:


读取 inc 的值。

对 inc 加 1。

将 inc 的值写回内存。volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:


线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改(+1),再将inc 的值写回内存。

线程 2 操作完毕后,线程 1 对 inc的值进行修改(+1),再将inc 的值写回内存。这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。

其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、Lock或者AtomicInteger都可以。

8. AtomicInteger

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

量的类型有很多种,所以在 Atomic 包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。本次我们只讲解

使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:


AtomicBoolean: 原子更新布尔类型

AtomicInteger: 原子更新整型

AtomicLong: 原子更新长整型

以上 3 个类提供的方法几乎一模一样,所以本节仅以 AtomicInteger 为例进行讲解,AtomicInteger 的常用方法如下:

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

8.1 AtomicInteger-内存解析

AtomicInteger原理 : 自旋锁 + CAS 算法

CAS算法:

有3个操作数(内存值V, 旧的预期值A,要修改的值B)

当旧的预期值A == 内存值 此时修改成功,将V改为B

当旧的预期值A!=内存值 此时修改失败,不做任何操作

并重新获取现在的最新值(这个重新获取的动作就是自旋)

8.2 悲观锁和乐观锁

synchronized和CAS的区别 :

相同点在多线程情况下,都可以保证共享数据的安全性。

不同点synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁)

cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。

如果别人修改过,那么我再次获取现在最新的值。

如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)9. 并发工具类

9. 并发工具类

9.1 Hashtable

Hashtable出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

9.2 ConcurrentHashMap

ConcurrentHashMap出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

基于以上两个原因我们可以使用JDK1.5以后所提供的ConcurrentHashMap。

总结 :


HashMap是线程不安全的。多线程环境下会有数据安全问题

Hashtable是线程安全的,但是会将整张表锁起来,效率低下

ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样

ConcurrentHashMap1.7原理

956a4347d153dd2ae2af9ede184bc870.png

90232b5f47af6cc93ae555834090a1bb.png

总结 :

  1. 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表。
  2. 计算当前元素应存入的索引。

如果该索引位置为null,则利用cas算法,将本结点添加到数组中。

如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。

当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性。

相关文章
|
2天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
15 9
|
5天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
2天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
4天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
15天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
5天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
15 3
|
5天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
14 1
|
5天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
6天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
28 1