Java多线程,原子性(synchronized与volatile以及Atomic的区别)

本文详细讲解了Java多线程概念,包括线程生命周期、并行与并发的区别、进程与线程的关系,以及多线程在Java中的实现方式,如继承Thread和实现Runnable接口。此外,还探讨了线程池、volatile、synchronized、原子性和并发工具类,如CountDownLatch和Semaphore。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程概念
1.是指同一时间运行多个应用程序。
2.CPU处理多个程序时,实际上是在多个程序中快速切换,让使用者感觉是在同时运行,对CPU而言还是轮流执行。

线程的生命周期
1.Thread创建对象完成时,该线程对象的生命就开始了。
2.当run()方法执行完毕 或 线程抛出一个未捕获的异常 或 错误时,该线程生命结束。

并行和并发
1.并行:同一时刻,有多个指令在多个CPU上同时执行。
2.并发:同一时刻,有多个指令在同一CPU上交替执行。

进程和线程
1.进程:正在运行的程序。

  1. 独立性:进程是能独立运行的基本单位,也是操作系统分配和调度资源的独立单位。
  2. 动态性:实际是程序的一次执行过程,进程是动态产生,动态消亡的。
  3. 并发性:任何进程可以同其他的进程同时执行。

2.线程:实际上是程序的单个顺序控制流,是一条执行路径。

  1. 单线程:一个进程中只有一条执行路径,那么该进程就是单线程程序。
  2. 多线程:一个进程中有多条执行路径,那么该进程就是多线程程序。

JAVA中多线程的实现
1.继承Thread类实现多线程。

  1. 定义一个类继承Thread。
  2. 重写Thread中的run()方法。
  3. 创建你定义类的对象。
  4. 启动线程。
  5. run()和start()方法的区别:
    1.run()方法只是通过调用对象执行方法,并没有启动一条新的线程。
    2.而start()方法是通过开启一条线程,在新的线程中由JVM去执行此线程的run方法。
  6. start() 方法在底层是被native关键字修饰的,该关键字表示start方法是一个本地方法。它会通过本地操作系统去自动适配并开启一条新线程。这也体现了Java的跨平台性。

2.实现Runnable接口实现多线程。

  1. 定义一个类实现Runnable接口。
  2. 重写run()方法。
  3. 创建你定义类的对象。
  4. 创建Thread类对象,把Runnable接口的实现类对象作为构造参数传递给Thread对象。
  5. 启动线程。

3.利用Callable和Future接口实现多线程。

  1. 定义一个类实现Callable接口。
  2. 重写实现类的call()方法。
  3. 创建Callable接口实现类的对象。
  4. 创建Future实现类FutureTask类对象,把Callable接口的实现类对象作为构造参数传递给FutureTask对象。
  5. 创建Thread类对象,把FutureTask对象作为构造参数传递给Thread对象。
  6. 启动线程。
  7. 再调用FutureTask对象的get方法,就可以获取线程执行后的结果。
import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程执行" + i);
        }

        return "通过"; // 返回值表示线程执行完毕后的结果
    }
}
		// 线程开启后执行里面的call方法
        MyCallable m1 = new MyCallable();

        // 可也获取线程开启后返回的结果,也可以作为参数传递给Thread对象
        FutureTask<String> f1 = new FutureTask<>(m1);

        Thread t1 = new Thread(f1);
        t1.start();

		System.out.println(f1.get()); 
		// get方法获取线程执行后的结果
		// 如果线程没有启动,get方法会死等
		// 所以get方法需要写在线程启动start方法之后

三种实现方式的对比
1.实现接口的方式:

  1. 优点:扩展性强,实现接口的同时还可以继承其他的类。
  2. 缺点:编码相对复杂,不能直接使用Thread类中的方法。

2.继承Thread的方式:

  1. 优点:编码相对简单,可以直接使用Thread类中的方法。
  2. 缺点:扩展性较差,不能继承其他的类。

Thread中的方法
1.获取当前线程的名称:

  1. String getName() // 使用Thread类中的方法 getName() 获取名称。
  2. static Thread currentThread() // 返回正在执行的线程对象的引用
  3. 可以通过当前正在执行的线程,使用该线程中的getName()方法获取名称。
		Thread t = Thread.currentThread();// 获取当前正在运行的线程
        String name = t.getName();// 获取主线程的名称
        System.out.println(name);

打印结果:
main

2.设置线程的名称:

  1. void setName(String name) // Thread类中的方法,改变线程名称。
  2. 创建一个带参数的构造方法,参数传递该线程的名称。调用父类的带参构造方法把线程名称传递给父类,让Thread类给子线程起名字。
public class MyThread extends Thread{
    
