资讯专栏INFORMATION COLUMN

Java 并发编程系列之带你了解多线程

Elle / 3426人阅读

摘要:的内置锁是一种互斥锁,意味着最多只有一个线程能持有这种锁。使用方式如下使用显示锁之前,解决多线程共享对象访问的机制只有和。后面会陆续的补充并发编程系列的文章。

早期的计算机不包含操作系统,它们从头到尾执行一个程序,这个程序可以访问计算机中的所有资源。在这种情况下,每次都只能运行一个程序,对于昂贵的计算机资源来说是一种严重的浪费。

操作系统出现后,计算机可以运行多个程序,不同的程序在多带带的进程中运行。操作系统负责为各个独立的进程分配各种资源。并且不同的进程间可以通过一些通信机制来交换数据,比如:套接字、信号处理器、共享内存、信号量等。

一、了解多线程

1.1 进程与线程

想必大家都听说过这两个名词,它们之间有什么联系与不同呢?

记得当时上操作系统课时,书上有这么一句话:进程是独立拥有 cpu 资源的最 小单位,线程是接受 cpu 调度的最小单位。网上看了那么多的解释,还是觉得书上说的最简单明了。

进程中可以同时存在多个程序控制流程的线程,线程会共享进程范围内的资源,因此一个进程可以有多个线程。打个比方,进程就像一个大工厂,而线程就是工厂的工人,多个工人负责协同完成工厂的任务。

1.2 多线程的优势

发挥多处理器的强大能力

简化建模过程

简化异步事件处理机制

响应更灵敏的用户界面

1.3 多线程安全性问题

多线程是一把双刃剑,使用多线程时意味着对开发人员有一定的技术要求。可见学会了多线程技术,就能写出更优雅的代码了,哈哈~

多个线程同时访问意味着“共享”变量,这些“共享”变量往往在其生命周期内是可变的,这样以来就极易出现多线程安全性问题。

什么是线程安全,这里给一个比较准确的定义:当多个线程访问某各类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。

二、Java 内存模型

为什么要在多线程专题里涉及到 Java 内存模型呢?我觉得它可以帮助我们更好的理解多线程的工作机制。

比如很多人都听说过或了解过 volatile 关键字,都知道它能保证内存可见性问题,但是理解起来总是太过抽象。如果你了解了 Java 内存模型,它可以很好的帮助你理解这些问题。

Java 内存模型规定了所有的变量都存储在主内存中(Main Memory)中(可以类比为物理硬件的主内存)。每条线程都有自己的工作内存(Working Memory),线程的工作内存中保存了主内存中的变量的拷贝,线程对变量的所有操作(读取、赋值)都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。

不同的线程之间无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

如果你对 Java 内存区域了解的话,很容易就会想工作内存、主内存与 Java 内存区域中的堆、栈、方法区是否有一定关系呢?

如果在实例变量的角度来看,主内存对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次来看,主内存直接对应于物理硬件的内存。

三、解决线程安全性问题

使用 Java 创建线程的几种方式这里就不再赘述了,我们来了解几种处理多线程安全性问题的方法。

3.1 synchronized 关键字

Java 提供了一种内置的锁机制(synchronized 关键字)来保证原子性与内存可见性。关于原子性与内存可见性问题会在讲述 volatile 关键字时详细的介绍,感兴趣的可以关注后面的文章。

Java 的内置锁是一种互斥锁,意味着最多只有一个线程能持有这种锁。当线程 A 和线程 B 同时访问临界资源,如果线程 A 获取锁,线程 B 就必须等待或阻塞,A 不释放 B 就只能等待下去。

由于每次只有一个线程执行内置锁内的代码,因此被 synchronized 关键字保护的临界资源会以原子(一组不可分割的单元)的方式执行,多个线程间执行时不会受到干扰,原子性与数据库事务有相同的含义。

synchronized 关键字可以用来修饰方法和代码块,如果修饰非静态方法和同步代码块,使用的锁是当前对象,如果修饰静态方法和静态代码块,使用的是当前类的 Class 对象作为锁。

使用方式如下

public class SyncTest {
    private Integer num = 1;
    
    public int numAutoIncrement() {
        synchronized (this) {
            return num ++;
        }
    }

    public static void main(String[] args) {
        SyncTest syncTest = new SyncTest();

        // 开启 10 个线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> 
                    System.out.println(Thread.currentThread().getName() 
                            + ":" + syncTest.numAutoIncrement())
            ).start();
        }
    }
}

3.2 使用原子类