    // 带参构造方法
    public MyThread(String name) {
        super.setName(name);
    }
    
}

3.休眠方法:
1.public static void sleep(long l) // 使程序暂停指定毫秒数。

4.守护线程方法:
1.public final void setDaemon(boolean on) // 设置为守护线程。
2.守护线程也称为后台线程,当一个程序只有后台线程时程序结束。

// 当普通线程结束后,守护线程也就没有再执行的意义了。
        m2.setDaemon(true);

注意:守护线程的设定必须在start方法之前设定好,否则会出现IllegalThreadStateException异常,该异常表示线程不处于执行操作的适当状态

5.线程的优先级——线程调度:

  1. 多线程的并发运行:计算机中的CPU在任意时刻只能执行一条语句,每个线程只有获得CPU使用权才能执行代码,各个线程轮流获得CPU使用权,分别执行各自的任务。
  2. 按线程对CPU的使用方式,分为两种调度模型:
    1.分时调度模型:每个线程轮流使用CPU,平均分配每个线程占用CPU的时间片。
    2.抢占式调度模型:优先让优先级高的线程优先使用CPU,如果优先级相同,CPU会随机选择一个线程执行。优先级高的线程获取时间片的几率相对大一些,概率问题,不是必然的。
  3. Java中采用的是抢占式调度模型。
  4. Thread类中有两种关于优先级的方法:
    1.public final void setPriority(int newPriority) // 设置线程优先级的方法。
    2.public final ing getPriority() // 获取线程优先级的方法,java默认优先级为5。
  5. 优先级的范围:MIN = 1; NORM = 5; MAX = 10; 值越大优先级越高。

线程让步
a) 通过yield()方法来实现。该方法使线程暂时停止,转为就绪状态。
b) yield()方法不会将线程阻塞,该方法使当前线程让步后只有优先级跟当前线程相同或比它高的才有资格抢占。
c) 调用yield()方法后若没有优先级高于当前线程或者优先级相同,则当前线程立即重启。

线程插队
a) Thread类中提供了join()方法,因此每个线程都有自己的join()方法。
b) 在当前线程中调用其他线程的join方法,当前线程被阻塞,直到被调用的线程执行结束才继续执行当前线程。

线程安全问题
1.单线程不会出现线程安全问题。
2.多个线程没有访问共享数据,不会出现线程安全问题。
3.多个线程访问同一组数据,会出现线程安全问题。

解决办法:Java引入三种方式实现同步操作

  1. 同步代码块:synchronized关键字修饰的代码块。表示多个线程对这个区域进行互斥访问。
	private final int resource = 100; // 资源

    // 创建一个锁对象
    Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized(obj){ //  共享的操作代码块
                
            }
        }
    }
  1. 同步方法:同步方法默认出入的锁为当前对象
	public synchronized void method(){
        // 可能出现线程安全的操作代码
    }
  1. 锁机制

生产者消费者模型

1.一种多线程的协作模式。
等待和唤醒方法

  1. wait() // 使当前线程等待,直到另一线程调用该对象的notify()或notifyAll()方法 唤醒它继续执行。
  2. notify() // 唤醒正在等待监视器的单个线程。
  3. notifyAll() // 唤醒正在等待监视器的所有线程。

阻塞队列实现等待唤醒机制
在这里插入图片描述

  1. Java中阻塞队列由BlockingQueue类实现
  2. 底层原理:ArrayBlockingQueue底层为数组,有界
  3. LinkedBlockingQueue 底层为链表,无界,实际上不是真正的无界,上界为int最大整型数
		// 参数为阻塞队列的容量
        ArrayBlockingQueue<String> a = new ArrayBlockingQueue<>(1);

        // put方法为生产者往阻塞队列中放入资源,当队列满时等待。
        a.put("apple");

        // take方法为消费者从队列中拿资源,当队列空时等待。
        System.out.println(a.take());

线程池和volatile

线程状态
在这里插入图片描述

线程可以处于以下状态之一:

  1. NEW 尚未启动的线程处于此状态。
  2. RUNNABLE 在Java虚拟机中执行的线程处于此状态。
  3. BLOCKED 被阻塞等待监视器锁定的线程处于此状态。
  4. WAITING 正在等待另一个线程执行特定动作的线程处于此状态。
  5. TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
  6. TERMINATED 已退出的线程处于此状态。

线程池的基本原理
当用户需要实现某一个需求时先在线程池查看有没有已存在的线程,若有直接用线程池的线程对象实现需求,需求实现后将此线程对象归还给线程池。若线程池没有所需对象则创建线程对象,执行完需求后再将此对象放到线程池中。

Java中线程池的操作:
在这里插入图片描述
Executors中的常用方法:

  1. public static ExecutorService newCachedThreadPool() // 创建一个默认的线程池。
  2. public static newFixedThreadPool() // 创建一个指定最多容纳线程数量的线程池。

ExecutorService中的两个常用方法:

  1. void shutdown() // 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。
  2. Future<?> submit(Runnable task) // 提交一个可运行的任务执行,并返回一个表示该任务的未来。

创建线程池
1.newCachedThreadPool

		// 创建一个线程池对象,默认此线程池时空的,默认可以容纳int的最大值
        ExecutorService executorService = Executors.newCachedThreadPool();
        // Executors 可以帮我们创建线程池对象。
        // ExecutorService 可以帮助我们控制线程池对象。

        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "执行了");
        });

		// Thread.sleep(1000); // 主线程睡眠。
		
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "执行了");
        });

        executorService.shutdown(); // 停止线程池

打印结果:
pool-1-thread-1执行了
pool-1-thread-2执行了

// 若两个submit中间睡眠一会,
打印结果:
pool-1-thread-1执行了
pool-1-thread-1执行了

// 原因:睡眠时第一个线程已经执行完了,将线程对象归还给线程池。
// 睡醒后 第二次提交查看到线程池中已经存在符合需求的线程对象,则直接用不创建新的,所以两个线程的名字是一样的。

2.newFixedThreadPool

		// 参数表示线程池中的最大值,而不是初始值
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
        System.out.println(pool.getPoolSize()); // 打印线程池中存在的线程数量

        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "执行了");
        });

        System.out.println(pool.getPoolSize()); // 打印线程池中存在的线程数量

        executorService.shutdown();

以上两种创建线程池的方法都是java中自带的。

自己创建一个线程池
ThreadPoolExecutor类
1.ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
// 创建一个新 ThreadPoolExecutor给定的初始参数。

  1. 参数1:核心线程数量。创建后不能被销毁,直到线程池释放。
  2. 参数2:线程池的最大线程数。
  3. 参数3:空闲时间值。临时线程空闲多长时间被销毁。
  4. 参数4:空闲时间单位。如毫秒,秒等
  5. 参数5:阻塞队列。也是任务队列。当有空闲线程时,从这个队列中获取任务并执行。
  6. 参数6:创建线程的方式。按照默认方式创建线程对象。
  7. 参数7:当执行任务过多时的解决方案。也称拒绝策略。
    1.什么时候拒绝。当提交任务 > 最大线程数量 + 任务队列时。
    2.怎么拒绝。// 所谓绕过线程池的拒绝策略是指,让别的线程去执行。一般是主线程。
    在这里插入图片描述