jdk1.5 之后 java.util.concurrent.atomic 包下引入了诸如 AtomicIntegerAtomicLong 等特殊的原子性变量类。

这些变量类底层使用 CAS 算法与 volatile 关键字确保对所有的计数器操作都能保证原子性与可见性,也就解决了多线程安全问题。

使用方式如下

public class SyncTest {
    private AtomicInteger atomicInteger = new AtomicInteger(1);
    
    public int numAutoIncrement() {
        return atomicInteger.getAndIncrement();
    }

    public static void main(String[] args) {
        SyncTest syncTest = new SyncTest();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> 
                    System.out.println(Thread.currentThread().getName() 
                            + ":" + syncTest.numAutoIncrement())
            ).start();
        }
    }
}

3.3 使用显示 Lock 锁

jdk1.5 之前,解决多线程共享对象访问的机制只有 synchronizedvolatile。jdk1.5 新增了一种新的机制:ReentrantLockReentrantLock 并不是为替代内置锁而生的,当内置锁机制不适用时,可以考虑使用这种更灵活的加锁机制。

使用方式如下

public class SyncTest {
    private Lock lock = new ReentrantLock();
    private Integer num = 1;
    
    public int numAutoIncrement() {
        lock.lock();
        try {
            return num++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SyncTest syncTest = new SyncTest();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> 
                    System.out.println(Thread.currentThread().getName() 
                            + ":" + syncTest.numAutoIncrement())
            ).start();
        }
    }
}

从上面的代码可以看出来,使用显示锁机制相对内置锁要麻烦一些,使用时要注意必须在 finally 语句块中释放锁,否则当出现异常时,获取到的锁永远不会被释放,在代码中存在这种情况无疑是一种定时炸弹。

显示锁相较于内置锁还提供了等待可中断、公平与非公平等功能,当然还有一个优点是显示锁更加灵活。

PS:
这篇博文中很多知识点拿出来都可以多带带写一篇文章,这里只是总结了一部分重要的知识点,先让大家对多线程有一个感性的认识。后面会陆续的补充并发编程系列的文章。如果大家觉得写得还行的话,请持续关注后面的文章。

参考资料

《Java 并发编程实战》
《深入理解 Java 虚拟机》

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77206.html

相关文章

  • jvm原理

    摘要:在之前,它是一个备受争议的关键字,因为在程序中使用它往往收集器理解和原理分析简称,是后提供的面向大内存区数到数多核系统的收集器,能够实现软停顿目标收集并且具有高吞吐量具有更可预测的停顿时间。 35 个 Java 代码性能优化总结 优化代码可以减小代码的体积,提高代码运行的效率。 从 JVM 内存模型谈线程安全 小白哥带你打通任督二脉 Java使用读写锁替代同步锁 应用情景 前一阵有个做...

    lufficc 评论0 收藏0
  • 线程编程完全指南

    摘要:在这个范围广大的并发技术领域当中多线程编程可以说是基础和核心,大多数抽象并发问题的构思与解决都是基于多线程模型来进行的。一般来说,多线程程序会面临三类问题正确性问题效率问题死锁问题。 多线程编程或者说范围更大的并发编程是一种非常复杂且容易出错的编程方式,但是我们为什么还要冒着风险艰辛地学习各种多线程编程技术、解决各种并发问题呢? 因为并发是整个分布式集群的基础,通过分布式集群不仅可以大...

    mengera88 评论0 收藏0
  • 【进阶1-4期】JavaScript深入带你走进内存机制

    摘要:引擎对堆内存中的对象进行分代管理新生代存活周期较短的对象,如临时变量字符串等。内存泄漏对于持续运行的服务进程,必须及时释放不再用到的内存。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导) 本周正式开始前端进阶的第一期,本周的主题是调用堆栈,今天是第4天。 本计划一共28期,每期重点攻克一个面试重难点,如果你还不了解本进阶计划...

    不知名网友 评论0 收藏0
  • 带你了解集合世界的fail-fast机制 和 CopyOnWriteArrayList 源码详解

    摘要:体现的就是适配器模式。数组对象集合世界中的机制机制集合世界中比较常见的错误检测机制,防止在对集合进行遍历过程当中,出现意料之外的修改,会通过异常暴力的反应出来。而在增强循环中,集合遍历是通过进行的。 前言 学习情况记录 时间:week 2 SMART子目标 :Java 容器 记录在学习Java容器 知识点中,关于List的重点知识点。 知识点概览: 容器中的设计模式 从Array...

    young.li 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<