在这里插入图片描述

        /*DAYS
        时间单位代表二十四小时
        HOURS
        时间单位代表六十分钟
        MICROSECONDS
        时间单位代表千分之一毫秒
        MILLISECONDS
        时间单位为千分之一秒
        MINUTES
        时间单位代表60秒
        NANOSECONDS
        时间单位代表千分之一千分之一
        SECONDS
        时间单位代表一秒
        */
        /*
        Executors.defaultThreadFactory() : 返回用于创建新线程的默认线程工厂。 */
        /*
        * ThreadPoolExecutor.AbortPolicy() : 被拒绝的任务的处理程序,抛出一个 RejectedExecutionException 。
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 2, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        pool.shutdown();

volatile问题
当A线程使用共享数据,B线程没有及时获取最新数据的变化还在使用原来的值。
原因:

  1. 堆内存是唯一的。每一个线程都有自己的线程栈。
  2. 每个线程在使用数据时会先拷贝一份到自己的线程栈中。
  3. 在线程中每一次使用数据,都是从自己的线程栈中获取的。

解决:

  1. volatile关键字:当线程访问该关键字修饰的变量时,该关键字强制线程每次在使用的时候都要去看共享数据的值。

synchronized关键字的操作步骤
在底层被synchronized修饰的代码块会按照以下步骤执行:

  1. 获得线程锁对象。
  2. 将自己的线程栈清空。
  3. 获取最新的共享数据。
  4. 执行代码。
  5. 将修改后的值付给线程栈的变量再传给共享数据。
  6. 释放锁对象。

原子性

概念:在一次操作或多次操作中,要么所有的操作全部得到了执行,要么全都不执行,这些操作是一个不可分割的整体。即为原子性

1.volatile关键字不具有原子性。
2.synchronized关键字具有原子性。但是这种方式速度会很慢。

Atomic
在这里插入图片描述
AtomicInteger
相对其他类,该类比较常用。
构造:

  1. public AtomicInteger(int initialValue) // 初始化一个默认值为指定值的原子型integer
  2. public AtomicInteger() // 初始化一个默认值为0的原子型integer

常用方法:

  1. int get() // 获取值。
  2. int getAndIncrement() // 以原子的方式给当前值加1,注意:返回的是自增前的值。
  3. int IncrementAndGet() // 以原子的方式给当前值加1,注意:返回的是自增后的值。
  4. int addAndGet(int data) // 以原子的方式将输入的值与实例中的值(AtomicInteger中的value)相加,并返回结果。
  5. int getAndset(int value) // 以原子的方式设置为newValue的值,并返回旧值。

AtomicInteger原子性的实现原理
自旋+CAS算法,CAS(内存值,旧的预期值,要修改的值)。
原理:假设有A线程和B线程共同操作一个共享数据n,此时n = 100;A线程先抢到CPU执行权将共享内存 n 的值复制了一份到自己的线程栈中并记录下这个值为旧值100,A线程完成自增操作并记录为即将要修改的值101,这时B线程抢到了CPU的执行权,此时A线程还没有将101赋值到共享内存中去,所以B线程的线程栈同A一样记录旧值100,要修改的值101。A线程准备将101赋值到共享内存中的 n 时又一次读取内存,此时n==100记录为内存值,发现内存值和它记录的旧值一样,则成功将101赋值给n。而此时B也完成了自增操作 记录了要修改的值为101,B准备将101赋值给n时,先查看此时的共享内存n ,发现 n != 它记录的旧值100,则修改失败,什么也不操作。 自旋发生:B线程再次将共享内存中的 n = 101 读到自己的线程栈中记录旧值101,完成自增得到要修改的值102,将102准备赋值给共享内存 n 时发现此时的n == 它记录的旧值101,修改成功,内存中n变为102。

上述比较的过程即为CAS算法,发现旧值与内存当前值不相等再次去克隆共享内存到自己线程栈的这一过程 即为自旋。

synchronized和CAS的区别——悲观锁和乐观锁
1.synchronized比较“悲观”,它认为总会有线程来抢共享资源并执行操作,所以synchronized需要对共享资源上同步锁,等我用完别人再用。即:悲观锁。
2.CAS比较“乐观”,它认为每次获取数据的时候别人都没有操作共享资源,所以不会上锁,只不过在修改共享数据的时候多做了一步检查,检查别人有没有操作过共享数据,若没有则复制成功,若有则自旋。即:乐观锁

并发工具类

Hashtable
1.线程安全:所有方法都用synchronized修饰。
2.效率低下。

HashMap
1.线程不安全。

ConcurrentHashMap在JDK1.7中的原理
1.底层:哈希表结构。
2.构造原理:默认创建一个长度为16无法扩容,加载因子为0.75的数组名为Setment[],再创建一个长度为默认2的小数组,将这个小数组的首地址放到Setment的0索引位置,Setment的其余索引值均为null。
3.添加元素:根据键的hash值计算出索引,判断该处的值是否为null,若是,则在该索引位置创建和0索引位置一样的小数组并将地址记录在该索引处,然后在小数组中通过元素的键进行二次hash计算出小数组索引,若是0并且此位置是null则存入,若是0但不是null则将新值以链表的形式头插形成哈希桶,若是1则小数组扩容至4(根据加载因子计算得出何时扩容),再存。重复此过程

4.线程安全:由ConcurrentHashMap的添加原理。当线程得到Setment数组时哈希计算得到索引然后进入小数组中上锁,保证线程安全的同时不影响其他索引位置的访问。

ConcurrentHashMap在JDK1.8中的原理
1.底层:数组+链表+红黑树
2.线程安全:通过CAS和synchronized代码块实现,当一个线程进来时通过哈希计算得到的索引对应的链表被synchronized锁住,实现线程安全,同时其他线程还可以操作其他索引,这样保证线程安全的同时提升操作效率。
3.构造原理:默认创建一个长度为16可以扩容,加载因子为0.75的数组。添加时使用CAS算法,当链表长度达到8以上时,该链表转换为红黑树。

CountDownLatch类
适用场景:某一线程需要等待其他线程都执行完再执行。
1.构造:public CountDownLatch(int count) // 等待线程数量。
2.常用方法:

  1. public void await() // 让线程等待。
  2. public void countDown() // 当前线程执行完毕。

SemaPhore类
适用场景:可以理解为管理员对象可以发指定个数个通行证,有通行证的执行代码,没有的等待发放通行证。
可以控制访问特定线程的数量。
1.构造:SemaPhore(int num) // 允许同时执行的最大线程数量
2.方法:

  1. acquire() // 获得进入semaPhore对象的权限,最多可被num个获得。(发放通行证)
  2. release() // 归还权限。(归还通行证)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值